diff --git a/bun.lock b/bun.lock index c9c61b6..423c764 100644 --- a/bun.lock +++ b/bun.lock @@ -60,9 +60,11 @@ "jules-fleet": "./dist/cli/index.mjs", }, "dependencies": { + "@clack/prompts": "^1.0.1", "@octokit/auth-app": "^8.2.0", "citty": "^0.1.6", "glob": "^13.0.6", + "libsodium-wrappers": "^0.8.2", "octokit": "^4.1.3", "yaml": "^2.8.2", "zod": "^3.25.0", @@ -70,6 +72,7 @@ "devDependencies": { "@google/jules-sdk": "^0.0.6", "@types/bun": "^1.3.9", + "@types/libsodium-wrappers": "^0.8.2", "@types/node": "^22.15.0", "typescript": "^5.8.3", "vitest": "^3.2.4", @@ -126,6 +129,10 @@ "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@clack/core": ["@clack/core@1.0.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g=="], + + "@clack/prompts": ["@clack/prompts@1.0.1", "", { "dependencies": { "@clack/core": "1.0.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], @@ -398,6 +405,8 @@ "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.8.2", "", { "dependencies": { "libsodium-wrappers": "*" } }, "sha512-+2IDfSULPUskSjIYfZl9suIUsIE5PXwoKZiE/j0MZWd+M9nEGvJDsk/ztMZKNhL1lBL+1CaypW0dQSjqPW2dMg=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], @@ -698,6 +707,10 @@ "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + "libsodium": ["libsodium@0.8.2", "", {}, "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw=="], + + "libsodium-wrappers": ["libsodium-wrappers@0.8.2", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw=="], + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], "locate-path": ["locate-path@8.0.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg=="], @@ -868,6 +881,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], diff --git a/package-lock.json b/package-lock.json index 20595f5..e91df8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,6 +126,27 @@ "node": ">=6.9.0" } }, + "node_modules/@clack/core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.1.tgz", + "integrity": "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.1.tgz", + "integrity": "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.0.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "dev": true, @@ -3372,6 +3393,21 @@ "dev": true, "license": "MIT" }, + "node_modules/libsodium": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz", + "integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz", + "integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.8.0" + } + }, "node_modules/local-pkg": { "version": "1.1.2", "dev": true, @@ -3919,7 +3955,6 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -4395,6 +4430,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -5392,9 +5433,11 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "@clack/prompts": "^1.0.1", "@octokit/auth-app": "^8.2.0", "citty": "^0.1.6", "glob": "^13.0.6", + "libsodium-wrappers": "^0.8.2", "octokit": "^4.1.3", "yaml": "^2.8.2", "zod": "^3.25.0" @@ -5404,7 +5447,7 @@ }, "devDependencies": { "@google/jules-sdk": "^0.0.6", - "@types/bun": "^1.2.0", + "@types/bun": "^1.3.9", "@types/node": "^22.15.0", "typescript": "^5.8.3", "vitest": "^3.2.4" diff --git a/packages/fleet/package.json b/packages/fleet/package.json index 05c9465..e00bee6 100644 --- a/packages/fleet/package.json +++ b/packages/fleet/package.json @@ -43,9 +43,11 @@ "access": "public" }, "dependencies": { + "@clack/prompts": "^1.0.1", "@octokit/auth-app": "^8.2.0", "citty": "^0.1.6", "glob": "^13.0.6", + "libsodium-wrappers": "^0.8.2", "octokit": "^4.1.3", "yaml": "^2.8.2", "zod": "^3.25.0" @@ -61,6 +63,7 @@ "devDependencies": { "@google/jules-sdk": "^0.0.6", "@types/bun": "^1.3.9", + "@types/libsodium-wrappers": "^0.8.2", "@types/node": "^22.15.0", "typescript": "^5.8.3", "vitest": "^3.2.4" diff --git a/packages/fleet/src/__tests__/analyze-handler.test.ts b/packages/fleet/src/__tests__/analyze-handler.test.ts index 7502a3a..df2b825 100644 --- a/packages/fleet/src/__tests__/analyze-handler.test.ts +++ b/packages/fleet/src/__tests__/analyze-handler.test.ts @@ -49,7 +49,7 @@ describe('AnalyzeHandler', () => { }); it('auto-injects triage goal when no goal files exist', async () => { - const handler = new AnalyzeHandler(octokit, dispatcher, noop); + const handler = new AnalyzeHandler({ octokit, dispatcher }); const result = await handler.execute({ goalsDir: '/nonexistent/dir', owner: 'o', @@ -65,7 +65,7 @@ describe('AnalyzeHandler', () => { }); it('returns NO_GOALS_FOUND when goal file does not exist', async () => { - const handler = new AnalyzeHandler(octokit, dispatcher, noop); + const handler = new AnalyzeHandler({ octokit, dispatcher }); const result = await handler.execute({ goal: '/nonexistent/goal.md', goalsDir: '.fleet/goals', @@ -90,7 +90,7 @@ describe('AnalyzeHandler', () => { const goalPath = join(dir, 'test-goal.md'); writeFileSync(goalPath, `---\nmilestone: "1"\n---\n\n# Test Goal\n\nDo something.`); - const handler = new AnalyzeHandler(octokit, dispatcher, noop); + const handler = new AnalyzeHandler({ octokit, dispatcher }); const result = await handler.execute({ goal: goalPath, goalsDir: '.fleet/goals', @@ -124,7 +124,7 @@ describe('AnalyzeHandler', () => { const goalPath = join(dir, 'test-goal.md'); writeFileSync(goalPath, '# Test\n\nBody.'); - const handler = new AnalyzeHandler(octokit, failingDispatcher, noop); + const handler = new AnalyzeHandler({ octokit, dispatcher: failingDispatcher }); const result = await handler.execute({ goal: goalPath, goalsDir: '.fleet/goals', diff --git a/packages/fleet/src/__tests__/analyze-triage.test.ts b/packages/fleet/src/__tests__/analyze-triage.test.ts index a31e84c..3dec1d6 100644 --- a/packages/fleet/src/__tests__/analyze-triage.test.ts +++ b/packages/fleet/src/__tests__/analyze-triage.test.ts @@ -60,7 +60,7 @@ describe('Triage Goal Auto-Injection', () => { const octokit = createMockOctokit(); const dispatcher = createMockDispatcher(); - const handler = new AnalyzeHandler(octokit, dispatcher, () => {}); + const handler = new AnalyzeHandler({ octokit, dispatcher }); const result = await handler.execute({ goalsDir: dir, @@ -92,8 +92,8 @@ describe('Triage Goal Auto-Injection', () => { const octokit = createMockOctokit(); const dispatcher = createMockDispatcher(); - const logs: string[] = []; - const handler = new AnalyzeHandler(octokit, dispatcher, (m) => logs.push(m)); + const events: Array<{ type: string }> = []; + const handler = new AnalyzeHandler({ octokit, dispatcher, emit: (e) => events.push(e) }); const result = await handler.execute({ goalsDir: dir, @@ -103,9 +103,9 @@ describe('Triage Goal Auto-Injection', () => { }); expect(result.success).toBe(true); - // Should NOT log the "Using built-in triage goal" message - const usedBuiltIn = logs.some((l) => l.includes('built-in triage')); - expect(usedBuiltIn).toBe(false); + // Should NOT be using built-in triage goal when user has their own + const goalStartEvents = events.filter((e) => e.type === 'analyze:goal:start'); + expect(goalStartEvents.length).toBe(1); const { rmSync } = await import('fs'); rmSync(dir, { recursive: true, force: true }); diff --git a/packages/fleet/src/__tests__/configure-handler.test.ts b/packages/fleet/src/__tests__/configure-handler.test.ts index d1ecbb1..3250474 100644 --- a/packages/fleet/src/__tests__/configure-handler.test.ts +++ b/packages/fleet/src/__tests__/configure-handler.test.ts @@ -31,7 +31,7 @@ describe('ConfigureHandler (Logic Tests)', () => { describe('create labels', () => { it('creates both fleet labels', async () => { const octokit = createMockOctokit(); - const handler = new ConfigureHandler(octokit, () => {}); + const handler = new ConfigureHandler({ octokit }); const result = await handler.execute({ resource: 'labels', action: 'create', @@ -57,7 +57,7 @@ describe('ConfigureHandler (Logic Tests)', () => { }, }); - const handler = new ConfigureHandler(octokit, () => {}); + const handler = new ConfigureHandler({ octokit }); const result = await handler.execute({ resource: 'labels', action: 'create', @@ -83,7 +83,7 @@ describe('ConfigureHandler (Logic Tests)', () => { }, }); - const handler = new ConfigureHandler(octokit, () => {}); + const handler = new ConfigureHandler({ octokit }); const result = await handler.execute({ resource: 'labels', action: 'create', @@ -101,7 +101,7 @@ describe('ConfigureHandler (Logic Tests)', () => { describe('delete labels', () => { it('deletes both fleet labels', async () => { const octokit = createMockOctokit(); - const handler = new ConfigureHandler(octokit, () => {}); + const handler = new ConfigureHandler({ octokit }); const result = await handler.execute({ resource: 'labels', action: 'delete', @@ -126,7 +126,7 @@ describe('ConfigureHandler (Logic Tests)', () => { }, }); - const handler = new ConfigureHandler(octokit, () => {}); + const handler = new ConfigureHandler({ octokit }); const result = await handler.execute({ resource: 'labels', action: 'delete', diff --git a/packages/fleet/src/__tests__/dispatch-handler.test.ts b/packages/fleet/src/__tests__/dispatch-handler.test.ts index 8a25710..3ff0f13 100644 --- a/packages/fleet/src/__tests__/dispatch-handler.test.ts +++ b/packages/fleet/src/__tests__/dispatch-handler.test.ts @@ -64,7 +64,7 @@ describe('DispatchHandler', () => { it('returns empty result when no fleet issues', async () => { const octokit = createMockOctokit({ openIssues: [] }); const dispatcher = createMockDispatcher(); - const handler = new DispatchHandler(octokit, dispatcher, noop); + const handler = new DispatchHandler({ octokit, dispatcher }); const result = await handler.execute({ milestone: '1', @@ -95,7 +95,7 @@ describe('DispatchHandler', () => { ], }); const dispatcher = createMockDispatcher(); - const handler = new DispatchHandler(octokit, dispatcher, noop); + const handler = new DispatchHandler({ octokit, dispatcher }); const result = await handler.execute({ milestone: '1', @@ -134,7 +134,7 @@ describe('DispatchHandler', () => { ], }); const dispatcher = createMockDispatcher(); - const handler = new DispatchHandler(octokit, dispatcher, noop); + const handler = new DispatchHandler({ octokit, dispatcher }); const result = await handler.execute({ milestone: '1', @@ -179,7 +179,7 @@ describe('DispatchHandler', () => { .mockResolvedValueOnce({ id: 'session-ok' }), }; - const handler = new DispatchHandler(octokit, dispatcher, noop); + const handler = new DispatchHandler({ octokit, dispatcher }); const result = await handler.execute({ milestone: '1', owner: 'o', diff --git a/packages/fleet/src/__tests__/init-handler.test.ts b/packages/fleet/src/__tests__/init-handler.test.ts new file mode 100644 index 0000000..8c85415 --- /dev/null +++ b/packages/fleet/src/__tests__/init-handler.test.ts @@ -0,0 +1,190 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect, vi } from 'vitest'; +import { InitHandler } from '../init/handler.js'; +import type { Octokit } from 'octokit'; +import type { FleetEvent } from '../shared/events.js'; + +/** Builds a mock Octokit with configurable behavior */ +function createMockOctokit(overrides: { + /** If true, createOrUpdateFileContents throws 422 for ALL files */ + allFilesExist?: boolean; + /** If true, createRef throws an error */ + branchCreateFails?: boolean; + /** If true, pulls.create throws */ + prCreateFails?: boolean; +} = {}): Octokit { + return { + rest: { + git: { + getRef: vi.fn().mockResolvedValue({ + data: { object: { sha: 'abc123' } }, + }), + createRef: overrides.branchCreateFails + ? vi.fn().mockRejectedValue(new Error('Branch exists')) + : vi.fn().mockResolvedValue({ data: {} }), + }, + repos: { + createOrUpdateFileContents: overrides.allFilesExist + ? vi.fn().mockRejectedValue(Object.assign(new Error('Already exists'), { status: 422 })) + : vi.fn().mockResolvedValue({ data: {} }), + }, + pulls: { + create: overrides.prCreateFails + ? vi.fn().mockRejectedValue(new Error('PR create failed')) + : vi.fn().mockResolvedValue({ + data: { html_url: 'https://github.com/o/r/pull/1', number: 1 }, + }), + }, + }, + } as unknown as Octokit; +} + +const baseInput = { + repo: 'o/r', + owner: 'o', + repoName: 'r', + baseBranch: 'main', +}; + +describe('InitHandler', () => { + it('succeeds when files are committed and PR is created', async () => { + const octokit = createMockOctokit(); + const events: FleetEvent[] = []; + const handler = new InitHandler({ octokit, emit: (e) => events.push(e) }); + + const result = await handler.execute(baseInput); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.prUrl).toBe('https://github.com/o/r/pull/1'); + expect(result.data.filesCreated.length).toBeGreaterThan(0); + } + + // Should have emitted init lifecycle events + const types = events.map((e) => e.type); + expect(types).toContain('init:start'); + expect(types).toContain('init:branch:creating'); + expect(types).toContain('init:branch:created'); + expect(types).toContain('init:pr:creating'); + expect(types).toContain('init:pr:created'); + expect(types).toContain('init:done'); + }); + + it('fails with FILE_COMMIT_FAILED when all files already exist', async () => { + const octokit = createMockOctokit({ allFilesExist: true }); + const events: FleetEvent[] = []; + const handler = new InitHandler({ octokit, emit: (e) => events.push(e) }); + + const result = await handler.execute(baseInput); + + // Should fail — not attempt to create a PR + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.code).toBe('FILE_COMMIT_FAILED'); + expect(result.error.message).toContain('already exist'); + expect(result.error.suggestion).toContain('already initialized'); + } + + // Should NOT have emitted PR events + const types = events.map((e) => e.type); + expect(types).not.toContain('init:pr:creating'); + expect(types).not.toContain('init:pr:created'); + expect(types).not.toContain('init:done'); + + // Should have emitted an error event + expect(types).toContain('error'); + const errorEvent = events.find((e) => e.type === 'error'); + expect(errorEvent).toBeDefined(); + if (errorEvent && errorEvent.type === 'error') { + expect(errorEvent.code).toBe('ALREADY_INITIALIZED'); + } + }); + + it('does not call pulls.create when all files exist', async () => { + const octokit = createMockOctokit({ allFilesExist: true }); + const handler = new InitHandler({ octokit }); + + await handler.execute(baseInput); + + // The PR API should never have been called + expect(octokit.rest.pulls.create).not.toHaveBeenCalled(); + }); + + it('emits file:skipped events for each existing file', async () => { + const octokit = createMockOctokit({ allFilesExist: true }); + const events: FleetEvent[] = []; + const handler = new InitHandler({ octokit, emit: (e) => events.push(e) }); + + await handler.execute(baseInput); + + const skippedEvents = events.filter((e) => e.type === 'init:file:skipped'); + // At least the 3 workflow templates should be skipped + expect(skippedEvents.length).toBeGreaterThanOrEqual(3); + }); + + it('succeeds with partial files (some exist, some new)', async () => { + // First call succeeds, second throws 422, third succeeds, goal succeeds + const createOrUpdate = vi.fn() + .mockResolvedValueOnce({ data: {} }) // analyze.yml — new + .mockRejectedValueOnce(Object.assign(new Error('exists'), { status: 422 })) // dispatch.yml — exists + .mockResolvedValueOnce({ data: {} }) // merge.yml — new + .mockResolvedValueOnce({ data: {} }); // example.md — new + + const octokit = { + rest: { + git: { + getRef: vi.fn().mockResolvedValue({ data: { object: { sha: 'abc' } } }), + createRef: vi.fn().mockResolvedValue({ data: {} }), + }, + repos: { createOrUpdateFileContents: createOrUpdate }, + pulls: { + create: vi.fn().mockResolvedValue({ + data: { html_url: 'https://github.com/o/r/pull/2', number: 2 }, + }), + }, + }, + } as unknown as Octokit; + + const events: FleetEvent[] = []; + const handler = new InitHandler({ octokit, emit: (e) => events.push(e) }); + + const result = await handler.execute(baseInput); + + // Should succeed — some files were created + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.filesCreated.length).toBe(3); // analyze + merge + example + } + + // Should have both committed and skipped events + const types = events.map((e) => e.type); + expect(types).toContain('init:file:committed'); + expect(types).toContain('init:file:skipped'); + expect(types).toContain('init:pr:created'); + }); + + it('returns BRANCH_CREATE_FAILED when branch creation fails', async () => { + const octokit = createMockOctokit({ branchCreateFails: true }); + const handler = new InitHandler({ octokit }); + + const result = await handler.execute(baseInput); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.code).toBe('BRANCH_CREATE_FAILED'); + } + }); +}); diff --git a/packages/fleet/src/__tests__/init-wizard.test.ts b/packages/fleet/src/__tests__/init-wizard.test.ts new file mode 100644 index 0000000..6dc96ee --- /dev/null +++ b/packages/fleet/src/__tests__/init-wizard.test.ts @@ -0,0 +1,229 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { validateHeadlessInputs } from '../init/wizard/headless.js'; +import type { FleetEvent } from '../shared/events.js'; + +// Mock getGitRepoInfo to avoid real git calls +vi.mock('../shared/auth/git.js', () => ({ + getGitRepoInfo: vi.fn().mockResolvedValue({ + owner: 'test-owner', + repo: 'test-repo', + fullName: 'test-owner/test-repo', + }), +})); + +describe('validateHeadlessInputs (Non-Interactive Mode)', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all auth-related env vars + delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_APP_ID; + delete process.env.GITHUB_APP_PRIVATE_KEY; + delete process.env.GITHUB_APP_PRIVATE_KEY_BASE64; + delete process.env.GITHUB_APP_INSTALLATION_ID; + delete process.env.GITHUB_REPOSITORY; + delete process.env.JULES_API_KEY; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('auto-detects repo from git when no --repo or GITHUB_REPOSITORY', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs({}, (e) => events.push(e)); + + expect('success' in result).toBe(false); // Not a fail result + if (!('success' in result)) { + expect(result.owner).toBe('test-owner'); + expect(result.repo).toBe('test-repo'); + } + }); + + it('uses --repo flag when provided', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'flag-owner/flag-repo' }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.owner).toBe('flag-owner'); + expect(result.repo).toBe('flag-repo'); + } + }); + + it('uses GITHUB_REPOSITORY env var', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + process.env.GITHUB_REPOSITORY = 'env-owner/env-repo'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs({}, (e) => events.push(e)); + + if (!('success' in result)) { + expect(result.owner).toBe('env-owner'); + expect(result.repo).toBe('env-repo'); + } + }); + + it('fails when no auth is configured', async () => { + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r' }, + (e) => events.push(e), + ); + + expect('success' in result).toBe(true); + if ('success' in result) { + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toContain('Missing GitHub authentication'); + expect(result.error.message).toContain('--non-interactive'); + } + } + }); + + it('detects PAT auth from GITHUB_TOKEN', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r' }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.authMethod).toBe('token'); + } + const authEvent = events.find((e) => e.type === 'init:auth:detected'); + expect(authEvent).toBeDefined(); + if (authEvent && authEvent.type === 'init:auth:detected') { + expect(authEvent.method).toBe('token'); + } + }); + + it('detects GitHub App auth from env vars', async () => { + process.env.GITHUB_APP_ID = '123'; + process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = 'key'; + process.env.GITHUB_APP_INSTALLATION_ID = '456'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r' }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.authMethod).toBe('app'); + } + }); + + it('--app-id flag overrides GITHUB_APP_ID env var', async () => { + process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = 'key'; + process.env.GITHUB_APP_INSTALLATION_ID = '456'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r', 'app-id': 'flag-id' }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.authMethod).toBe('app'); + expect(process.env.GITHUB_APP_ID).toBe('flag-id'); + } + }); + + it('emits warning when JULES_API_KEY is not set', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + await validateHeadlessInputs({ repo: 'o/r' }, (e) => events.push(e)); + + const skipEvent = events.find( + (e) => e.type === 'init:secret:skipped' && e.name === 'JULES_API_KEY', + ); + expect(skipEvent).toBeDefined(); + if (skipEvent && skipEvent.type === 'init:secret:skipped') { + expect(skipEvent.reason).toContain('Not set'); + } + }); + + it('does not warn about JULES_API_KEY when it is set', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + process.env.JULES_API_KEY = 'jk_test'; + const events: FleetEvent[] = []; + await validateHeadlessInputs({ repo: 'o/r' }, (e) => events.push(e)); + + const skipEvent = events.find( + (e) => e.type === 'init:secret:skipped' && e.name === 'JULES_API_KEY', + ); + expect(skipEvent).toBeUndefined(); + }); + + it('never uploads secrets in non-interactive mode', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + process.env.JULES_API_KEY = 'jk_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs({ repo: 'o/r' }, (e) => events.push(e)); + + if (!('success' in result)) { + expect(Object.keys(result.secretsToUpload)).toHaveLength(0); + } + }); + + it('emits dry-run event with --dry-run flag', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r', 'dry-run': true }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.dryRun).toBe(true); + } + const dryRunEvent = events.find((e) => e.type === 'init:dry-run'); + expect(dryRunEvent).toBeDefined(); + if (dryRunEvent && dryRunEvent.type === 'init:dry-run') { + expect(dryRunEvent.files.length).toBeGreaterThan(0); + } + }); + + it('defaults baseBranch to main', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r' }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.baseBranch).toBe('main'); + } + }); + + it('uses --base flag for baseBranch', async () => { + process.env.GITHUB_TOKEN = 'ghp_test'; + const events: FleetEvent[] = []; + const result = await validateHeadlessInputs( + { repo: 'o/r', base: 'develop' }, + (e) => events.push(e), + ); + + if (!('success' in result)) { + expect(result.baseBranch).toBe('develop'); + } + }); +}); diff --git a/packages/fleet/src/__tests__/merge-handler.test.ts b/packages/fleet/src/__tests__/merge-handler.test.ts index 4861da6..4044f08 100644 --- a/packages/fleet/src/__tests__/merge-handler.test.ts +++ b/packages/fleet/src/__tests__/merge-handler.test.ts @@ -66,7 +66,7 @@ function makePR(number: number, labels: string[] = []) { describe('MergeHandler (Logic Tests)', () => { it('returns empty result when no PRs found', async () => { const octokit = createMockOctokit(); - const handler = new MergeHandler(octokit, () => { }, noopSleep); + const handler = new MergeHandler({ octokit, sleep: noopSleep }); const result = await handler.execute(baseInput); expect(result.success).toBe(true); @@ -95,7 +95,7 @@ describe('MergeHandler (Logic Tests)', () => { }, }); - const handler = new MergeHandler(octokit, () => { }, noopSleep); + const handler = new MergeHandler({ octokit, sleep: noopSleep }); const result = await handler.execute(baseInput); expect(result.success).toBe(true); @@ -128,7 +128,7 @@ describe('MergeHandler (Logic Tests)', () => { }, }); - const handler = new MergeHandler(octokit, () => { }, noopSleep); + const handler = new MergeHandler({ octokit, sleep: noopSleep }); const result = await handler.execute(baseInput); expect(result.success).toBe(true); @@ -160,7 +160,7 @@ describe('MergeHandler (Logic Tests)', () => { }, }); - const handler = new MergeHandler(octokit, () => { }, noopSleep); + const handler = new MergeHandler({ octokit, sleep: noopSleep }); const result = await handler.execute(baseInput); expect(result.success).toBe(true); @@ -191,7 +191,7 @@ describe('MergeHandler (Logic Tests)', () => { }, }); - const handler = new MergeHandler(octokit, () => { }, noopSleep); + const handler = new MergeHandler({ octokit, sleep: noopSleep }); const result = await handler.execute(baseInput); expect(result.success).toBe(false); @@ -226,7 +226,7 @@ describe('MergeHandler (Logic Tests)', () => { }, }); - const handler = new MergeHandler(octokit, () => { }, noopSleep); + const handler = new MergeHandler({ octokit, sleep: noopSleep }); const result = await handler.execute(baseInput); expect(result.success).toBe(true); diff --git a/packages/fleet/src/analyze/handler.ts b/packages/fleet/src/analyze/handler.ts index 5bf519f..aa54fef 100644 --- a/packages/fleet/src/analyze/handler.ts +++ b/packages/fleet/src/analyze/handler.ts @@ -18,6 +18,7 @@ import { globSync } from 'glob'; import type { Octokit } from 'octokit'; import type { AnalyzeInput, AnalyzeResult, AnalyzeSpec } from './spec.js'; import type { SessionDispatcher } from '../shared/session-dispatcher.js'; +import type { FleetEmitter } from '../shared/events.js'; import { ok, fail } from '../shared/result/index.js'; import { parseGoalFile, parseGoalContent } from './goals.js'; import { getMilestoneContext } from './milestone.js'; @@ -25,17 +26,27 @@ import { toIssueMarkdown, formatPRContext } from './formatting.js'; import { buildAnalyzerPrompt } from './prompt.js'; import { TRIAGE_GOAL_FILENAME, getBuiltInTriagePrompt } from './triage-prompt.js'; +export interface AnalyzeHandlerDeps { + octokit: Octokit; + dispatcher: SessionDispatcher; + emit?: FleetEmitter; +} + /** * AnalyzeHandler reads goal files, fetches milestone context, * builds a prompt, and dispatches Jules analyzer sessions. * Never throws — all errors returned as Result. */ export class AnalyzeHandler implements AnalyzeSpec { - constructor( - private octokit: Octokit, - private dispatcher: SessionDispatcher, - private log: (msg: string) => void = console.log, - ) {} + private octokit: Octokit; + private dispatcher: SessionDispatcher; + private emit: FleetEmitter; + + constructor(deps: AnalyzeHandlerDeps) { + this.octokit = deps.octokit; + this.dispatcher = deps.dispatcher; + this.emit = deps.emit ?? (() => { }); + } async execute(input: AnalyzeInput): Promise { try { @@ -51,20 +62,30 @@ export class AnalyzeHandler implements AnalyzeSpec { ); } - this.log(`📂 Processing ${goalFiles.length} goal file(s)...`); + this.emit({ + type: 'analyze:start', + owner: input.owner, + repo: input.repo, + goalCount: goalFiles.length, + }); const sessionsStarted: Array<{ goal: string; sessionId: string }> = []; // 2. Process each goal - for (const goalFile of goalFiles) { - this.log(`\n${'─'.repeat(60)}`); - const result = await this.processGoal(goalFile, input); + for (let i = 0; i < goalFiles.length; i++) { + const goalFile = goalFiles[i]; + const result = await this.processGoal(goalFile, input, i + 1, goalFiles.length); if (result) { sessionsStarted.push(result); } } - this.log(`\n✅ All ${goalFiles.length} goal(s) processed.`); + this.emit({ + type: 'analyze:done', + sessionsStarted: sessionsStarted.length, + goalsProcessed: goalFiles.length, + }); + return ok({ sessionsStarted }); } catch (error) { return fail( @@ -100,6 +121,8 @@ export class AnalyzeHandler implements AnalyzeSpec { private async processGoal( goalFile: string, input: AnalyzeInput, + index: number, + total: number, ): Promise<{ goal: string; sessionId: string } | null> { // Handle built-in triage goal const isBuiltIn = goalFile.startsWith('__builtin__:'); @@ -110,19 +133,21 @@ export class AnalyzeHandler implements AnalyzeSpec { const repoFullName = `${input.owner}/${input.repo}`; goalInstructions = getBuiltInTriagePrompt(repoFullName); goal = parseGoalContent(goalInstructions); - this.log(`🔄 Using built-in triage goal (no ${TRIAGE_GOAL_FILENAME} found)`); } else { goal = parseGoalFile(goalFile); goalInstructions = readFileSync(goalFile, 'utf-8'); } + const displayName = isBuiltIn ? `triage.md (built-in)` : basename(goalFile); const milestoneId = input.milestone ?? goal.config?.milestone?.toString(); - if (milestoneId) { - this.log(`📡 Fetching context for Milestone ${milestoneId}...`); - } else { - this.log('📡 Running in General Mode (no milestone)...'); - } + this.emit({ + type: 'analyze:goal:start', + file: displayName, + index, + total, + milestone: milestoneId, + }); const ctx = await getMilestoneContext(this.octokit, { owner: input.owner, @@ -131,9 +156,20 @@ export class AnalyzeHandler implements AnalyzeSpec { }); if (ctx.milestone?.title) { - this.log(`✅ Milestone resolved: "${ctx.milestone.title}"`); + this.emit({ + type: 'analyze:milestone:resolved', + title: ctx.milestone.title, + id: milestoneId!, + }); } + this.emit({ + type: 'analyze:context:fetched', + openIssues: ctx.issues.open.length, + closedIssues: ctx.issues.closed.length, + prs: ctx.pullRequests.length, + }); + const openContext = ctx.issues.open.map(toIssueMarkdown).join('\n') || 'None.'; const closedContext = @@ -141,10 +177,6 @@ export class AnalyzeHandler implements AnalyzeSpec { const prContext = ctx.pullRequests.map(formatPRContext).join('\n') || 'None.'; - this.log( - `📄 Context: ${ctx.issues.open.length} open + ${ctx.issues.closed.length} closed issues, ${ctx.pullRequests.length} PRs`, - ); - const prompt = buildAnalyzerPrompt({ goalInstructions, openContext, @@ -154,7 +186,7 @@ export class AnalyzeHandler implements AnalyzeSpec { milestoneId, }); - this.log(`🔍 Dispatching Analyzer session for ${goalFile}...`); + this.emit({ type: 'analyze:session:dispatching', goal: displayName }); try { const session = await this.dispatcher.dispatch({ @@ -167,12 +199,18 @@ export class AnalyzeHandler implements AnalyzeSpec { autoPr: false, }); - this.log(`✅ Analyzer session started: ${session.id}`); + this.emit({ + type: 'analyze:session:started', + id: session.id, + goal: displayName, + }); return { goal: goalFile, sessionId: session.id }; } catch (error) { - this.log( - `❌ Failed to dispatch session for ${goalFile}: ${error instanceof Error ? error.message : error}`, - ); + this.emit({ + type: 'analyze:session:failed', + goal: displayName, + error: error instanceof Error ? error.message : String(error), + }); return null; } } diff --git a/packages/fleet/src/cli/analyze.command.ts b/packages/fleet/src/cli/analyze.command.ts index d4b1cca..7890dc2 100644 --- a/packages/fleet/src/cli/analyze.command.ts +++ b/packages/fleet/src/cli/analyze.command.ts @@ -17,6 +17,7 @@ import { AnalyzeInputSchema } from '../analyze/spec.js'; import { AnalyzeHandler } from '../analyze/handler.js'; import { createFleetOctokit } from '../shared/auth/octokit.js'; import { getGitRepoInfo } from '../shared/auth/git.js'; +import { createRenderer, createEmitter } from '../shared/ui/index.js'; import type { SessionDispatcher } from '../shared/session-dispatcher.js'; export default defineCommand({ @@ -49,6 +50,8 @@ export default defineCommand({ }, }, async run({ args }) { + const renderer = createRenderer(); + // Auto-detect owner/repo from git remote if not provided let owner = args.owner; let repo = args.repo; @@ -58,6 +61,8 @@ export default defineCommand({ repo = repo || repoInfo.repo; } + renderer.start(`Fleet Analyze — ${owner}/${repo}`); + const input = AnalyzeInputSchema.parse({ goal: args.goal || undefined, goalsDir: args['goals-dir'], @@ -81,19 +86,17 @@ export default defineCommand({ }; const octokit = createFleetOctokit(); - const handler = new AnalyzeHandler(octokit, dispatcher); + const emit = createEmitter(renderer); + const handler = new AnalyzeHandler({ octokit, dispatcher, emit }); const result = await handler.execute(input); if (!result.success) { - console.error(`❌ ${result.error.message}`); - if (result.error.suggestion) { - console.error(` 💡 ${result.error.suggestion}`); - } + renderer.error(result.error.message); process.exit(1); } - console.log( - `\n✅ Started ${result.data.sessionsStarted.length} analyzer session(s).`, + renderer.end( + `${result.data.sessionsStarted.length} session(s) dispatched.`, ); }, }); diff --git a/packages/fleet/src/cli/configure.command.ts b/packages/fleet/src/cli/configure.command.ts index 526b731..51fe322 100644 --- a/packages/fleet/src/cli/configure.command.ts +++ b/packages/fleet/src/cli/configure.command.ts @@ -17,6 +17,7 @@ import { ConfigureInputSchema } from '../configure/spec.js'; import { ConfigureHandler } from '../configure/handler.js'; import { createFleetOctokit } from '../shared/auth/octokit.js'; import { getGitRepoInfo } from '../shared/auth/git.js'; +import { createRenderer, createEmitter } from '../shared/ui/index.js'; export default defineCommand({ meta: { @@ -44,6 +45,8 @@ export default defineCommand({ }, }, async run({ args }) { + const renderer = createRenderer(); + // Auto-detect owner/repo from git remote if not provided let owner = args.owner; let repo = args.repo; @@ -53,25 +56,26 @@ export default defineCommand({ repo = repo || repoInfo.repo; } + const action = args.delete ? 'delete' : 'create'; + renderer.start(`Fleet Configure — ${args.resource} (${action})`); + const input = ConfigureInputSchema.parse({ resource: args.resource, - action: args.delete ? 'delete' : 'create', + action, owner, repo, }); const octokit = createFleetOctokit(); - const handler = new ConfigureHandler(octokit); + const emit = createEmitter(renderer); + const handler = new ConfigureHandler({ octokit, emit }); const result = await handler.execute(input); if (!result.success) { - console.error(`❌ ${result.error.message}`); + renderer.error(result.error.message); process.exit(1); } - const { created, deleted, skipped } = result.data; - if (created.length) console.log(`Created: ${created.join(', ')}`); - if (deleted.length) console.log(`Deleted: ${deleted.join(', ')}`); - if (skipped.length) console.log(`Skipped: ${skipped.join(', ')}`); + renderer.end(`${args.resource} configured.`); }, }); diff --git a/packages/fleet/src/cli/dispatch.command.ts b/packages/fleet/src/cli/dispatch.command.ts index 2c38d81..0caf3ee 100644 --- a/packages/fleet/src/cli/dispatch.command.ts +++ b/packages/fleet/src/cli/dispatch.command.ts @@ -17,6 +17,7 @@ import { DispatchInputSchema } from '../dispatch/spec.js'; import { DispatchHandler } from '../dispatch/handler.js'; import { createFleetOctokit } from '../shared/auth/octokit.js'; import { getGitRepoInfo } from '../shared/auth/git.js'; +import { createRenderer, createEmitter } from '../shared/ui/index.js'; import type { SessionDispatcher } from '../shared/session-dispatcher.js'; export default defineCommand({ @@ -41,6 +42,8 @@ export default defineCommand({ }, }, async run({ args }) { + const renderer = createRenderer(); + // Auto-detect owner/repo from git remote if not provided let owner = args.owner; let repo = args.repo; @@ -50,6 +53,8 @@ export default defineCommand({ repo = repo || repoInfo.repo; } + renderer.start(`Fleet Dispatch — Milestone ${args.milestone}`); + const input = DispatchInputSchema.parse({ milestone: args.milestone, owner, @@ -71,20 +76,16 @@ export default defineCommand({ }; const octokit = createFleetOctokit(); - const handler = new DispatchHandler(octokit, dispatcher); + const emit = createEmitter(renderer); + const handler = new DispatchHandler({ octokit, dispatcher, emit }); const result = await handler.execute(input); if (!result.success) { - console.error(`❌ ${result.error.message}`); - if (result.error.suggestion) { - console.error(` 💡 ${result.error.suggestion}`); - } + renderer.error(result.error.message); process.exit(1); } const { dispatched, skipped } = result.data; - console.log( - `\nDispatched: ${dispatched.length}, Skipped: ${skipped}`, - ); + renderer.end(`${dispatched.length} dispatched, ${skipped} skipped.`); }, }); diff --git a/packages/fleet/src/cli/init.command.ts b/packages/fleet/src/cli/init.command.ts index d5376bd..b8e9b60 100644 --- a/packages/fleet/src/cli/init.command.ts +++ b/packages/fleet/src/cli/init.command.ts @@ -17,7 +17,11 @@ import { InitInputSchema } from '../init/spec.js'; import { InitHandler } from '../init/handler.js'; import { ConfigureHandler } from '../configure/handler.js'; import { createFleetOctokit } from '../shared/auth/octokit.js'; -import { getGitRepoInfo } from '../shared/auth/git.js'; +import { createRenderer, createEmitter, isInteractive } from '../shared/ui/index.js'; +import { runInitWizard, validateHeadlessInputs } from '../init/wizard/index.js'; +import type { InitArgs } from '../init/wizard/types.js'; +import { uploadSecret } from '../init/ops/upload-secrets.js'; +import { WORKFLOW_TEMPLATES } from '../init/templates.js'; export default defineCommand({ meta: { @@ -34,41 +38,103 @@ export default defineCommand({ description: 'Base branch for the PR', default: 'main', }, + 'non-interactive': { + type: 'boolean', + description: 'Disable wizard prompts — all inputs via flags/env vars', + default: false, + }, + 'dry-run': { + type: 'boolean', + description: 'Show what would be created without making changes', + default: false, + }, + auth: { + type: 'string', + description: 'Auth mode: token | app (auto-detected from env vars)', + }, + 'app-id': { + type: 'string', + description: 'GitHub App ID (overrides GITHUB_APP_ID env var)', + }, + 'installation-id': { + type: 'string', + description: 'GitHub App Installation ID (overrides env var)', + }, + 'upload-secrets': { + type: 'boolean', + description: 'Upload secrets to GitHub Actions (default: true in interactive, false in non-interactive)', + }, }, async run({ args }) { - // Auto-detect from git remote if --repo not provided - let repoSlug = args.repo; - if (!repoSlug) { - const repoInfo = await getGitRepoInfo(); - repoSlug = `${repoInfo.owner}/${repoInfo.repo}`; + const nonInteractive = args['non-interactive'] || !isInteractive(); + const renderer = createRenderer(!nonInteractive); + const emit = createEmitter(renderer); + + // ── Collect inputs: wizard or headless ── + const wizardArgs = args as unknown as InitArgs; + const inputs = nonInteractive + ? await validateHeadlessInputs(wizardArgs, emit) + : await runInitWizard(wizardArgs, emit); + + // Check if input collection failed + if ('success' in inputs && !inputs.success) { + renderer.error(inputs.error.message); + if (inputs.error.suggestion) { + renderer.render({ + type: 'error', + code: inputs.error.code, + message: inputs.error.suggestion, + }); + } + process.exit(1); } - const [owner, repoName] = repoSlug.split('/'); + const { owner, repo, baseBranch, secretsToUpload, dryRun } = inputs as Exclude; + + renderer.start(`Fleet Init — ${owner}/${repo}`); + + // ── Dry run: list files and exit ── + if (dryRun) { + const files = WORKFLOW_TEMPLATES.map((t) => t.repoPath); + files.push('.fleet/goals/example.md'); + emit({ type: 'init:dry-run', files }); + renderer.end(`Dry run complete. ${files.length} files would be created.`); + return; + } + + // ── Execute init pipeline ── const input = InitInputSchema.parse({ - repo: repoSlug, + repo: `${owner}/${repo}`, owner, - repoName, - baseBranch: args.base, + repoName: repo, + baseBranch, }); const octokit = createFleetOctokit(); - const labelConfigurator = new ConfigureHandler(octokit); - const handler = new InitHandler(octokit, console.log, labelConfigurator); + const labelConfigurator = new ConfigureHandler({ octokit }); + const handler = new InitHandler({ octokit, emit, labelConfigurator }); const result = await handler.execute(input); if (!result.success) { - console.error(`❌ ${result.error.message}`); + renderer.error(result.error.message); if (result.error.suggestion) { - console.error(` 💡 ${result.error.suggestion}`); + renderer.render({ + type: 'error', + code: result.error.code, + message: result.error.suggestion, + }); } process.exit(1); } - console.log(`\n✅ Fleet initialized!`); - console.log(` PR: ${result.data.prUrl}`); - console.log(` Files: ${result.data.filesCreated.join(', ')}`); - if (result.data.labelsCreated.length > 0) { - console.log(` Labels: ${result.data.labelsCreated.join(', ')}`); + // ── Upload secrets ── + const secretNames = Object.keys(secretsToUpload); + if (secretNames.length > 0) { + for (const name of secretNames) { + await uploadSecret(octokit, owner, repo, name, secretsToUpload[name], emit); + } } + + renderer.end('Fleet initialized! Merge the PR to activate Fleet.'); }, }); diff --git a/packages/fleet/src/cli/merge.command.ts b/packages/fleet/src/cli/merge.command.ts index 289ce04..6c00f37 100644 --- a/packages/fleet/src/cli/merge.command.ts +++ b/packages/fleet/src/cli/merge.command.ts @@ -17,6 +17,7 @@ import { MergeInputSchema } from '../merge/spec.js'; import { MergeHandler } from '../merge/handler.js'; import { createFleetOctokit } from '../shared/auth/octokit.js'; import { getGitRepoInfo } from '../shared/auth/git.js'; +import { createRenderer, createEmitter } from '../shared/ui/index.js'; export default defineCommand({ meta: { @@ -59,6 +60,8 @@ export default defineCommand({ }, }, async run({ args }) { + const renderer = createRenderer(); + // Auto-detect owner/repo from git remote if not provided let owner = args.owner; let repo = args.repo; @@ -68,6 +71,8 @@ export default defineCommand({ repo = repo || repoInfo.repo; } + renderer.start(`Fleet Merge — ${owner}/${repo} (${args.mode} mode)`); + const input = MergeInputSchema.parse({ mode: args.mode, runId: args['run-id'] || undefined, @@ -79,18 +84,15 @@ export default defineCommand({ }); const octokit = createFleetOctokit(); - const handler = new MergeHandler(octokit); + const emit = createEmitter(renderer); + const handler = new MergeHandler({ octokit, emit }); const result = await handler.execute(input); if (!result.success) { - console.error(`❌ ${result.error.message}`); - if (result.error.suggestion) { - console.error(` 💡 ${result.error.suggestion}`); - } + renderer.error(result.error.message); process.exit(1); } - const { merged, skipped, redispatched } = result.data; - console.log(`\nMerged: ${merged.length}, Skipped: ${skipped.length}, Re-dispatched: ${redispatched.length}`); + renderer.end('Sequential merge complete.'); }, }); diff --git a/packages/fleet/src/configure/handler.ts b/packages/fleet/src/configure/handler.ts index 9e7a480..d14af80 100644 --- a/packages/fleet/src/configure/handler.ts +++ b/packages/fleet/src/configure/handler.ts @@ -18,25 +18,44 @@ import type { ConfigureResult, ConfigureSpec, } from './spec.js'; +import type { FleetEmitter } from '../shared/events.js'; import { ok, fail } from '../shared/result/index.js'; import { FLEET_LABELS } from './labels.js'; +export interface ConfigureHandlerDeps { + octokit: Octokit; + emit?: FleetEmitter; +} + /** * ConfigureHandler manages repo resources (labels, etc.). * Never throws — all errors returned as Result. */ export class ConfigureHandler implements ConfigureSpec { - constructor( - private octokit: Octokit, - private log: (msg: string) => void = console.log, - ) { } + private octokit: Octokit; + private emit: FleetEmitter; + + constructor(deps: ConfigureHandlerDeps) { + this.octokit = deps.octokit; + this.emit = deps.emit ?? (() => { }); + } async execute(input: ConfigureInput): Promise { try { + this.emit({ + type: 'configure:start', + resource: input.resource, + owner: input.owner, + repo: input.repo, + }); + if (input.resource === 'labels') { - return input.action === 'create' - ? this.createLabels(input.owner, input.repo) - : this.deleteLabels(input.owner, input.repo); + const result = input.action === 'create' + ? await this.createLabels(input.owner, input.repo) + : await this.deleteLabels(input.owner, input.repo); + + this.emit({ type: 'configure:done' }); + return result; } return fail( @@ -70,7 +89,7 @@ export class ConfigureHandler implements ConfigureSpec { description: label.description, }); created.push(label.name); - this.log(` ✅ Created label: ${label.name}`); + this.emit({ type: 'configure:label:created', name: label.name }); } catch (error: unknown) { const status = error && typeof error === 'object' && 'status' in error @@ -79,7 +98,7 @@ export class ConfigureHandler implements ConfigureSpec { if (status === 422) { // Already exists skipped.push(label.name); - this.log(` ⏭️ Label already exists: ${label.name}`); + this.emit({ type: 'configure:label:exists', name: label.name }); } else { return fail( 'GITHUB_API_ERROR', @@ -108,7 +127,7 @@ export class ConfigureHandler implements ConfigureSpec { name: label.name, }); deleted.push(label.name); - this.log(` 🗑️ Deleted label: ${label.name}`); + this.emit({ type: 'configure:label:created', name: label.name }); } catch (error: unknown) { const status = error && typeof error === 'object' && 'status' in error @@ -116,7 +135,7 @@ export class ConfigureHandler implements ConfigureSpec { : 0; if (status === 404) { skipped.push(label.name); - this.log(` ⏭️ Label not found: ${label.name}`); + this.emit({ type: 'configure:label:exists', name: label.name }); } else { return fail( 'GITHUB_API_ERROR', diff --git a/packages/fleet/src/dispatch/handler.ts b/packages/fleet/src/dispatch/handler.ts index 8dea591..af4b77a 100644 --- a/packages/fleet/src/dispatch/handler.ts +++ b/packages/fleet/src/dispatch/handler.ts @@ -15,28 +15,38 @@ import type { Octokit } from 'octokit'; import type { DispatchInput, DispatchResult, DispatchSpec } from './spec.js'; import type { SessionDispatcher } from '../shared/session-dispatcher.js'; +import type { FleetEmitter } from '../shared/events.js'; import { ok, fail } from '../shared/result/index.js'; import { getMilestoneContext } from '../analyze/milestone.js'; import { getDispatchStatus } from './status.js'; import { recordDispatch } from './events.js'; +export interface DispatchHandlerDeps { + octokit: Octokit; + dispatcher: SessionDispatcher; + emit?: FleetEmitter; +} + /** * DispatchHandler finds undispatched fleet issues in a milestone * and fires Jules worker sessions for each. * Never throws — all errors returned as Result. */ export class DispatchHandler implements DispatchSpec { - constructor( - private octokit: Octokit, - private dispatcher: SessionDispatcher, - private log: (msg: string) => void = console.log, - ) {} + private octokit: Octokit; + private dispatcher: SessionDispatcher; + private emit: FleetEmitter; + + constructor(deps: DispatchHandlerDeps) { + this.octokit = deps.octokit; + this.dispatcher = deps.dispatcher; + this.emit = deps.emit ?? (() => { }); + } async execute(input: DispatchInput): Promise { try { - this.log( - `📡 Fetching issues for milestone ${input.milestone} in ${input.owner}/${input.repo}...`, - ); + this.emit({ type: 'dispatch:start', milestone: input.milestone }); + this.emit({ type: 'dispatch:scanning' }); // 1. Get milestone context const ctx = await getMilestoneContext(this.octokit, { @@ -51,14 +61,10 @@ export class DispatchHandler implements DispatchSpec { ); if (fleetIssues.length === 0) { - this.log('✅ No fleet-labeled open issues. Nothing to dispatch.'); + this.emit({ type: 'dispatch:done', dispatched: 0, skipped: 0 }); return ok({ dispatched: [], skipped: 0 }); } - this.log( - `Found ${fleetIssues.length} fleet-labeled issues. Checking dispatch status...`, - ); - // 3. Get dispatch status for each issue const statuses = await getDispatchStatus( this.octokit, @@ -75,19 +81,21 @@ export class DispatchHandler implements DispatchSpec { const skipped = statuses.length - undispatched.length; if (undispatched.length === 0) { - this.log( - '✅ All fleet issues are already dispatched or have linked PRs.', - ); + this.emit({ type: 'dispatch:done', dispatched: 0, skipped }); return ok({ dispatched: [], skipped }); } - this.log(`🚀 Dispatching ${undispatched.length} issues...`); + this.emit({ type: 'dispatch:found', count: undispatched.length }); const dispatched: Array<{ issueNumber: number; sessionId: string }> = []; for (const status of undispatched) { const issue = fleetIssues.find((i) => i.number === status.number)!; - this.log(`\n Dispatching #${issue.number}: ${issue.title}`); + this.emit({ + type: 'dispatch:issue:dispatching', + number: issue.number, + title: issue.title, + }); const workerPrompt = `Fix Issue #${issue.number}: ${issue.title} @@ -124,17 +132,38 @@ ${issue.body} issueNumber: issue.number, sessionId: session.id, }); - this.log( - ` ✅ Session ${session.id} dispatched, event recorded on #${issue.number}`, - ); + + this.emit({ + type: 'dispatch:issue:dispatched', + number: issue.number, + sessionId: session.id, + }); } catch (error) { - this.log( - ` ❌ Failed to dispatch #${issue.number}: ${error instanceof Error ? error.message : error}`, - ); + this.emit({ + type: 'error', + code: 'DISPATCH_FAILED', + message: `Failed to dispatch #${issue.number}: ${error instanceof Error ? error.message : error}`, + }); } } - this.log(`\n🎉 Dispatched ${dispatched.length} issues.`); + // Emit skipped issues + for (const status of statuses) { + if (!undispatched.includes(status)) { + this.emit({ + type: 'dispatch:issue:skipped', + number: status.number, + reason: status.dispatchEvent ? 'already dispatched' : 'has linked PRs', + }); + } + } + + this.emit({ + type: 'dispatch:done', + dispatched: dispatched.length, + skipped, + }); + return ok({ dispatched, skipped }); } catch (error) { return fail( diff --git a/packages/fleet/src/init/handler.ts b/packages/fleet/src/init/handler.ts index a4c8b0e..24dad09 100644 --- a/packages/fleet/src/init/handler.ts +++ b/packages/fleet/src/init/handler.ts @@ -18,10 +18,17 @@ import { ok, fail } from '../shared/result/index.js'; import { WORKFLOW_TEMPLATES } from './templates.js'; import { EXAMPLE_GOAL } from './templates/example-goal.js'; import type { LabelConfigurator } from './types.js'; +import type { FleetEmitter } from '../shared/events.js'; import { createBranch, isBranchResult } from './ops/create-branch.js'; import { commitFiles, isCommitResult } from './ops/commit-files.js'; import { createInitPR, isPRResult } from './ops/create-pr.js'; +export interface InitHandlerDeps { + octokit: Octokit; + emit?: FleetEmitter; + labelConfigurator?: LabelConfigurator; +} + /** * InitHandler scaffolds fleet workflow files by creating a PR via GitHub REST API. * Never throws — all errors are Result values. @@ -29,20 +36,24 @@ import { createInitPR, isPRResult } from './ops/create-pr.js'; * Pipeline: createBranch → commitFiles → createInitPR → configureLabels */ export class InitHandler implements InitSpec { - constructor( - private octokit: Octokit, - private log: (msg: string) => void = console.log, - private labelConfigurator?: LabelConfigurator, - ) { } + private octokit: Octokit; + private emit: FleetEmitter; + private labelConfigurator?: LabelConfigurator; + + constructor(deps: InitHandlerDeps) { + this.octokit = deps.octokit; + this.emit = deps.emit ?? (() => { }); + this.labelConfigurator = deps.labelConfigurator; + } async execute(input: InitInput): Promise { try { const { owner, repoName: repo, baseBranch } = input; - this.log(`📦 Initializing fleet for ${owner}/${repo}...`); + this.emit({ type: 'init:start', owner, repo }); // 1. Create branch const branchResult = await createBranch( - this.octokit, owner, repo, baseBranch, this.log, + this.octokit, owner, repo, baseBranch, this.emit, ); if (isBranchResult(branchResult)) return branchResult; const { branchName } = branchResult; @@ -52,7 +63,7 @@ export class InitHandler implements InitSpec { owner, repo, branchName, - log: this.log, + emit: this.emit, }; // 2. Commit workflow templates + example goal @@ -60,6 +71,22 @@ export class InitHandler implements InitSpec { if (isCommitResult(filesResult)) return filesResult; const filesCreated = filesResult; + // 2b. Guard: bail out if every file was skipped (nothing to PR) + if (filesCreated.length === 0) { + this.emit({ + type: 'error', + code: 'ALREADY_INITIALIZED', + message: 'All fleet files already exist — nothing to commit.', + suggestion: 'This repo appears to be already initialized. Use jules-fleet configure to update settings.', + }); + return fail( + 'FILE_COMMIT_FAILED', + 'All fleet files already exist — nothing to commit.', + false, + 'This repo appears to be already initialized. Use jules-fleet configure to update settings.', + ); + } + // 3. Create PR const prResult = await createInitPR(ctx, baseBranch, filesCreated); if (isPRResult(prResult)) return prResult; @@ -68,7 +95,6 @@ export class InitHandler implements InitSpec { // 4. Configure labels let labelsCreated: string[] = []; if (this.labelConfigurator) { - this.log(` 🏷️ Configuring labels...`); const labelResult = await this.labelConfigurator.execute({ resource: 'labels', action: 'create', @@ -78,6 +104,13 @@ export class InitHandler implements InitSpec { labelsCreated = labelResult.success ? labelResult.data.created : []; } + this.emit({ + type: 'init:done', + prUrl, + files: filesCreated, + labels: labelsCreated, + }); + return ok({ prUrl, prNumber, filesCreated, labelsCreated }); } catch (error) { return fail( @@ -88,3 +121,4 @@ export class InitHandler implements InitSpec { } } } + diff --git a/packages/fleet/src/init/ops/commit-files.ts b/packages/fleet/src/init/ops/commit-files.ts index 1e3e689..1b6cfbc 100644 --- a/packages/fleet/src/init/ops/commit-files.ts +++ b/packages/fleet/src/init/ops/commit-files.ts @@ -40,14 +40,14 @@ export async function commitFiles( branch: ctx.branchName, }); filesCreated.push(tmpl.repoPath); - ctx.log(` 📄 Added: ${tmpl.repoPath}`); + ctx.emit({ type: 'init:file:committed', path: tmpl.repoPath }); } catch (error: unknown) { const status = error && typeof error === 'object' && 'status' in error ? (error as { status: number }).status : 0; if (status === 422) { - ctx.log(` ⏭️ Already exists: ${tmpl.repoPath}`); + ctx.emit({ type: 'init:file:skipped', path: tmpl.repoPath, reason: 'already exists' }); } else { return fail( 'FILE_COMMIT_FAILED', @@ -69,16 +69,18 @@ export async function commitFiles( branch: ctx.branchName, }); filesCreated.push('.fleet/goals/example.md'); - ctx.log(` 📄 Added: .fleet/goals/example.md`); + ctx.emit({ type: 'init:file:committed', path: '.fleet/goals/example.md' }); } catch (error: unknown) { const status = error && typeof error === 'object' && 'status' in error ? (error as { status: number }).status : 0; if (status !== 422) { - ctx.log( - ` ⚠️ Failed to create example goal: ${error instanceof Error ? error.message : error}`, - ); + ctx.emit({ + type: 'init:file:skipped', + path: '.fleet/goals/example.md', + reason: `${error instanceof Error ? error.message : error}`, + }); } } diff --git a/packages/fleet/src/init/ops/create-branch.ts b/packages/fleet/src/init/ops/create-branch.ts index 702fefc..d796da1 100644 --- a/packages/fleet/src/init/ops/create-branch.ts +++ b/packages/fleet/src/init/ops/create-branch.ts @@ -15,6 +15,7 @@ import type { Octokit } from 'octokit'; import { fail } from '../../shared/result/index.js'; import type { InitResult } from '../spec.js'; +import type { FleetEmitter } from '../../shared/events.js'; /** * Create the fleet-init branch from the base branch SHA. @@ -25,7 +26,7 @@ export async function createBranch( owner: string, repo: string, baseBranch: string, - log: (msg: string) => void, + emit: FleetEmitter, ): Promise<{ branchName: string; baseSha: string } | InitResult> { const { data: refData } = await octokit.rest.git.getRef({ owner, @@ -35,7 +36,7 @@ export async function createBranch( const baseSha = refData.object.sha; const branchName = `fleet-init-${Date.now()}`; - log(` 🌿 Creating branch: ${branchName}`); + emit({ type: 'init:branch:creating', name: branchName, base: baseBranch }); try { await octokit.rest.git.createRef({ @@ -44,6 +45,7 @@ export async function createBranch( ref: `refs/heads/${branchName}`, sha: baseSha, }); + emit({ type: 'init:branch:created', name: branchName }); } catch (error) { return fail( 'BRANCH_CREATE_FAILED', diff --git a/packages/fleet/src/init/ops/create-pr.ts b/packages/fleet/src/init/ops/create-pr.ts index 0f4de62..109dd04 100644 --- a/packages/fleet/src/init/ops/create-pr.ts +++ b/packages/fleet/src/init/ops/create-pr.ts @@ -26,7 +26,7 @@ export async function createInitPR( baseBranch: string, filesCreated: string[], ): Promise<{ prUrl: string; prNumber: number } | InitResult> { - ctx.log(` 🔗 Creating pull request...`); + ctx.emit({ type: 'init:pr:creating' }); try { const { data: pr } = await ctx.octokit.rest.pulls.create({ @@ -37,7 +37,7 @@ export async function createInitPR( head: ctx.branchName, base: baseBranch, }); - ctx.log(` ✅ PR created: ${pr.html_url}`); + ctx.emit({ type: 'init:pr:created', url: pr.html_url, number: pr.number }); return { prUrl: pr.html_url, prNumber: pr.number }; } catch (error) { return fail( diff --git a/packages/fleet/src/init/ops/upload-secrets.ts b/packages/fleet/src/init/ops/upload-secrets.ts new file mode 100644 index 0000000..99cd90b --- /dev/null +++ b/packages/fleet/src/init/ops/upload-secrets.ts @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Octokit } from 'octokit'; +import type { FleetEmitter } from '../../shared/events.js'; + +/** + * Upload a secret to GitHub Actions using NaCl sealed-box encryption. + * + * Uses the repo public key endpoint + libsodium-wrappers. + */ +export async function uploadSecret( + octokit: Octokit, + owner: string, + repo: string, + secretName: string, + secretValue: string, + emit: FleetEmitter, +): Promise<{ success: boolean; error?: string }> { + emit({ type: 'init:secret:uploading', name: secretName }); + + try { + // 1. Get the repo public key for encryption + const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({ + owner, + repo, + }); + + // 2. Encrypt the secret using libsodium + const sodium = await import('libsodium-wrappers'); + await sodium.default.ready; + + const binKey = sodium.default.from_base64( + publicKey.key, + sodium.default.base64_variants.ORIGINAL, + ); + const binSecret = sodium.default.from_string(secretValue); + const encrypted = sodium.default.crypto_box_seal(binSecret, binKey); + const encryptedBase64 = sodium.default.to_base64( + encrypted, + sodium.default.base64_variants.ORIGINAL, + ); + + // 3. Upload the encrypted secret + await octokit.rest.actions.createOrUpdateRepoSecret({ + owner, + repo, + secret_name: secretName, + encrypted_value: encryptedBase64, + key_id: publicKey.key_id, + }); + + emit({ type: 'init:secret:uploaded', name: secretName }); + return { success: true }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + emit({ + type: 'init:secret:skipped', + name: secretName, + reason: message, + }); + return { success: false, error: message }; + } +} diff --git a/packages/fleet/src/init/types.ts b/packages/fleet/src/init/types.ts index 5fe00c2..d6ba0e5 100644 --- a/packages/fleet/src/init/types.ts +++ b/packages/fleet/src/init/types.ts @@ -13,6 +13,7 @@ // limitations under the License. import type { ConfigureResult } from '../configure/spec.js'; +import type { FleetEmitter } from '../shared/events.js'; /** Interface for label configuration — decouples init from configure slice */ export interface LabelConfigurator { @@ -30,5 +31,5 @@ export interface InitContext { owner: string; repo: string; branchName: string; - log: (msg: string) => void; + emit: FleetEmitter; } diff --git a/packages/fleet/src/init/wizard/headless.ts b/packages/fleet/src/init/wizard/headless.ts new file mode 100644 index 0000000..0cd5c4d --- /dev/null +++ b/packages/fleet/src/init/wizard/headless.ts @@ -0,0 +1,112 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { fail } from '../../shared/result/index.js'; +import { getGitRepoInfo } from '../../shared/auth/git.js'; +import type { FleetEmitter } from '../../shared/events.js'; +import { WORKFLOW_TEMPLATES } from '../templates.js'; +import type { InitArgs, InitWizardResult } from './types.js'; + +/** + * Validate all required inputs from flags + env vars in non-interactive mode. + * Fails with actionable errors when required values are missing. + */ +export async function validateHeadlessInputs( + args: InitArgs, + emit: FleetEmitter, +): Promise> { + // ── Repository ── + let repoSlug = args.repo ?? process.env.GITHUB_REPOSITORY; + if (!repoSlug) { + try { + const info = await getGitRepoInfo(); + repoSlug = info.fullName; + } catch { + return fail( + 'UNKNOWN_ERROR', + 'Missing repository. Set --repo, GITHUB_REPOSITORY env var, or run from a git repo.', + true, + ); + } + } + const [owner, repo] = repoSlug.split('/'); + if (!owner || !repo) { + return fail('UNKNOWN_ERROR', `Invalid repo format: "${repoSlug}". Expected owner/repo.`, false); + } + + // ── Authentication ── + const hasToken = !!process.env.GITHUB_TOKEN; + const hasApp = !!( + (args['app-id'] || process.env.GITHUB_APP_ID) && + (process.env.GITHUB_APP_PRIVATE_KEY_BASE64 || process.env.GITHUB_APP_PRIVATE_KEY) && + (args['installation-id'] || process.env.GITHUB_APP_INSTALLATION_ID) + ); + + let authMethod: 'token' | 'app'; + + if (args.auth === 'app' || (!args.auth && hasApp)) { + authMethod = 'app'; + if (args['app-id']) process.env.GITHUB_APP_ID = args['app-id']; + if (args['installation-id']) process.env.GITHUB_APP_INSTALLATION_ID = args['installation-id']; + if (!hasApp) { + const missing: string[] = []; + if (!process.env.GITHUB_APP_ID && !args['app-id']) missing.push('GITHUB_APP_ID (env) or --app-id'); + if (!process.env.GITHUB_APP_PRIVATE_KEY_BASE64 && !process.env.GITHUB_APP_PRIVATE_KEY) { + missing.push('GITHUB_APP_PRIVATE_KEY_BASE64 (env)'); + } + if (!process.env.GITHUB_APP_INSTALLATION_ID && !args['installation-id']) { + missing.push('GITHUB_APP_INSTALLATION_ID (env) or --installation-id'); + } + return fail( + 'UNKNOWN_ERROR', + `Missing GitHub App credentials: ${missing.join(', ')}.\nOr run without --non-interactive for guided setup.`, + true, + ); + } + } else if (args.auth === 'token' || (!args.auth && hasToken)) { + authMethod = 'token'; + } else { + return fail( + 'UNKNOWN_ERROR', + 'Missing GitHub authentication.\nSet GITHUB_TOKEN or GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY_BASE64 + GITHUB_APP_INSTALLATION_ID.\nOr run without --non-interactive for guided setup.', + true, + ); + } + + emit({ type: 'init:auth:detected', method: authMethod }); + + // ── Jules API Key ── + if (!process.env.JULES_API_KEY) { + emit({ + type: 'init:secret:skipped', + name: 'JULES_API_KEY', + reason: 'Not set — Fleet workflows will not be able to dispatch sessions.', + }); + } + + const baseBranch = args.base ?? 'main'; + const dryRun = args['dry-run'] ?? false; + + // In non-interactive mode, never upload secrets by default + const secretsToUpload: Record = {}; + + // ── Dry run ── + if (dryRun) { + const files = WORKFLOW_TEMPLATES.map((t) => t.repoPath); + files.push('.fleet/goals/example.md'); + emit({ type: 'init:dry-run', files }); + } + + return { owner, repo, baseBranch, authMethod, secretsToUpload, dryRun }; +} diff --git a/packages/fleet/src/init/wizard/index.ts b/packages/fleet/src/init/wizard/index.ts new file mode 100644 index 0000000..370ce14 --- /dev/null +++ b/packages/fleet/src/init/wizard/index.ts @@ -0,0 +1,17 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export type { InitWizardResult, InitArgs } from './types.js'; +export { runInitWizard } from './interactive.js'; +export { validateHeadlessInputs } from './headless.js'; diff --git a/packages/fleet/src/init/wizard/interactive.ts b/packages/fleet/src/init/wizard/interactive.ts new file mode 100644 index 0000000..1f5c724 --- /dev/null +++ b/packages/fleet/src/init/wizard/interactive.ts @@ -0,0 +1,207 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as p from '@clack/prompts'; +import { fail } from '../../shared/result/index.js'; +import { getGitRepoInfo } from '../../shared/auth/git.js'; +import type { FleetEmitter } from '../../shared/events.js'; +import { WORKFLOW_TEMPLATES } from '../templates.js'; +import type { InitArgs, InitWizardResult } from './types.js'; + +/** + * Collect all init inputs via interactive wizard prompts. + * Each step checks if the value is already available from flags/env + * and skips the prompt if so. + */ +export async function runInitWizard( + args: InitArgs, + emit: FleetEmitter, +): Promise> { + // ── Step 1: Repository ── + let repoSlug: string | undefined = args.repo ?? process.env.GITHUB_REPOSITORY; + if (!repoSlug) { + try { + const info = await getGitRepoInfo(); + repoSlug = info.fullName; + } catch { + // Could not auto-detect + } + } + + if (repoSlug) { + const confirmed = await p.confirm({ + message: `Detected repository: ${repoSlug}. Is this correct?`, + initialValue: true, + }); + if (p.isCancel(confirmed)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + if (!confirmed) { + const manual = await p.text({ + message: 'Enter repository in owner/repo format:', + validate: (v) => !v || !/^[^/]+\/[^/]+$/.test(v) ? 'Must be owner/repo format' : undefined, + }); + if (p.isCancel(manual)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + repoSlug = manual; + } + } else { + const manual = await p.text({ + message: 'Enter repository in owner/repo format:', + validate: (v) => !v || !/^[^/]+\/[^/]+$/.test(v) ? 'Must be owner/repo format' : undefined, + }); + if (p.isCancel(manual)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + repoSlug = manual; + } + + const [owner, repo] = repoSlug.split('/'); + + // ── Step 2: Base branch ── + const baseBranch = args.base ?? 'main'; + + // ── Step 3: Authentication ── + const hasToken = !!process.env.GITHUB_TOKEN; + const hasApp = !!(process.env.GITHUB_APP_ID && (process.env.GITHUB_APP_PRIVATE_KEY_BASE64 || process.env.GITHUB_APP_PRIVATE_KEY) && process.env.GITHUB_APP_INSTALLATION_ID); + + let authMethod: 'token' | 'app'; + + if (args.auth === 'token' || args.auth === 'app') { + authMethod = args.auth; + } else if (hasApp) { + authMethod = 'app'; + p.log.success('GitHub App credentials detected'); + } else if (hasToken) { + authMethod = 'token'; + p.log.success('GITHUB_TOKEN detected'); + } else { + const authChoice = await p.select({ + message: 'How will Fleet authenticate with GitHub?', + options: [ + { value: 'token' as const, label: 'Personal Access Token (GITHUB_TOKEN)' }, + { value: 'app' as const, label: 'GitHub App (recommended for orgs)' }, + ], + }); + if (p.isCancel(authChoice)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + authMethod = authChoice; + + // Prompt for credentials + if (authMethod === 'token') { + if (!hasToken) { + const token = await p.password({ + message: 'Paste your GitHub token:', + }); + if (p.isCancel(token)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + process.env.GITHUB_TOKEN = token; + } + } else { + if (!process.env.GITHUB_APP_ID) { + const appId = await p.text({ message: 'Enter your GitHub App ID:' }); + if (p.isCancel(appId)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + process.env.GITHUB_APP_ID = appId; + } + if (!process.env.GITHUB_APP_INSTALLATION_ID) { + const installId = await p.text({ message: 'Enter your Installation ID:' }); + if (p.isCancel(installId)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + process.env.GITHUB_APP_INSTALLATION_ID = installId; + } + if (!process.env.GITHUB_APP_PRIVATE_KEY_BASE64 && !process.env.GITHUB_APP_PRIVATE_KEY) { + const key = await p.password({ message: 'Paste your private key (base64 encoded):' }); + if (p.isCancel(key)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = key; + } + } + } + + emit({ type: 'init:auth:detected', method: authMethod }); + + // ── Step 4: Jules API Key ── + const secretsToUpload: Record = {}; + const julesKey = process.env.JULES_API_KEY; + + if (!julesKey) { + const wantKey = await p.confirm({ + message: 'Fleet needs a JULES_API_KEY to dispatch sessions. Do you have one?', + initialValue: true, + }); + if (!p.isCancel(wantKey) && wantKey) { + const key = await p.password({ message: 'Enter your Jules API key:' }); + if (!p.isCancel(key)) { + process.env.JULES_API_KEY = key; + secretsToUpload['JULES_API_KEY'] = key; + } + } + } else { + p.log.success('JULES_API_KEY detected'); + secretsToUpload['JULES_API_KEY'] = julesKey; + } + + // ── Step 5: Upload secrets? ── + const shouldUpload = args['upload-secrets'] ?? true; + if (shouldUpload && Object.keys(secretsToUpload).length > 0) { + const confirmed = await p.confirm({ + message: `Upload ${Object.keys(secretsToUpload).length} secret(s) to GitHub Actions secrets?`, + initialValue: true, + }); + if (p.isCancel(confirmed) || !confirmed) { + Object.keys(secretsToUpload).forEach((k) => delete secretsToUpload[k]); + } + } + + // Also offer to upload app credentials if using app auth + if (shouldUpload && authMethod === 'app') { + const uploadApp = await p.confirm({ + message: 'Upload GitHub App credentials to repo secrets?', + initialValue: true, + }); + if (!p.isCancel(uploadApp) && uploadApp) { + if (process.env.GITHUB_APP_ID) secretsToUpload['GITHUB_APP_ID'] = process.env.GITHUB_APP_ID; + if (process.env.GITHUB_APP_PRIVATE_KEY_BASE64) { + secretsToUpload['GITHUB_APP_PRIVATE_KEY_BASE64'] = process.env.GITHUB_APP_PRIVATE_KEY_BASE64; + } + if (process.env.GITHUB_APP_INSTALLATION_ID) { + secretsToUpload['GITHUB_APP_INSTALLATION_ID'] = process.env.GITHUB_APP_INSTALLATION_ID; + } + } + } + + // ── Step 6: Dry run? ── + const dryRun = args['dry-run'] ?? false; + + // ── Step 7: Confirmation ── + if (!dryRun) { + const files = WORKFLOW_TEMPLATES.map((t) => t.repoPath); + files.push('.fleet/goals/example.md'); + + p.log.info([ + 'Fleet will:', + ` • Create a branch from ${baseBranch}`, + ` • Commit ${files.length} files`, + ' • Open a pull request', + ' • Configure labels (fleet, fleet-merge-ready)', + ].join('\n')); + + const proceed = await p.confirm({ + message: 'Create the PR now?', + initialValue: true, + }); + if (p.isCancel(proceed)) return fail('UNKNOWN_ERROR', 'Setup cancelled.', false); + if (!proceed) { + emit({ type: 'init:dry-run', files }); + return fail( + 'UNKNOWN_ERROR', + `Dry run: would create ${files.length} files. Run again to proceed.`, + false, + ); + } + } + + return { owner, repo, baseBranch, authMethod, secretsToUpload, dryRun }; +} diff --git a/packages/fleet/src/init/wizard/types.ts b/packages/fleet/src/init/wizard/types.ts new file mode 100644 index 0000000..28fac23 --- /dev/null +++ b/packages/fleet/src/init/wizard/types.ts @@ -0,0 +1,37 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Resolved inputs from the wizard or flags+env validation */ +export interface InitWizardResult { + owner: string; + repo: string; + baseBranch: string; + authMethod: 'token' | 'app'; + /** Secrets to upload (name → value). Empty if user declines or non-interactive. */ + secretsToUpload: Record; + /** Whether to perform a dry run (list files but don't create PR) */ + dryRun: boolean; +} + +/** Parsed args from citty */ +export interface InitArgs { + repo?: string; + base?: string; + 'non-interactive'?: boolean; + 'dry-run'?: boolean; + auth?: string; + 'app-id'?: string; + 'installation-id'?: string; + 'upload-secrets'?: boolean; +} diff --git a/packages/fleet/src/merge/handler.ts b/packages/fleet/src/merge/handler.ts index 2961019..957e90d 100644 --- a/packages/fleet/src/merge/handler.ts +++ b/packages/fleet/src/merge/handler.ts @@ -19,6 +19,7 @@ import type { MergeSpec, } from './spec.js'; import type { PR } from '../shared/schemas/pr.js'; +import type { FleetEmitter } from '../shared/events.js'; import { ok, fail } from '../shared/result/index.js'; import { selectByLabel } from './select/by-label.js'; import { selectByFleetRun } from './select/by-fleet-run.js'; @@ -27,6 +28,12 @@ import { waitForCI } from './ops/wait-for-ci.js'; import { squashMerge } from './ops/squash-merge.js'; import { redispatch } from './ops/redispatch.js'; +export interface MergeHandlerDeps { + octokit: Octokit; + emit?: FleetEmitter; + sleep?: (ms: number) => Promise; +} + /** Default delay helper */ const defaultSleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)); @@ -37,14 +44,14 @@ const defaultSleep = (ms: number): Promise => * Never throws — all errors returned as Result. */ export class MergeHandler implements MergeSpec { + private octokit: Octokit; + private emit: FleetEmitter; private sleep: (ms: number) => Promise; - constructor( - private octokit: Octokit, - private log: (msg: string) => void = console.log, - sleep?: (ms: number) => Promise, - ) { - this.sleep = sleep ?? defaultSleep; + constructor(deps: MergeHandlerDeps) { + this.octokit = deps.octokit; + this.emit = deps.emit ?? (() => { }); + this.sleep = deps.sleep ?? defaultSleep; } async execute(input: MergeInput): Promise { @@ -53,11 +60,17 @@ export class MergeHandler implements MergeSpec { const prs = await this.selectPRs(input); if (prs.length === 0) { - this.log('ℹ️ No PRs found matching selection criteria.'); + this.emit({ type: 'merge:no-prs' }); return ok({ merged: [], skipped: [], redispatched: [] }); } - this.log(`Found ${prs.length} PR(s) to merge.`); + this.emit({ + type: 'merge:start', + owner: input.owner, + repo: input.repo, + mode: input.mode, + prCount: prs.length, + }); const merged: number[] = []; const skipped: number[] = []; @@ -70,11 +83,12 @@ export class MergeHandler implements MergeSpec { let prMerged = false; while (!prMerged) { - const retryLabel = - retryCount > 0 ? ` (retry ${retryCount})` : ''; - this.log( - `\n📦 Processing PR #${currentPr.number}${retryLabel}`, - ); + this.emit({ + type: 'merge:pr:processing', + number: currentPr.number, + title: currentPr.body?.split('\n')[0] ?? `PR #${currentPr.number}`, + retry: retryCount > 0 ? retryCount : undefined, + }); // Update branch from base (skip for first PR on first attempt) if (prs.indexOf(pr) > 0 || retryCount > 0) { @@ -83,7 +97,7 @@ export class MergeHandler implements MergeSpec { input.owner, input.repo, currentPr.number, - this.log, + this.emit, ); if (!updateResult.ok && updateResult.conflict) { @@ -106,10 +120,6 @@ export class MergeHandler implements MergeSpec { ); } - this.log( - ` ⚠️ Merge conflict detected. Re-dispatching...`, - ); - const newPr = await redispatch( this.octokit, input.owner, @@ -117,7 +127,7 @@ export class MergeHandler implements MergeSpec { currentPr, input.baseBranch, input.pollTimeoutSeconds, - this.log, + this.emit, this.sleep, ); @@ -151,39 +161,38 @@ export class MergeHandler implements MergeSpec { } // Wait for CI - this.log(` 🧪 Waiting for CI on PR #${currentPr.number}...`); const ciResult = await waitForCI( this.octokit, input.owner, input.repo, currentPr.number, input.maxCIWaitSeconds * 1000, - this.log, + this.emit, this.sleep, ); if (ciResult === 'none') { - this.log( - ` ℹ️ No CI checks configured. Proceeding.`, - ); + // No CI checks — proceed } else if (ciResult === 'fail') { - this.log( - ` ❌ CI failed for PR #${currentPr.number}. Skipping.`, - ); + this.emit({ + type: 'merge:pr:skipped', + prNumber: currentPr.number, + reason: 'CI failure', + }); skipped.push(currentPr.number); break; } else if (ciResult === 'timeout') { - this.log( - ` ⏰ CI timeout for PR #${currentPr.number}. Skipping.`, - ); + this.emit({ + type: 'merge:pr:skipped', + prNumber: currentPr.number, + reason: 'CI timeout', + }); skipped.push(currentPr.number); break; } // Merge - this.log( - ` ✅ CI passed. Merging PR #${currentPr.number}...`, - ); + this.emit({ type: 'merge:pr:merging', prNumber: currentPr.number }); const mergeResult = await squashMerge( this.octokit, input.owner, @@ -199,7 +208,7 @@ export class MergeHandler implements MergeSpec { ); } - this.log(` 🎉 PR #${currentPr.number} merged successfully.`); + this.emit({ type: 'merge:pr:merged', prNumber: currentPr.number }); merged.push(currentPr.number); prMerged = true; } @@ -208,7 +217,7 @@ export class MergeHandler implements MergeSpec { await this.sleep(5_000); } - this.log(`\n✅ Sequential merge complete.`); + this.emit({ type: 'merge:done', merged, skipped, redispatched }); return ok({ merged, skipped, redispatched }); } catch (error) { return fail( diff --git a/packages/fleet/src/merge/ops/redispatch.ts b/packages/fleet/src/merge/ops/redispatch.ts index 54fca4f..5521b3b 100644 --- a/packages/fleet/src/merge/ops/redispatch.ts +++ b/packages/fleet/src/merge/ops/redispatch.ts @@ -14,6 +14,7 @@ import type { Octokit } from 'octokit'; import type { PR } from '../../shared/schemas/pr.js'; +import type { FleetEmitter } from '../../shared/events.js'; /** * Closes a conflicting PR and re-dispatches via Jules SDK. @@ -26,11 +27,12 @@ export async function redispatch( oldPr: PR, baseBranch: string, pollTimeoutSeconds: number, - log: (msg: string) => void, + emit: FleetEmitter, sleep: (ms: number) => Promise, ): Promise { + emit({ type: 'merge:redispatch:start', oldPr: oldPr.number }); + // Close the conflicting PR - log(` 🔒 Closing conflicting PR #${oldPr.number}...`); try { await octokit.rest.pulls.update({ owner, @@ -40,13 +42,10 @@ export async function redispatch( body: `${oldPr.body ?? ''}\n\n---\n⚠️ Closed by fleet-merge: merge conflict detected. Task re-dispatched.`, }); } catch { - log( - ` ⚠️ Failed to close PR #${oldPr.number}, continuing...`, - ); + // Non-fatal — continue with re-dispatch } // Re-dispatch via Jules SDK - log(` 🚀 Re-dispatching against current ${baseBranch}...`); try { const { jules } = await import('@google/jules-sdk'); const session = await jules.session({ @@ -56,10 +55,9 @@ export async function redispatch( baseBranch, }, }); - log(` 📝 New session: ${session.id}`); // Poll for new PR - log(` ⏳ Waiting for new PR from session ${session.id}...`); + emit({ type: 'merge:redispatch:waiting', oldPr: oldPr.number }); const start = Date.now(); const timeoutMs = pollTimeoutSeconds * 1000; const pollIntervalMs = 30_000; @@ -81,23 +79,26 @@ export async function redispatch( ); if (newPr) { - log( - ` ✅ New PR #${newPr.number} found (${newPr.head.ref})`, - ); - return { + const result: PR = { number: newPr.number, headRef: newPr.head.ref, headSha: newPr.head.sha, body: newPr.body, }; + emit({ + type: 'merge:redispatch:done', + oldPr: oldPr.number, + newPr: newPr.number, + }); + return result; } - - log(` ⏳ No PR yet... polling again in 30s`); } } catch (error) { - log( - ` ❌ Re-dispatch failed: ${error instanceof Error ? error.message : error}`, - ); + emit({ + type: 'error', + code: 'REDISPATCH_FAILED', + message: `Re-dispatch failed: ${error instanceof Error ? error.message : error}`, + }); } return null; diff --git a/packages/fleet/src/merge/ops/update-branch.ts b/packages/fleet/src/merge/ops/update-branch.ts index bd59196..35a5b2d 100644 --- a/packages/fleet/src/merge/ops/update-branch.ts +++ b/packages/fleet/src/merge/ops/update-branch.ts @@ -13,6 +13,7 @@ // limitations under the License. import type { Octokit } from 'octokit'; +import type { FleetEmitter } from '../../shared/events.js'; /** * Updates a PR branch from its base branch. @@ -23,15 +24,16 @@ export async function updateBranch( owner: string, repo: string, prNumber: number, - log: (msg: string) => void, + emit: FleetEmitter, ): Promise<{ ok: boolean; conflict: boolean; error?: string }> { try { - log(` 🔄 Updating branch from base...`); + emit({ type: 'merge:branch:updating', prNumber }); await octokit.rest.pulls.updateBranch({ owner, repo, pull_number: prNumber, }); + emit({ type: 'merge:branch:updated', prNumber }); return { ok: true, conflict: false }; } catch (error: unknown) { const status = @@ -39,6 +41,7 @@ export async function updateBranch( ? (error as { status: number }).status : 0; if (status === 422) { + emit({ type: 'merge:conflict:detected', prNumber }); return { ok: false, conflict: true }; } const message = diff --git a/packages/fleet/src/merge/ops/wait-for-ci.ts b/packages/fleet/src/merge/ops/wait-for-ci.ts index daba1c1..05f526a 100644 --- a/packages/fleet/src/merge/ops/wait-for-ci.ts +++ b/packages/fleet/src/merge/ops/wait-for-ci.ts @@ -13,6 +13,7 @@ // limitations under the License. import type { Octokit } from 'octokit'; +import type { FleetEmitter } from '../../shared/events.js'; /** * Polls GitHub Checks API until all checks complete or timeout. @@ -24,7 +25,7 @@ export async function waitForCI( repo: string, prNumber: number, maxWaitMs: number, - log: (msg: string) => void, + emit: FleetEmitter, sleep: (ms: number) => Promise, ): Promise<'pass' | 'fail' | 'none' | 'timeout'> { const { data: prData } = await octokit.rest.pulls.get({ @@ -34,6 +35,8 @@ export async function waitForCI( }); const headSha = prData.head.sha; + emit({ type: 'merge:ci:waiting', prNumber }); + const start = Date.now(); while (Date.now() - start < maxWaitMs) { const { data } = await octokit.rest.checks.listForRef({ @@ -43,9 +46,26 @@ export async function waitForCI( }); if (data.check_runs.length === 0) { + emit({ type: 'merge:ci:none', prNumber }); return 'none'; } + // Emit status for each check run + for (const run of data.check_runs) { + if (run.status === 'completed') { + const passed = run.conclusion === 'success' || run.conclusion === 'skipped'; + const durationMs = run.started_at && run.completed_at + ? new Date(run.completed_at).getTime() - new Date(run.started_at).getTime() + : undefined; + emit({ + type: 'merge:ci:check', + name: run.name, + status: passed ? 'pass' : 'fail', + duration: durationMs ? Math.round(durationMs / 1000) : undefined, + }); + } + } + const allComplete = data.check_runs.every( (run) => run.status === 'completed', ); @@ -54,12 +74,18 @@ export async function waitForCI( run.conclusion === 'success' || run.conclusion === 'skipped', ); - if (allComplete && allPassed) return 'pass'; - if (allComplete && !allPassed) return 'fail'; + if (allComplete && allPassed) { + emit({ type: 'merge:ci:passed', prNumber }); + return 'pass'; + } + if (allComplete && !allPassed) { + emit({ type: 'merge:ci:failed', prNumber }); + return 'fail'; + } - log(` ⏳ CI pending... waiting 30s`); await sleep(30_000); } + emit({ type: 'merge:ci:timeout', prNumber }); return 'timeout'; } diff --git a/packages/fleet/src/shared/events.ts b/packages/fleet/src/shared/events.ts new file mode 100644 index 0000000..deaa209 --- /dev/null +++ b/packages/fleet/src/shared/events.ts @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ── Re-export shim ────────────────────────────────────────────────── +// This file exists for backward-compatible imports. +// The canonical event types now live in shared/events/.ts. +// New code should import from './events/index.js' directly. + +export type { + InitEvent, + AnalyzeEvent, + MergeEvent, + DispatchEvent, + ConfigureEvent, + ErrorEvent, + FleetEvent, + FleetEmitter, +} from './events/index.js'; diff --git a/packages/fleet/src/shared/events/analyze.ts b/packages/fleet/src/shared/events/analyze.ts new file mode 100644 index 0000000..91ff544 --- /dev/null +++ b/packages/fleet/src/shared/events/analyze.ts @@ -0,0 +1,24 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Analyze domain events */ +export type AnalyzeEvent = + | { type: 'analyze:start'; owner: string; repo: string; goalCount: number } + | { type: 'analyze:goal:start'; file: string; index: number; total: number; milestone?: string } + | { type: 'analyze:milestone:resolved'; title: string; id: string } + | { type: 'analyze:context:fetched'; openIssues: number; closedIssues: number; prs: number } + | { type: 'analyze:session:dispatching'; goal: string } + | { type: 'analyze:session:started'; id: string; goal: string } + | { type: 'analyze:session:failed'; goal: string; error: string } + | { type: 'analyze:done'; sessionsStarted: number; goalsProcessed: number }; diff --git a/packages/fleet/src/shared/events/configure.ts b/packages/fleet/src/shared/events/configure.ts new file mode 100644 index 0000000..313d2de --- /dev/null +++ b/packages/fleet/src/shared/events/configure.ts @@ -0,0 +1,22 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Configure domain events */ +export type ConfigureEvent = + | { type: 'configure:start'; resource: string; owner: string; repo: string } + | { type: 'configure:label:created'; name: string } + | { type: 'configure:label:exists'; name: string } + | { type: 'configure:secret:uploading'; name: string } + | { type: 'configure:secret:uploaded'; name: string } + | { type: 'configure:done' }; diff --git a/packages/fleet/src/shared/events/dispatch.ts b/packages/fleet/src/shared/events/dispatch.ts new file mode 100644 index 0000000..2e25f62 --- /dev/null +++ b/packages/fleet/src/shared/events/dispatch.ts @@ -0,0 +1,23 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Dispatch domain events */ +export type DispatchEvent = + | { type: 'dispatch:start'; milestone: string } + | { type: 'dispatch:scanning' } + | { type: 'dispatch:found'; count: number } + | { type: 'dispatch:issue:dispatching'; number: number; title: string } + | { type: 'dispatch:issue:dispatched'; number: number; sessionId: string } + | { type: 'dispatch:issue:skipped'; number: number; reason: string } + | { type: 'dispatch:done'; dispatched: number; skipped: number }; diff --git a/packages/fleet/src/shared/events/error.ts b/packages/fleet/src/shared/events/error.ts new file mode 100644 index 0000000..9a06060 --- /dev/null +++ b/packages/fleet/src/shared/events/error.ts @@ -0,0 +1,21 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Shared error event — not domain-specific */ +export type ErrorEvent = { + type: 'error'; + code: string; + message: string; + suggestion?: string; +}; diff --git a/packages/fleet/src/shared/events/index.ts b/packages/fleet/src/shared/events/index.ts new file mode 100644 index 0000000..dbb5987 --- /dev/null +++ b/packages/fleet/src/shared/events/index.ts @@ -0,0 +1,43 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ── Fleet Event Re-exports ────────────────────────────────────────── +// This barrel aggregates per-domain event types into a single union. +// Adding a new domain? Create shared/events/.ts and add it here. + +export type { InitEvent } from './init.js'; +export type { AnalyzeEvent } from './analyze.js'; +export type { MergeEvent } from './merge.js'; +export type { DispatchEvent } from './dispatch.js'; +export type { ConfigureEvent } from './configure.js'; +export type { ErrorEvent } from './error.js'; + +import type { InitEvent } from './init.js'; +import type { AnalyzeEvent } from './analyze.js'; +import type { MergeEvent } from './merge.js'; +import type { DispatchEvent } from './dispatch.js'; +import type { ConfigureEvent } from './configure.js'; +import type { ErrorEvent } from './error.js'; + +/** All possible fleet events */ +export type FleetEvent = + | InitEvent + | AnalyzeEvent + | MergeEvent + | DispatchEvent + | ConfigureEvent + | ErrorEvent; + +/** Emitter function signature — handlers call this to emit events */ +export type FleetEmitter = (event: FleetEvent) => void; diff --git a/packages/fleet/src/shared/events/init.ts b/packages/fleet/src/shared/events/init.ts new file mode 100644 index 0000000..c22845a --- /dev/null +++ b/packages/fleet/src/shared/events/init.ts @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Init domain events */ +export type InitEvent = + | { type: 'init:start'; owner: string; repo: string } + | { type: 'init:branch:creating'; name: string; base: string } + | { type: 'init:branch:created'; name: string } + | { type: 'init:file:committed'; path: string } + | { type: 'init:file:skipped'; path: string; reason: string } + | { type: 'init:pr:creating' } + | { type: 'init:pr:created'; url: string; number: number } + | { type: 'init:done'; prUrl: string; files: string[]; labels: string[] } + // Wizard events + | { type: 'init:auth:detected'; method: 'token' | 'app' } + | { type: 'init:secret:uploading'; name: string } + | { type: 'init:secret:uploaded'; name: string } + | { type: 'init:secret:skipped'; name: string; reason: string } + | { type: 'init:dry-run'; files: string[] } + | { type: 'init:already-initialized' }; diff --git a/packages/fleet/src/shared/events/merge.ts b/packages/fleet/src/shared/events/merge.ts new file mode 100644 index 0000000..4a83fae --- /dev/null +++ b/packages/fleet/src/shared/events/merge.ts @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Merge domain events */ +export type MergeEvent = + | { type: 'merge:start'; owner: string; repo: string; mode: string; prCount: number } + | { type: 'merge:no-prs' } + | { type: 'merge:pr:processing'; number: number; title: string; retry?: number } + | { type: 'merge:branch:updating'; prNumber: number } + | { type: 'merge:branch:updated'; prNumber: number } + | { type: 'merge:ci:waiting'; prNumber: number } + | { type: 'merge:ci:check'; name: string; status: 'pass' | 'fail' | 'pending'; duration?: number } + | { type: 'merge:ci:passed'; prNumber: number } + | { type: 'merge:ci:failed'; prNumber: number } + | { type: 'merge:ci:timeout'; prNumber: number } + | { type: 'merge:ci:none'; prNumber: number } + | { type: 'merge:pr:merging'; prNumber: number } + | { type: 'merge:pr:merged'; prNumber: number } + | { type: 'merge:pr:skipped'; prNumber: number; reason: string } + | { type: 'merge:conflict:detected'; prNumber: number } + | { type: 'merge:redispatch:start'; oldPr: number } + | { type: 'merge:redispatch:waiting'; oldPr: number } + | { type: 'merge:redispatch:done'; oldPr: number; newPr: number } + | { + type: 'merge:done'; + merged: number[]; + skipped: number[]; + redispatched: Array<{ oldPr: number; newPr: number }>; + }; diff --git a/packages/fleet/src/shared/index.ts b/packages/fleet/src/shared/index.ts index 91ae157..2827d4b 100644 --- a/packages/fleet/src/shared/index.ts +++ b/packages/fleet/src/shared/index.ts @@ -25,3 +25,10 @@ export { } from './schemas/index.js'; export * from './auth/index.js'; export type { SessionDispatcher } from './session-dispatcher.js'; +export type { FleetEvent, FleetEmitter } from './events/index.js'; +export { + createRenderer, + createEmitter, + isInteractive, + type FleetRenderer, +} from './ui/index.js'; diff --git a/packages/fleet/src/shared/ui/assert-never.ts b/packages/fleet/src/shared/ui/assert-never.ts new file mode 100644 index 0000000..d6695fc --- /dev/null +++ b/packages/fleet/src/shared/ui/assert-never.ts @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Compile-time + runtime exhaustiveness check. + * Use as the default case in switch statements to ensure all event types are handled. + * + * @example + * ```ts + * switch (event.type) { + * case 'init:start': // ... + * default: assertNever(event); + * } + * ``` + */ +export function assertNever(value: never, message?: string): never { + throw new Error(message ?? `Unhandled event: ${JSON.stringify(value)}`); +} diff --git a/packages/fleet/src/shared/ui/index.ts b/packages/fleet/src/shared/ui/index.ts new file mode 100644 index 0000000..4656839 --- /dev/null +++ b/packages/fleet/src/shared/ui/index.ts @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { FleetRenderer } from './spec.js'; +import type { FleetEmitter } from '../events.js'; +import { InteractiveRenderer } from './interactive.js'; +import { PlainRenderer } from './plain.js'; + +export type { FleetRenderer } from './spec.js'; +export { InteractiveRenderer } from './interactive.js'; +export { PlainRenderer } from './plain.js'; +export { sessionUrl, repoConfigUrl, ansiLink } from './session-url.js'; + +/** + * Detect whether the current environment supports interactive UI. + */ +export function isInteractive(): boolean { + if (process.env.CI === 'true') return false; + if (!process.stdout.isTTY) return false; + return true; +} + +/** + * Create a renderer appropriate for the current environment. + */ +export function createRenderer(interactive?: boolean): FleetRenderer { + const useInteractive = interactive ?? isInteractive(); + return useInteractive ? new InteractiveRenderer() : new PlainRenderer(); +} + +/** + * Create an emitter function that forwards events to a renderer. + */ +export function createEmitter(renderer: FleetRenderer): FleetEmitter { + return (event) => renderer.render(event); +} diff --git a/packages/fleet/src/shared/ui/interactive.ts b/packages/fleet/src/shared/ui/interactive.ts new file mode 100644 index 0000000..fa85bec --- /dev/null +++ b/packages/fleet/src/shared/ui/interactive.ts @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as p from '@clack/prompts'; +import type { FleetEvent } from '../events.js'; +import type { FleetRenderer, RenderContext } from './spec.js'; +import { renderInitEvent } from './render/init.js'; +import { renderConfigureEvent } from './render/configure.js'; +import { renderAnalyzeEvent } from './render/analyze.js'; +import { renderDispatchEvent } from './render/dispatch.js'; +import { renderMergeEvent } from './render/merge.js'; +import { renderErrorEvent } from './render/error.js'; +import type { InitEvent } from '../events/init.js'; +import type { ConfigureEvent } from '../events/configure.js'; +import type { AnalyzeEvent } from '../events/analyze.js'; +import type { DispatchEvent } from '../events/dispatch.js'; +import type { MergeEvent } from '../events/merge.js'; +import type { ErrorEvent } from '../events/error.js'; + +/** + * InteractiveRenderer uses @clack/prompts for rich TUI output. + * Used when stdout is a TTY (local development). + * + * This is a thin shell — all domain-specific rendering is delegated + * to per-domain functions in render/*.ts via the RenderContext interface. + */ +export class InteractiveRenderer implements FleetRenderer { + private spinner: ReturnType | null = null; + + private ctx: RenderContext = { + info: (msg) => p.log.info(msg), + success: (msg) => p.log.success(msg), + warn: (msg) => p.log.warn(msg), + error: (msg) => p.log.error(msg), + message: (msg) => p.log.message(msg), + step: (msg) => p.log.step(msg), + startSpinner: (msg) => this.startSpinner(msg), + stopSpinner: (msg) => this.stopSpinner(msg), + }; + + start(title: string): void { + p.intro(title); + } + + end(message: string): void { + this.stopSpinner(); + p.outro(message); + } + + error(message: string): void { + this.stopSpinner(); + p.log.error(message); + } + + render(event: FleetEvent): void { + if (event.type.startsWith('init:')) return renderInitEvent(event as InitEvent, this.ctx); + if (event.type.startsWith('configure:')) return renderConfigureEvent(event as ConfigureEvent, this.ctx); + if (event.type.startsWith('analyze:')) return renderAnalyzeEvent(event as AnalyzeEvent, this.ctx); + if (event.type.startsWith('dispatch:')) return renderDispatchEvent(event as DispatchEvent, this.ctx); + if (event.type.startsWith('merge:')) return renderMergeEvent(event as MergeEvent, this.ctx); + if (event.type === 'error') return renderErrorEvent(event as ErrorEvent, this.ctx); + } + + // ── Private helpers ────────────────────────────────────────────── + private startSpinner(message: string): void { + this.stopSpinner(); + this.spinner = p.spinner(); + this.spinner.start(message); + } + + private stopSpinner(message?: string): void { + if (this.spinner) { + this.spinner.stop(message); + this.spinner = null; + } + } +} diff --git a/packages/fleet/src/shared/ui/plain.ts b/packages/fleet/src/shared/ui/plain.ts new file mode 100644 index 0000000..74165aa --- /dev/null +++ b/packages/fleet/src/shared/ui/plain.ts @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { FleetEvent } from '../events.js'; +import type { FleetRenderer, RenderContext } from './spec.js'; +import { renderInitEvent } from './render/init.js'; +import { renderConfigureEvent } from './render/configure.js'; +import { renderAnalyzeEvent } from './render/analyze.js'; +import { renderDispatchEvent } from './render/dispatch.js'; +import { renderMergeEvent } from './render/merge.js'; +import { renderErrorEvent } from './render/error.js'; +import type { InitEvent } from '../events/init.js'; +import type { ConfigureEvent } from '../events/configure.js'; +import type { AnalyzeEvent } from '../events/analyze.js'; +import type { DispatchEvent } from '../events/dispatch.js'; +import type { MergeEvent } from '../events/merge.js'; +import type { ErrorEvent } from '../events/error.js'; + +/** + * PlainRenderer uses console.log for CI-friendly plain text output. + * Used when stdout is not a TTY (CI environments). + * + * This is a thin shell — all domain-specific rendering is delegated + * to per-domain functions in render/*.ts via the RenderContext interface. + */ +export class PlainRenderer implements FleetRenderer { + private ctx: RenderContext = { + info: (msg) => console.log(msg), + success: (msg) => console.log(msg), + warn: (msg) => console.log(msg), + error: (msg) => console.error(msg), + message: (msg) => console.log(msg), + step: (msg) => console.log(msg), + startSpinner: (msg) => console.log(msg), + stopSpinner: (msg) => { if (msg) console.log(` ✓ ${msg}`); }, + }; + + start(title: string): void { + console.log(`\n═══ ${title} ═══\n`); + } + + end(message: string): void { + console.log(`\n═══ ${message} ═══\n`); + } + + error(message: string): void { + console.error(`ERROR: ${message}`); + } + + render(event: FleetEvent): void { + if (event.type.startsWith('init:')) return renderInitEvent(event as InitEvent, this.ctx); + if (event.type.startsWith('configure:')) return renderConfigureEvent(event as ConfigureEvent, this.ctx); + if (event.type.startsWith('analyze:')) return renderAnalyzeEvent(event as AnalyzeEvent, this.ctx); + if (event.type.startsWith('dispatch:')) return renderDispatchEvent(event as DispatchEvent, this.ctx); + if (event.type.startsWith('merge:')) return renderMergeEvent(event as MergeEvent, this.ctx); + if (event.type === 'error') return renderErrorEvent(event as ErrorEvent, this.ctx); + } +} diff --git a/packages/fleet/src/shared/ui/render/analyze.ts b/packages/fleet/src/shared/ui/render/analyze.ts new file mode 100644 index 0000000..9b82ae9 --- /dev/null +++ b/packages/fleet/src/shared/ui/render/analyze.ts @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { AnalyzeEvent } from '../../events/analyze.js'; +import type { RenderContext } from '../spec.js'; +import { sessionUrl } from '../session-url.js'; + +/** Render an analyze-domain event. */ +export function renderAnalyzeEvent(event: AnalyzeEvent, ctx: RenderContext): void { + switch (event.type) { + case 'analyze:start': + ctx.info(`Analyzing ${event.goalCount} goal(s) for ${event.owner}/${event.repo}`); + break; + case 'analyze:goal:start': + if (event.total > 1) { + ctx.step(`[${event.index}/${event.total}] ${event.file}`); + } else { + ctx.step(event.file); + } + if (event.milestone) ctx.info(` Milestone: ${event.milestone}`); + break; + case 'analyze:milestone:resolved': + ctx.info(` Milestone "${event.title}" (#${event.id})`); + break; + case 'analyze:context:fetched': + ctx.info( + ` Context: ${event.openIssues} open, ${event.closedIssues} closed, ${event.prs} PRs`, + ); + break; + case 'analyze:session:dispatching': + ctx.startSpinner(`Dispatching session for ${event.goal}…`); + break; + case 'analyze:session:started': + ctx.stopSpinner(`Session started: ${event.id}`); + ctx.info(` ${sessionUrl(event.id)}`); + break; + case 'analyze:session:failed': + ctx.stopSpinner(); + ctx.error(` Failed: ${event.error}`); + break; + case 'analyze:done': + ctx.success( + `Analysis complete — ${event.sessionsStarted} session(s) from ${event.goalsProcessed} goal(s)`, + ); + break; + } +} diff --git a/packages/fleet/src/shared/ui/render/configure.ts b/packages/fleet/src/shared/ui/render/configure.ts new file mode 100644 index 0000000..4a59a93 --- /dev/null +++ b/packages/fleet/src/shared/ui/render/configure.ts @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { ConfigureEvent } from '../../events/configure.js'; +import type { RenderContext } from '../spec.js'; + +/** Render a configure-domain event. */ +export function renderConfigureEvent(event: ConfigureEvent, ctx: RenderContext): void { + switch (event.type) { + case 'configure:start': + ctx.info(`Configuring ${event.resource} for ${event.owner}/${event.repo}`); + break; + case 'configure:label:created': + ctx.info(` ✓ Label "${event.name}" created`); + break; + case 'configure:label:exists': + ctx.warn(` ⊘ Label "${event.name}" already exists`); + break; + case 'configure:secret:uploading': + ctx.startSpinner(`Uploading secret ${event.name}…`); + break; + case 'configure:secret:uploaded': + ctx.stopSpinner(`Secret ${event.name} uploaded`); + break; + case 'configure:done': + ctx.success('Configuration complete'); + break; + } +} diff --git a/packages/fleet/src/shared/ui/render/dispatch.ts b/packages/fleet/src/shared/ui/render/dispatch.ts new file mode 100644 index 0000000..23ce9f4 --- /dev/null +++ b/packages/fleet/src/shared/ui/render/dispatch.ts @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { DispatchEvent } from '../../events/dispatch.js'; +import type { RenderContext } from '../spec.js'; +import { sessionUrl } from '../session-url.js'; + +/** Render a dispatch-domain event. */ +export function renderDispatchEvent(event: DispatchEvent, ctx: RenderContext): void { + switch (event.type) { + case 'dispatch:start': + ctx.info(`Dispatching from milestone ${event.milestone}`); + break; + case 'dispatch:scanning': + ctx.startSpinner('Scanning for fleet issues…'); + break; + case 'dispatch:found': + ctx.stopSpinner(`Found ${event.count} undispatched issue(s)`); + break; + case 'dispatch:issue:dispatching': + ctx.startSpinner(`#${event.number}: ${event.title}`); + break; + case 'dispatch:issue:dispatched': + ctx.stopSpinner(`#${event.number} → session ${event.sessionId}`); + ctx.info(` ${sessionUrl(event.sessionId)}`); + break; + case 'dispatch:issue:skipped': + ctx.warn(` ⊘ #${event.number}: ${event.reason}`); + break; + case 'dispatch:done': + ctx.success( + `Dispatch complete — ${event.dispatched} dispatched, ${event.skipped} skipped`, + ); + break; + } +} diff --git a/packages/fleet/src/shared/ui/render/error.ts b/packages/fleet/src/shared/ui/render/error.ts new file mode 100644 index 0000000..78a10d4 --- /dev/null +++ b/packages/fleet/src/shared/ui/render/error.ts @@ -0,0 +1,23 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { ErrorEvent } from '../../events/error.js'; +import type { RenderContext } from '../spec.js'; + +/** Render an error event. */ +export function renderErrorEvent(event: ErrorEvent, ctx: RenderContext): void { + ctx.stopSpinner(); + ctx.error(`[${event.code}] ${event.message}`); + if (event.suggestion) ctx.info(` 💡 ${event.suggestion}`); +} diff --git a/packages/fleet/src/shared/ui/render/init.ts b/packages/fleet/src/shared/ui/render/init.ts new file mode 100644 index 0000000..e9c10f5 --- /dev/null +++ b/packages/fleet/src/shared/ui/render/init.ts @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { InitEvent } from '../../events/init.js'; +import type { RenderContext } from '../spec.js'; + +/** Render an init-domain event. */ +export function renderInitEvent(event: InitEvent, ctx: RenderContext): void { + switch (event.type) { + case 'init:start': + ctx.info(`Initializing fleet for ${event.owner}/${event.repo}`); + break; + case 'init:branch:creating': + ctx.startSpinner(`Creating branch ${event.name} from ${event.base}`); + break; + case 'init:branch:created': + ctx.stopSpinner(`Branch ${event.name} created`); + break; + case 'init:file:committed': + ctx.info(` ✓ ${event.path}`); + break; + case 'init:file:skipped': + ctx.warn(` ⊘ ${event.path} — ${event.reason}`); + break; + case 'init:pr:creating': + ctx.startSpinner('Creating pull request…'); + break; + case 'init:pr:created': + ctx.stopSpinner(`PR #${event.number} created`); + ctx.info(` ${event.url}`); + break; + case 'init:done': + ctx.success(`Fleet initialized — PR: ${event.prUrl}`); + break; + case 'init:auth:detected': + ctx.success(`Auth: ${event.method === 'token' ? 'GITHUB_TOKEN' : 'GitHub App'}`); + break; + case 'init:secret:uploading': + ctx.startSpinner(`Uploading secret ${event.name}…`); + break; + case 'init:secret:uploaded': + ctx.stopSpinner(`Secret ${event.name} saved`); + break; + case 'init:secret:skipped': + ctx.warn(` ⊘ ${event.name} — ${event.reason}`); + break; + case 'init:dry-run': + ctx.info('Would create:'); + event.files.forEach((f) => ctx.message(` ${f}`)); + break; + case 'init:already-initialized': + ctx.warn('Repository is already initialized'); + break; + } +} diff --git a/packages/fleet/src/shared/ui/render/merge.ts b/packages/fleet/src/shared/ui/render/merge.ts new file mode 100644 index 0000000..c848b0a --- /dev/null +++ b/packages/fleet/src/shared/ui/render/merge.ts @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { MergeEvent } from '../../events/merge.js'; +import type { RenderContext } from '../spec.js'; + +/** Render a merge-domain event. */ +export function renderMergeEvent(event: MergeEvent, ctx: RenderContext): void { + switch (event.type) { + case 'merge:start': + ctx.info( + `Merging ${event.prCount} PR(s) in ${event.owner}/${event.repo} [${event.mode}]`, + ); + break; + case 'merge:no-prs': + ctx.info('No PRs ready to merge.'); + break; + case 'merge:pr:processing': + ctx.startSpinner( + `PR #${event.number}: ${event.title}${event.retry ? ` (retry ${event.retry})` : ''}`, + ); + break; + case 'merge:branch:updating': + ctx.startSpinner(`Updating branch for PR #${event.prNumber}…`); + break; + case 'merge:branch:updated': + ctx.stopSpinner(`Branch updated for PR #${event.prNumber}`); + break; + case 'merge:ci:waiting': + ctx.startSpinner(`Waiting for CI on PR #${event.prNumber}…`); + break; + case 'merge:ci:check': { + const icon = event.status === 'pass' ? '✓' : event.status === 'fail' ? '✗' : '…'; + const dur = event.duration ? ` (${event.duration}s)` : ''; + ctx.info(` ${icon} ${event.name}${dur}`); + break; + } + case 'merge:ci:passed': + ctx.stopSpinner(`CI passed for PR #${event.prNumber}`); + break; + case 'merge:ci:failed': + ctx.stopSpinner(`CI failed for PR #${event.prNumber}`); + break; + case 'merge:ci:timeout': + ctx.stopSpinner(`CI timed out for PR #${event.prNumber}`); + break; + case 'merge:ci:none': + ctx.stopSpinner(`No CI checks for PR #${event.prNumber}`); + break; + case 'merge:pr:merging': + ctx.startSpinner(`Merging PR #${event.prNumber}…`); + break; + case 'merge:pr:merged': + ctx.stopSpinner(`PR #${event.prNumber} merged ✓`); + break; + case 'merge:pr:skipped': + ctx.warn(` ⊘ PR #${event.prNumber}: ${event.reason}`); + break; + case 'merge:conflict:detected': + ctx.stopSpinner(`Conflict detected on PR #${event.prNumber}`); + break; + case 'merge:redispatch:start': + ctx.startSpinner(`Re-dispatching PR #${event.oldPr}…`); + break; + case 'merge:redispatch:waiting': + ctx.startSpinner(`Waiting for re-dispatched PR (was #${event.oldPr})…`); + break; + case 'merge:redispatch:done': + ctx.stopSpinner(`Re-dispatched: #${event.oldPr} → #${event.newPr}`); + break; + case 'merge:done': + ctx.success( + `Merge complete — ${event.merged.length} merged, ${event.skipped.length} skipped`, + ); + break; + } +} diff --git a/packages/fleet/src/shared/ui/session-url.ts b/packages/fleet/src/shared/ui/session-url.ts new file mode 100644 index 0000000..18d1ab6 --- /dev/null +++ b/packages/fleet/src/shared/ui/session-url.ts @@ -0,0 +1,37 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const JULES_BASE_URL = 'https://jules.google.com'; + +/** + * Build a Jules session URL from a session ID. + */ +export function sessionUrl(sessionId: string): string { + return `${JULES_BASE_URL}/sessions/${sessionId}`; +} + +/** + * Build a Jules repo config URL. + */ +export function repoConfigUrl(owner: string, repo: string): string { + return `${JULES_BASE_URL}/repo/github/${owner}/${repo}/config`; +} + +/** + * Wrap text in an ANSI hyperlink (OSC 8) for terminals that support it. + * Falls back to plain text in terminals that don't. + */ +export function ansiLink(text: string, url: string): string { + return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`; +} diff --git a/packages/fleet/src/shared/ui/spec.ts b/packages/fleet/src/shared/ui/spec.ts new file mode 100644 index 0000000..f8f9a38 --- /dev/null +++ b/packages/fleet/src/shared/ui/spec.ts @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { FleetEvent } from '../events.js'; + +/** + * FleetRenderer interface — the UI contract. + * Implementations render FleetEvents into terminal output. + */ +export interface FleetRenderer { + /** Render a single event */ + render(event: FleetEvent): void; + + /** Start the UI (e.g., intro banner) */ + start(title: string): void; + + /** End the UI (e.g., outro message) */ + end(message: string): void; + + /** End the UI with an error */ + error(message: string): void; +} + +/** + * RenderContext abstracts the difference between interactive (clack) and plain + * (console) rendering. Domain-specific render functions depend on this + * interface instead of coupling to @clack/prompts or console directly. + */ +export interface RenderContext { + info(msg: string): void; + success(msg: string): void; + warn(msg: string): void; + error(msg: string): void; + message(msg: string): void; + step(msg: string): void; + startSpinner(msg: string): void; + stopSpinner(msg?: string): void; +} diff --git a/tmp-fleet-test/package-lock.json b/tmp-fleet-test/package-lock.json new file mode 100644 index 0000000..2f9cb06 --- /dev/null +++ b/tmp-fleet-test/package-lock.json @@ -0,0 +1,1247 @@ +{ + "name": "tmp-fleet-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tmp-fleet-test", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@google/jules-fleet": "file:../packages/fleet/google-jules-fleet-0.0.1.tgz" + } + }, + "node_modules/@clack/core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.1.tgz", + "integrity": "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.1.tgz", + "integrity": "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.0.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@google/jules-fleet": { + "version": "0.0.1", + "resolved": "file:../packages/fleet/google-jules-fleet-0.0.1.tgz", + "integrity": "sha512-qNmKngbjb+rQIUnGgUvOdBW+29nPvmWe0rMqZoo/WMmRtanCvp2VMO2Lw8D0uJ047WDswG9/mu4zsw8Anlk2/Q==", + "license": "Apache-2.0", + "dependencies": { + "@clack/prompts": "^1.0.1", + "@octokit/auth-app": "^8.2.0", + "citty": "^0.1.6", + "glob": "^13.0.6", + "libsodium-wrappers": "^0.8.2", + "octokit": "^4.1.3", + "yaml": "^2.8.2", + "zod": "^3.25.0" + }, + "bin": { + "jules-fleet": "dist/cli/index.mjs" + }, + "peerDependencies": { + "@google/jules-sdk": "*" + }, + "peerDependenciesMeta": { + "@google/jules-sdk": { + "optional": true + } + } + }, + "node_modules/@octokit/app": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.6.tgz", + "integrity": "sha512-WELCamoCJo9SN0lf3SWZccf68CF0sBNPQuLYmZ/n87p5qvBJDe9aBtr5dHkh7T9nxWZ608pizwsUbypSzZAiUw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^7.2.1", + "@octokit/auth-unauthenticated": "^6.1.3", + "@octokit/core": "^6.1.5", + "@octokit/oauth-app": "^7.1.6", + "@octokit/plugin-paginate-rest": "^12.0.0", + "@octokit/types": "^14.0.0", + "@octokit/webhooks": "^13.6.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/auth-app": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.2.2.tgz", + "integrity": "sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.1.4", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-app": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.4.tgz", + "integrity": "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-device": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.5.tgz", + "integrity": "sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-user": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.6.tgz", + "integrity": "sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/oauth-authorization-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/oauth-methods": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.5.tgz", + "integrity": "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/app/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/app/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@octokit/auth-app": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.2.0.tgz", + "integrity": "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz", + "integrity": "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz", + "integrity": "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz", + "integrity": "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-token": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.3.tgz", + "integrity": "sha512-d5gWJla3WdSl1yjbfMpET+hUSFCE15qM0KVSB0H1shyuJihf/RL1KqWoZMIaonHvlNojkL9XtLFp8QeLe+1iwA==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", + "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/core/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/graphql/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@octokit/oauth-app": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.6.tgz", + "integrity": "sha512-OMcMzY2WFARg80oJNFwWbY51TBUfLH4JGTy119cqiDawSFXSIBujxmpXiKbGWQlvfn0CxE6f7/+c6+Kr5hI2YA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.1.3", + "@octokit/auth-oauth-user": "^5.1.3", + "@octokit/auth-unauthenticated": "^6.1.2", + "@octokit/core": "^6.1.4", + "@octokit/oauth-authorization-url": "^7.1.1", + "@octokit/oauth-methods": "^5.1.4", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-app": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.4.tgz", + "integrity": "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/auth-oauth-user": "^5.1.4", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-device": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.5.tgz", + "integrity": "sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-user": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.6.tgz", + "integrity": "sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.1.5", + "@octokit/oauth-methods": "^5.1.5", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/endpoint": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-authorization-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-methods": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.5.tgz", + "integrity": "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/request": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/oauth-app/node_modules/fast-content-type-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz", + "integrity": "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-11.0.0.tgz", + "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz", + "integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-12.0.0.tgz", + "integrity": "sha512-MPd6WK1VtZ52lFrgZ0R2FlaoiWllzgqFHaSZxvp72NmoDeZ0m8GeJdg4oB6ctqMTYyrnDYp592Xma21mrgiyDA==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-14.0.0.tgz", + "integrity": "sha512-iQt6ovem4b7zZYZQtdv+PwgbL5VPq37th1m2x2TdkgimIDJpsi2A6Q/OI/23i/hR6z5mL0EgisNR4dcbmckSZQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.2.1.tgz", + "integrity": "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-10.0.0.tgz", + "integrity": "sha512-Kuq5/qs0DVYTHZuBAzCZStCzo2nKvVRo/TDNhCcpC2TKiOGz/DisXMCvjt3/b5kr6SCI1Y8eeeJTHBxxpFvZEg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^6.1.3" + } + }, + "node_modules/@octokit/plugin-throttling/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "13.9.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.9.1.tgz", + "integrity": "sha512-Nss2b4Jyn4wB3EAqAPJypGuCJFalz/ZujKBQQ5934To7Xw9xjf4hkr/EAByxQY7hp7MKd790bWGz7XYSTsHmaw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-webhooks-types": "11.0.0", + "@octokit/request-error": "^6.1.7", + "@octokit/webhooks-methods": "^5.1.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.1.tgz", + "integrity": "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/@octokit/webhooks/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.160", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.160.tgz", + "integrity": "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "license": "Apache-2.0" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json-with-bigint": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.3.tgz", + "integrity": "sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A==", + "license": "MIT" + }, + "node_modules/libsodium": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz", + "integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz", + "integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.8.0" + } + }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/octokit": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-4.1.4.tgz", + "integrity": "sha512-cRvxRte6FU3vAHRC9+PMSY3D+mRAs2Rd9emMoqp70UGRvJRM3sbAoim2IXRZNNsf8wVfn4sGxVBHRAP+JBVX/g==", + "license": "MIT", + "dependencies": { + "@octokit/app": "^15.1.6", + "@octokit/core": "^6.1.5", + "@octokit/oauth-app": "^7.1.6", + "@octokit/plugin-paginate-graphql": "^5.2.4", + "@octokit/plugin-paginate-rest": "^12.0.0", + "@octokit/plugin-rest-endpoint-methods": "^14.0.0", + "@octokit/plugin-retry": "^7.2.1", + "@octokit/plugin-throttling": "^10.0.0", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", + "@octokit/webhooks": "^13.8.3" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit/node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "license": "MIT" + }, + "node_modules/octokit/node_modules/@octokit/request-error": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/tmp-fleet-test/package.json b/tmp-fleet-test/package.json new file mode 100644 index 0000000..c7e82c7 --- /dev/null +++ b/tmp-fleet-test/package.json @@ -0,0 +1,15 @@ +{ + "name": "tmp-fleet-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@google/jules-fleet": "file:../packages/fleet/google-jules-fleet-0.0.1.tgz" + } +}