issue-flow 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{analyze-TNFTUMOO.js → analyze-2RVGY5OK.js} +10 -8
- package/dist/{analyze-TNFTUMOO.js.map → analyze-2RVGY5OK.js.map} +1 -1
- package/dist/{chunk-5IRBHUYO.js → chunk-3E6KASSE.js} +11 -9
- package/dist/{chunk-5IRBHUYO.js.map → chunk-3E6KASSE.js.map} +1 -1
- package/dist/{chunk-A42R36BX.js → chunk-6IOPYFUM.js} +10 -8
- package/dist/{chunk-A42R36BX.js.map → chunk-6IOPYFUM.js.map} +1 -1
- package/dist/{chunk-JECNPBBO.js → chunk-6L2BPOE3.js} +11 -9
- package/dist/{chunk-JECNPBBO.js.map → chunk-6L2BPOE3.js.map} +1 -1
- package/dist/{chunk-DWF46N65.js → chunk-6UTVPKNF.js} +11 -9
- package/dist/{chunk-DWF46N65.js.map → chunk-6UTVPKNF.js.map} +1 -1
- package/dist/{chunk-6S2XT3JE.js → chunk-EOTULFWV.js} +10 -8
- package/dist/{chunk-6S2XT3JE.js.map → chunk-EOTULFWV.js.map} +1 -1
- package/dist/{chunk-CD7BMWXW.js → chunk-EYVXLRRD.js} +3 -3
- package/dist/{chunk-OE7RYTUT.js → chunk-G37JNQKB.js} +2 -2
- package/dist/chunk-JOK7VXRV.js +447 -0
- package/dist/chunk-JOK7VXRV.js.map +1 -0
- package/dist/{chunk-DF7SUHT5.js → chunk-KHTLN54U.js} +2 -2
- package/dist/{chunk-D56VWHSM.js → chunk-OPIDR2DC.js} +2 -2
- package/dist/{chunk-5NLLEWJD.js → chunk-RUTDK6ZF.js} +12 -2
- package/dist/{chunk-5NLLEWJD.js.map → chunk-RUTDK6ZF.js.map} +1 -1
- package/dist/chunk-SLHZPB3C.js +50 -0
- package/dist/chunk-SLHZPB3C.js.map +1 -0
- package/dist/{chunk-BMRBQ23Q.js → chunk-WGDLUPSF.js} +9 -7
- package/dist/{chunk-BMRBQ23Q.js.map → chunk-WGDLUPSF.js.map} +1 -1
- package/dist/{chunk-5Q2TMW7K.js → chunk-WO6ERABE.js} +2 -1
- package/dist/{chunk-QLVAEDVD.js → chunk-XPW46TI5.js} +3 -3
- package/dist/{chunk-I7FTA5FI.js → chunk-ZYBYMEWG.js} +2 -47
- package/dist/chunk-ZYBYMEWG.js.map +1 -0
- package/dist/cli.js +28 -13
- package/dist/cli.js.map +1 -1
- package/dist/{config-U3FC4WH6.js → config-OAZG7KIK.js} +3 -3
- package/dist/execute-AOEM52CD.js +14 -0
- package/dist/{generate-RHPOWGDX.js → generate-BEJF3SVJ.js} +10 -8
- package/dist/{generate-RHPOWGDX.js.map → generate-BEJF3SVJ.js.map} +1 -1
- package/dist/init-TBC3WI74.js +9 -0
- package/dist/plan-YDZYB2BO.js +16 -0
- package/dist/pr-SIXMJIZF.js +16 -0
- package/dist/pr-review-AY56ZPJR.js +16 -0
- package/dist/prd-MBSSBLGO.js +16 -0
- package/dist/review-2MDDHZ4X.js +14 -0
- package/dist/{run-Z6ZDXZB6.js → run-SEB6F3W4.js} +23 -196
- package/dist/run-SEB6F3W4.js.map +1 -0
- package/dist/web-3SBYL7QJ.js +188 -0
- package/dist/web-3SBYL7QJ.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-I7FTA5FI.js.map +0 -1
- package/dist/execute-BNBA77WJ.js +0 -13
- package/dist/init-IF72GR7O.js +0 -9
- package/dist/plan-FITZZXZQ.js +0 -15
- package/dist/pr-GO244D7M.js +0 -15
- package/dist/pr-review-XY6VL62M.js +0 -15
- package/dist/prd-2Y6RMVTF.js +0 -15
- package/dist/review-ITKXJMX3.js +0 -13
- package/dist/run-Z6ZDXZB6.js.map +0 -1
- /package/dist/{chunk-CD7BMWXW.js.map → chunk-EYVXLRRD.js.map} +0 -0
- /package/dist/{chunk-OE7RYTUT.js.map → chunk-G37JNQKB.js.map} +0 -0
- /package/dist/{chunk-DF7SUHT5.js.map → chunk-KHTLN54U.js.map} +0 -0
- /package/dist/{chunk-D56VWHSM.js.map → chunk-OPIDR2DC.js.map} +0 -0
- /package/dist/{chunk-5Q2TMW7K.js.map → chunk-WO6ERABE.js.map} +0 -0
- /package/dist/{chunk-QLVAEDVD.js.map → chunk-XPW46TI5.js.map} +0 -0
- /package/dist/{config-U3FC4WH6.js.map → config-OAZG7KIK.js.map} +0 -0
- /package/dist/{execute-BNBA77WJ.js.map → execute-AOEM52CD.js.map} +0 -0
- /package/dist/{init-IF72GR7O.js.map → init-TBC3WI74.js.map} +0 -0
- /package/dist/{plan-FITZZXZQ.js.map → plan-YDZYB2BO.js.map} +0 -0
- /package/dist/{pr-GO244D7M.js.map → pr-SIXMJIZF.js.map} +0 -0
- /package/dist/{pr-review-XY6VL62M.js.map → pr-review-AY56ZPJR.js.map} +0 -0
- /package/dist/{prd-2Y6RMVTF.js.map → prd-MBSSBLGO.js.map} +0 -0
- /package/dist/{review-ITKXJMX3.js.map → review-2MDDHZ4X.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/pr.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { runHeadless } from '../core/headless.js';\nimport { runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { Issue, ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/**\n * Extract a PR URL from headless output.\n */\nfunction parsePrUrl(output: string): string | null {\n const match = output.match(/(https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+)/);\n return match?.[1] ?? null;\n}\n\n/**\n * The numeric id inside a PR URL, so later phases (`pr-review`) address the\n * Pull Request without querying GitHub again.\n */\nfunction parsePrNumber(url: string): number | null {\n const match = url.match(/\\/pull\\/(\\d+)/);\n return match?.[1] === undefined ? null : Number(match[1]);\n}\n\n/**\n * The `Closes #N` line for the PR body, empty when the Issue has no remote\n * counterpart: GitHub only understands the reference for Issues it hosts, and\n * an invented `#N` would silently point at an unrelated Issue.\n */\nfunction issueClosesLine(issue: Issue, fallbackId: string): string {\n if (issue.remoteRef === null) {\n return '';\n }\n return `Closes #${issue.number ?? fallbackId}`;\n}\n\nexport async function runPr(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n // Get current branch\n let branchName: string;\n try {\n const proc = await execa('git', ['branch', '--show-current'], { reject: false });\n branchName = proc.stdout?.toString().trim() ?? '';\n if (!branchName) {\n printError('Could not determine current branch');\n return 1;\n }\n } catch {\n printError('Failed to get current branch');\n return 1;\n }\n\n const template = await loadPrompt('pr');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __BRANCH_NAME__: branchName,\n __TASKS_PATH__: tasksPath,\n __ISSUE_CLOSES__: issueClosesLine(resolution.resolved.issue, issueNumber),\n ...issuePlaceholders(resolution.resolved),\n });\n\n let headlessOutput = '';\n\n const outcome = await runPhaseWithRetry({\n phase: 'pr',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 15,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text parsePrUrl() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Creating PR for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('pr', result.cost, startedAtMs);\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `PR creation failed: ${result.error}`,\n };\n }\n\n headlessOutput = result.result;\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `PR creation failed for issue #${issueNumber}`);\n return 1;\n }\n\n const prUrl = parsePrUrl(headlessOutput);\n\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.prCreated = true;\n const prNumber = prUrl === null ? null : parsePrNumber(prUrl);\n // Without a URL the PR is still assumed created (the phase succeeded), but\n // there is nothing trustworthy to record: an invented number would send\n // `pr-review` at an unrelated Pull Request.\n if (prUrl !== null && prNumber !== null) {\n plan.pullRequest = {\n number: prNumber,\n url: prUrl,\n headBranch: branchName,\n createdAt: isoNow(),\n };\n }\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n if (prUrl) {\n printSuccess(`PR created: ${prUrl}`);\n } else {\n printSuccess('PR creation completed');\n }\n return 0;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/commands/pr.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { runHeadless } from '../core/headless.js';\nimport { runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { Issue, ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/**\n * Extract a PR URL from headless output.\n */\nfunction parsePrUrl(output: string): string | null {\n const match = output.match(/(https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+)/);\n return match?.[1] ?? null;\n}\n\n/**\n * The numeric id inside a PR URL, so later phases (`pr-review`) address the\n * Pull Request without querying GitHub again.\n */\nfunction parsePrNumber(url: string): number | null {\n const match = url.match(/\\/pull\\/(\\d+)/);\n return match?.[1] === undefined ? null : Number(match[1]);\n}\n\n/**\n * The `Closes #N` line for the PR body, empty when the Issue has no remote\n * counterpart: GitHub only understands the reference for Issues it hosts, and\n * an invented `#N` would silently point at an unrelated Issue.\n */\nfunction issueClosesLine(issue: Issue, fallbackId: string): string {\n if (issue.remoteRef === null) {\n return '';\n }\n return `Closes #${issue.number ?? fallbackId}`;\n}\n\nexport async function runPr(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n // Get current branch\n let branchName: string;\n try {\n const proc = await execa('git', ['branch', '--show-current'], { reject: false });\n branchName = proc.stdout?.toString().trim() ?? '';\n if (!branchName) {\n printError('Could not determine current branch');\n return 1;\n }\n } catch {\n printError('Failed to get current branch');\n return 1;\n }\n\n const template = await loadPrompt('pr');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __BRANCH_NAME__: branchName,\n __TASKS_PATH__: tasksPath,\n __ISSUE_CLOSES__: issueClosesLine(resolution.resolved.issue, issueNumber),\n ...issuePlaceholders(resolution.resolved),\n });\n\n let headlessOutput = '';\n\n const outcome = await runPhaseWithRetry({\n phase: 'pr',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 15,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text parsePrUrl() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Creating PR for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('pr', result.cost, startedAtMs);\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `PR creation failed: ${result.error}`,\n };\n }\n\n headlessOutput = result.result;\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `PR creation failed for issue #${issueNumber}`);\n return 1;\n }\n\n const prUrl = parsePrUrl(headlessOutput);\n\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.prCreated = true;\n const prNumber = prUrl === null ? null : parsePrNumber(prUrl);\n // Without a URL the PR is still assumed created (the phase succeeded), but\n // there is nothing trustworthy to record: an invented number would send\n // `pr-review` at an unrelated Pull Request.\n if (prUrl !== null && prNumber !== null) {\n plan.pullRequest = {\n number: prNumber,\n url: prUrl,\n headBranch: branchName,\n createdAt: isoNow(),\n };\n }\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n if (prUrl) {\n printSuccess(`PR created: ${prUrl}`);\n } else {\n printSuccess('PR creation completed');\n }\n return 0;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
import {
|
|
3
3
|
issuePlaceholders,
|
|
4
4
|
resolveCommandIssue
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EYVXLRRD.js";
|
|
6
6
|
import {
|
|
7
7
|
runHeadless
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KHTLN54U.js";
|
|
9
9
|
import {
|
|
10
|
-
applyPlaceholders,
|
|
11
|
-
loadPrompt,
|
|
12
10
|
publishPhaseMetrics
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ZYBYMEWG.js";
|
|
12
|
+
import {
|
|
13
|
+
applyPlaceholders,
|
|
14
|
+
loadPrompt
|
|
15
|
+
} from "./chunk-SLHZPB3C.js";
|
|
14
16
|
import {
|
|
15
17
|
resolveIssuePaths
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-RUTDK6ZF.js";
|
|
17
19
|
import {
|
|
18
20
|
getGlobalTimeout,
|
|
19
21
|
loadTaskPlan,
|
|
20
22
|
printError,
|
|
21
23
|
printSuccess,
|
|
22
24
|
saveTaskPlan
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-WO6ERABE.js";
|
|
24
26
|
|
|
25
27
|
// src/commands/review.ts
|
|
26
28
|
function parseReviewResult(output) {
|
|
@@ -105,4 +107,4 @@ async function runReview(issue, resolvedIssue) {
|
|
|
105
107
|
export {
|
|
106
108
|
runReview
|
|
107
109
|
};
|
|
108
|
-
//# sourceMappingURL=chunk-
|
|
110
|
+
//# sourceMappingURL=chunk-EOTULFWV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\n\nexport interface ReviewResult {\n status: 'PASS' | 'FAIL';\n findings?: string;\n}\n\n/**\n * Parse the <review-result> block from headless output.\n */\nfunction parseReviewResult(output: string): ReviewResult {\n const match = output.match(/<review-result>([\\s\\S]*?)<\\/review-result>/);\n if (!match) {\n // Try to detect PASS/FAIL from the raw output\n if (/STATUS:\\s*PASS/i.test(output)) {\n return { status: 'PASS' };\n }\n if (/STATUS:\\s*FAIL/i.test(output)) {\n const findingsMatch = output.match(/FINDINGS:([\\s\\S]*?)(?:$|<\\/)/);\n return { status: 'FAIL', findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings' };\n }\n // Default to PASS if no explicit status found and no errors\n return { status: 'PASS' };\n }\n\n const block = match[1];\n if (/STATUS:\\s*PASS/i.test(block)) {\n return { status: 'PASS' };\n }\n\n const findingsMatch = block.match(/FINDINGS:([\\s\\S]*)/);\n return {\n status: 'FAIL',\n findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings',\n };\n}\n\nexport async function runReview(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n const template = await loadPrompt('review');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __TASKS_PATH__: tasksPath,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseReviewResult() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\n // Before the success check: the tokens were spent either way. A correction\n // cycle calls runReview() again and the reducer sums both invocations.\n publishPhaseMetrics('review', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Review failed: ${result.error}`);\n return 1;\n }\n\n const review = parseReviewResult(result.result);\n\n if (review.status === 'PASS') {\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = true;\n plan.lastReviewFindings = null;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n printSuccess('Review PASSED — implementation meets acceptance criteria');\n return 0;\n }\n\n printError('Review FAILED — issues found:');\n if (review.findings) {\n console.log(review.findings);\n }\n\n // Persist the findings so a correction cycle can act on them: without this,\n // a re-run of execute sees every userStories[].passes already true and\n // exits immediately without attempting any fix (see engine.ts's\n // \"already complete\" guards).\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = review.findings ?? 'Unknown findings';\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n return 1;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\n\nexport interface ReviewResult {\n status: 'PASS' | 'FAIL';\n findings?: string;\n}\n\n/**\n * Parse the <review-result> block from headless output.\n */\nfunction parseReviewResult(output: string): ReviewResult {\n const match = output.match(/<review-result>([\\s\\S]*?)<\\/review-result>/);\n if (!match) {\n // Try to detect PASS/FAIL from the raw output\n if (/STATUS:\\s*PASS/i.test(output)) {\n return { status: 'PASS' };\n }\n if (/STATUS:\\s*FAIL/i.test(output)) {\n const findingsMatch = output.match(/FINDINGS:([\\s\\S]*?)(?:$|<\\/)/);\n return { status: 'FAIL', findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings' };\n }\n // Default to PASS if no explicit status found and no errors\n return { status: 'PASS' };\n }\n\n const block = match[1];\n if (/STATUS:\\s*PASS/i.test(block)) {\n return { status: 'PASS' };\n }\n\n const findingsMatch = block.match(/FINDINGS:([\\s\\S]*)/);\n return {\n status: 'FAIL',\n findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings',\n };\n}\n\nexport async function runReview(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n const template = await loadPrompt('review');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __TASKS_PATH__: tasksPath,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseReviewResult() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\n // Before the success check: the tokens were spent either way. A correction\n // cycle calls runReview() again and the reducer sums both invocations.\n publishPhaseMetrics('review', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Review failed: ${result.error}`);\n return 1;\n }\n\n const review = parseReviewResult(result.result);\n\n if (review.status === 'PASS') {\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = true;\n plan.lastReviewFindings = null;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n printSuccess('Review PASSED — implementation meets acceptance criteria');\n return 0;\n }\n\n printError('Review FAILED — issues found:');\n if (review.findings) {\n console.log(review.findings);\n }\n\n // Persist the findings so a correction cycle can act on them: without this,\n // a re-run of execute sees every userStories[].passes already true and\n // exits immediately without attempting any fix (see engine.ts's\n // \"already complete\" guards).\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = review.findings ?? 'Unknown findings';\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n return 1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAkBA,SAAS,kBAAkB,QAA8B;AACvD,QAAM,QAAQ,OAAO,MAAM,4CAA4C;AACvE,MAAI,CAAC,OAAO;AAEV,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,aAAO,EAAE,QAAQ,OAAO;AAAA,IAC1B;AACA,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,YAAMA,iBAAgB,OAAO,MAAM,8BAA8B;AACjE,aAAO,EAAE,QAAQ,QAAQ,UAAUA,iBAAgB,CAAC,GAAG,KAAK,KAAK,mBAAmB;AAAA,IACtF;AAEA,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,QAAQ,MAAM,CAAC;AACrB,MAAI,kBAAkB,KAAK,KAAK,GAAG;AACjC,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,gBAAgB,MAAM,MAAM,oBAAoB;AACtD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,UAAU,gBAAgB,CAAC,GAAG,KAAK,KAAK;AAAA,EAC1C;AACF;AAEA,eAAsB,UAAU,OAAe,eAAgD;AAC7F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,YAAY,MAAM;AAExB,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,WAAW,MAAM,WAAW,QAAQ;AAC1C,QAAM,SAAS,kBAAkB,UAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,IAChB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAAA,IAC7C,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAGD,sBAAoB,UAAU,OAAO,MAAM,WAAW;AAEtD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,kBAAkB,OAAO,KAAK,EAAE;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,kBAAkB,OAAO,MAAM;AAE9C,MAAI,OAAO,WAAW,QAAQ;AAE5B,QAAI;AACF,YAAM,OAAO,MAAM,aAAa,SAAS;AACzC,WAAK,SAAS,kBAAkB;AAChC,WAAK,qBAAqB;AAC1B,YAAM,aAAa,WAAW,IAAI;AAAA,IACpC,QAAQ;AAAA,IAER;AAEA,iBAAa,+DAA0D;AACvE,WAAO;AAAA,EACT;AAEA,aAAW,oCAA+B;AAC1C,MAAI,OAAO,UAAU;AACnB,YAAQ,IAAI,OAAO,QAAQ;AAAA,EAC7B;AAMA,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,kBAAkB;AAChC,SAAK,qBAAqB,OAAO,YAAY;AAC7C,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;","names":["findingsMatch"]}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
resolveIssuePaths,
|
|
6
6
|
resolveProjectPaths,
|
|
7
7
|
run
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RUTDK6ZF.js";
|
|
9
9
|
import {
|
|
10
10
|
isoNow,
|
|
11
11
|
issueMetadataSchema,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
printInfo,
|
|
14
14
|
printWarning,
|
|
15
15
|
writeFileAtomic
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-WO6ERABE.js";
|
|
17
17
|
|
|
18
18
|
// src/issues/registry.ts
|
|
19
19
|
var providers = /* @__PURE__ */ new Map();
|
|
@@ -753,4 +753,4 @@ export {
|
|
|
753
753
|
issuePlaceholders,
|
|
754
754
|
resolveCommandIssue
|
|
755
755
|
};
|
|
756
|
-
//# sourceMappingURL=chunk-
|
|
756
|
+
//# sourceMappingURL=chunk-EYVXLRRD.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
printInfo,
|
|
5
5
|
printSuccess,
|
|
6
6
|
printWarning
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WO6ERABE.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/init.ts
|
|
10
10
|
import { execa } from "execa";
|
|
@@ -132,4 +132,4 @@ async function runInit(source = "github") {
|
|
|
132
132
|
export {
|
|
133
133
|
runInit
|
|
134
134
|
};
|
|
135
|
-
//# sourceMappingURL=chunk-
|
|
135
|
+
//# sourceMappingURL=chunk-G37JNQKB.js.map
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolvePackageDir
|
|
4
|
+
} from "./chunk-SLHZPB3C.js";
|
|
5
|
+
import {
|
|
6
|
+
getGlobalRoot,
|
|
7
|
+
webLockSchema
|
|
8
|
+
} from "./chunk-RUTDK6ZF.js";
|
|
9
|
+
import {
|
|
10
|
+
NullPublisher,
|
|
11
|
+
isoNow,
|
|
12
|
+
printInfo,
|
|
13
|
+
printWarning
|
|
14
|
+
} from "./chunk-WO6ERABE.js";
|
|
15
|
+
|
|
16
|
+
// src/web/lock.ts
|
|
17
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
18
|
+
import { readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
19
|
+
import { join as join2 } from "path";
|
|
20
|
+
import chalk2 from "chalk";
|
|
21
|
+
|
|
22
|
+
// src/web/server.ts
|
|
23
|
+
import { createHash } from "crypto";
|
|
24
|
+
import { readFile } from "fs/promises";
|
|
25
|
+
import { createServer } from "http";
|
|
26
|
+
import { createRequire } from "module";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
import chalk from "chalk";
|
|
29
|
+
var require2 = createRequire(import.meta.url);
|
|
30
|
+
function readPackageVersion() {
|
|
31
|
+
try {
|
|
32
|
+
return require2("../../package.json").version;
|
|
33
|
+
} catch {
|
|
34
|
+
return "0.0.0";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var JSON_TYPE = "application/json; charset=utf-8";
|
|
38
|
+
function publisherSessionSource(publisher) {
|
|
39
|
+
return {
|
|
40
|
+
list: () => {
|
|
41
|
+
const snapshot = publisher.snapshot();
|
|
42
|
+
return snapshot.sessionId === null ? [] : [snapshot];
|
|
43
|
+
},
|
|
44
|
+
get: (sessionId) => {
|
|
45
|
+
const snapshot = publisher.snapshot();
|
|
46
|
+
return snapshot.sessionId === sessionId ? snapshot : void 0;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function directorySessionSource(handle) {
|
|
51
|
+
return {
|
|
52
|
+
list: () => handle.sessions().map((entry) => entry.snapshot),
|
|
53
|
+
get: (sessionId) => handle.getSession(sessionId)?.snapshot
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function resolveSessionSource(options) {
|
|
57
|
+
if (options.sessions) return directorySessionSource(options.sessions);
|
|
58
|
+
return publisherSessionSource(options.publisher ?? new NullPublisher());
|
|
59
|
+
}
|
|
60
|
+
var STATIC_ROUTES = {
|
|
61
|
+
"/": { file: "index.html", contentType: "text/html; charset=utf-8" },
|
|
62
|
+
"/app.css": { file: "app.css", contentType: "text/css; charset=utf-8" },
|
|
63
|
+
"/app.js": { file: "app.js", contentType: "text/javascript; charset=utf-8" }
|
|
64
|
+
};
|
|
65
|
+
async function loadStaticAssets(publicDir) {
|
|
66
|
+
const assets = /* @__PURE__ */ new Map();
|
|
67
|
+
if (publicDir === null) return assets;
|
|
68
|
+
for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {
|
|
69
|
+
try {
|
|
70
|
+
const body = await readFile(join(publicDir, file), "utf-8");
|
|
71
|
+
assets.set(route, { body, contentType });
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return assets;
|
|
76
|
+
}
|
|
77
|
+
function baseHeaders(res) {
|
|
78
|
+
res.setHeader("Cache-Control", "no-store");
|
|
79
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
80
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
81
|
+
res.setHeader("X-Robots-Tag", "noindex, nofollow");
|
|
82
|
+
}
|
|
83
|
+
function respond(res, status, contentType, body) {
|
|
84
|
+
res.statusCode = status;
|
|
85
|
+
res.setHeader("Content-Type", contentType);
|
|
86
|
+
res.end(body);
|
|
87
|
+
}
|
|
88
|
+
function respondJson(res, status, payload) {
|
|
89
|
+
respond(res, status, JSON_TYPE, JSON.stringify(payload));
|
|
90
|
+
}
|
|
91
|
+
async function startWebServer(options) {
|
|
92
|
+
const info = options.info ?? printInfo;
|
|
93
|
+
const warn = options.warn ?? printWarning;
|
|
94
|
+
const version = options.version ?? readPackageVersion();
|
|
95
|
+
const startedAtMs = Date.now();
|
|
96
|
+
const assets = await loadStaticAssets(
|
|
97
|
+
options.publicDir ?? resolvePackageDir(join("web", "public"))
|
|
98
|
+
);
|
|
99
|
+
const source = resolveSessionSource(options);
|
|
100
|
+
const statusCache = /* @__PURE__ */ new Map();
|
|
101
|
+
const statusPayload = (snapshot) => {
|
|
102
|
+
const key = snapshot.sessionId ?? "";
|
|
103
|
+
const body = JSON.stringify(snapshot);
|
|
104
|
+
const cached = statusCache.get(key);
|
|
105
|
+
if (cached !== void 0 && cached.body === body) return cached;
|
|
106
|
+
const etag = `"${createHash("sha1").update(body).digest("hex")}"`;
|
|
107
|
+
const entry = { body, etag };
|
|
108
|
+
statusCache.set(key, entry);
|
|
109
|
+
return entry;
|
|
110
|
+
};
|
|
111
|
+
const handleRequest = (req, res) => {
|
|
112
|
+
baseHeaders(res);
|
|
113
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
114
|
+
const path = requestUrl.pathname;
|
|
115
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
116
|
+
respondJson(res, 404, { error: "Not found" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (path === "/api/status" || path === "/status.json") {
|
|
120
|
+
const sessionId = requestUrl.searchParams.get("session");
|
|
121
|
+
let snapshot;
|
|
122
|
+
if (sessionId !== null) {
|
|
123
|
+
snapshot = source.get(sessionId);
|
|
124
|
+
if (snapshot === void 0) {
|
|
125
|
+
respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const active = source.list();
|
|
130
|
+
if (active.length === 0) {
|
|
131
|
+
respondJson(res, 404, {
|
|
132
|
+
error: "No active session. Pass ?session=<id> \u2014 see GET /api/sessions."
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (active.length > 1) {
|
|
137
|
+
respondJson(res, 409, {
|
|
138
|
+
error: "Multiple active sessions; specify ?session=<id>.",
|
|
139
|
+
sessions: active.map((entry) => entry.sessionId)
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
snapshot = active[0];
|
|
144
|
+
}
|
|
145
|
+
const { body, etag } = statusPayload(snapshot);
|
|
146
|
+
res.setHeader("ETag", etag);
|
|
147
|
+
if (req.headers["if-none-match"] === etag) {
|
|
148
|
+
res.statusCode = 304;
|
|
149
|
+
res.end();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
respond(res, 200, JSON_TYPE, body);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (path === "/api/sessions") {
|
|
156
|
+
respondJson(
|
|
157
|
+
res,
|
|
158
|
+
200,
|
|
159
|
+
source.list().map((snapshot) => ({
|
|
160
|
+
sessionId: snapshot.sessionId,
|
|
161
|
+
issueNumber: snapshot.issue.number,
|
|
162
|
+
status: snapshot.status,
|
|
163
|
+
startedAt: snapshot.startedAt,
|
|
164
|
+
updatedAt: snapshot.updatedAt,
|
|
165
|
+
statusUrl: `/api/status?session=${encodeURIComponent(snapshot.sessionId ?? "")}`
|
|
166
|
+
}))
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (path === "/api/health") {
|
|
171
|
+
respondJson(res, 200, {
|
|
172
|
+
ok: true,
|
|
173
|
+
uptime: Math.round((Date.now() - startedAtMs) / 1e3),
|
|
174
|
+
version,
|
|
175
|
+
refreshSeconds: options.refreshSeconds ?? 5
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const asset = assets.get(path);
|
|
180
|
+
if (asset) {
|
|
181
|
+
respond(res, 200, asset.contentType, asset.body);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
respondJson(res, 404, { error: "Not found" });
|
|
185
|
+
};
|
|
186
|
+
const server = createServer((req, res) => {
|
|
187
|
+
try {
|
|
188
|
+
handleRequest(req, res);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
try {
|
|
191
|
+
respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const listening = await new Promise((resolve) => {
|
|
197
|
+
const onError = (err) => {
|
|
198
|
+
if (err.code === "EADDRINUSE") {
|
|
199
|
+
warn(
|
|
200
|
+
`Web monitor port ${options.port} is already in use. Continuing without the web server.`
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);
|
|
204
|
+
}
|
|
205
|
+
resolve(false);
|
|
206
|
+
};
|
|
207
|
+
server.once("error", onError);
|
|
208
|
+
server.listen(options.port, options.host, () => {
|
|
209
|
+
server.removeListener("error", onError);
|
|
210
|
+
resolve(true);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
if (!listening) return null;
|
|
214
|
+
if (options.unref !== false) {
|
|
215
|
+
server.unref();
|
|
216
|
+
}
|
|
217
|
+
server.on("error", (err) => {
|
|
218
|
+
warn(`Web monitor server error: ${err.message}`);
|
|
219
|
+
});
|
|
220
|
+
const address = server.address();
|
|
221
|
+
const port = address.port;
|
|
222
|
+
const displayHost = options.host === "0.0.0.0" ? "localhost" : options.host;
|
|
223
|
+
const url = `http://${displayHost}:${port}`;
|
|
224
|
+
if (options.host === "0.0.0.0") {
|
|
225
|
+
warn("Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.");
|
|
226
|
+
}
|
|
227
|
+
info(`Web monitor running at ${chalk.bold.cyan(url)}`);
|
|
228
|
+
let closed = false;
|
|
229
|
+
let closePromise = null;
|
|
230
|
+
const doClose = () => new Promise((resolve) => {
|
|
231
|
+
process.removeListener("SIGINT", onSignal);
|
|
232
|
+
process.removeListener("SIGTERM", onSignal);
|
|
233
|
+
server.close(() => resolve());
|
|
234
|
+
server.closeAllConnections();
|
|
235
|
+
});
|
|
236
|
+
const close = () => {
|
|
237
|
+
if (closed) return closePromise ?? Promise.resolve();
|
|
238
|
+
closed = true;
|
|
239
|
+
closePromise = doClose();
|
|
240
|
+
return closePromise;
|
|
241
|
+
};
|
|
242
|
+
const handle = { server, host: options.host, port, url, close };
|
|
243
|
+
const onSignal = (signal) => {
|
|
244
|
+
void handle.close().finally(() => {
|
|
245
|
+
process.kill(process.pid, signal);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
process.once("SIGINT", onSignal);
|
|
249
|
+
process.once("SIGTERM", onSignal);
|
|
250
|
+
return handle;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/web/lock.ts
|
|
254
|
+
var WEB_LOCK_FILENAME = "web.lock";
|
|
255
|
+
var HEALTH_PROBE_TIMEOUT_MS = 1e3;
|
|
256
|
+
var LOCK_CLAIM_RETRIES = 5;
|
|
257
|
+
var LOCK_CLAIM_RETRY_DELAY_MS = 100;
|
|
258
|
+
function getWebLockFile(options = {}) {
|
|
259
|
+
return join2(getGlobalRoot(options), WEB_LOCK_FILENAME);
|
|
260
|
+
}
|
|
261
|
+
function sleep(ms) {
|
|
262
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
263
|
+
}
|
|
264
|
+
function isProcessAlive(pid) {
|
|
265
|
+
try {
|
|
266
|
+
process.kill(pid, 0);
|
|
267
|
+
return true;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return err.code === "EPERM";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function probeUrl(host, port) {
|
|
273
|
+
const probeHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
274
|
+
return `http://${probeHost}:${port}/api/health`;
|
|
275
|
+
}
|
|
276
|
+
async function probeHealth(host, port) {
|
|
277
|
+
try {
|
|
278
|
+
const res = await fetch(probeUrl(host, port), {
|
|
279
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS)
|
|
280
|
+
});
|
|
281
|
+
return res.ok;
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function readWebLock(lockFile) {
|
|
287
|
+
let raw;
|
|
288
|
+
try {
|
|
289
|
+
raw = await readFile2(lockFile, "utf-8");
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const result = webLockSchema.safeParse(JSON.parse(raw));
|
|
295
|
+
return result.success ? result.data : null;
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function removeWebLock(lockFile) {
|
|
301
|
+
try {
|
|
302
|
+
await rm(lockFile, { force: true });
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function detectActiveInstance(lockFile) {
|
|
307
|
+
const lock = await readWebLock(lockFile);
|
|
308
|
+
if (lock === null) return null;
|
|
309
|
+
if (isProcessAlive(lock.pid) && await probeHealth(lock.host, lock.port)) {
|
|
310
|
+
return lock;
|
|
311
|
+
}
|
|
312
|
+
await removeWebLock(lockFile);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
async function claimWebLock(lockFile, lock) {
|
|
316
|
+
try {
|
|
317
|
+
await writeFile(lockFile, JSON.stringify(lock, null, 2), { encoding: "utf-8", flag: "wx" });
|
|
318
|
+
return true;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err.code === "EEXIST") return false;
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function reusedHandle(lock) {
|
|
325
|
+
const displayHost = lock.host === "0.0.0.0" ? "localhost" : lock.host;
|
|
326
|
+
return {
|
|
327
|
+
host: lock.host,
|
|
328
|
+
port: lock.port,
|
|
329
|
+
url: `http://${displayHost}:${lock.port}`,
|
|
330
|
+
close: async () => {
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async function ensureSingleWebServer(options, lockOptions = {}) {
|
|
335
|
+
const warn = options.warn ?? printWarning;
|
|
336
|
+
const info = options.info ?? printInfo;
|
|
337
|
+
const lockFile = getWebLockFile(lockOptions);
|
|
338
|
+
for (let attempt = 0; attempt <= LOCK_CLAIM_RETRIES; attempt++) {
|
|
339
|
+
const active = await detectActiveInstance(lockFile);
|
|
340
|
+
if (active !== null) {
|
|
341
|
+
info(`Reusing existing web monitor at ${reusedHandle(active).url}`);
|
|
342
|
+
return reusedHandle(active);
|
|
343
|
+
}
|
|
344
|
+
const handle = await startWebServer(options);
|
|
345
|
+
if (handle === null) {
|
|
346
|
+
await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
let claimed;
|
|
350
|
+
try {
|
|
351
|
+
claimed = await claimWebLock(lockFile, {
|
|
352
|
+
pid: process.pid,
|
|
353
|
+
port: handle.port,
|
|
354
|
+
host: handle.host,
|
|
355
|
+
startedAt: isoNow()
|
|
356
|
+
});
|
|
357
|
+
} catch (err) {
|
|
358
|
+
await handle.close();
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
if (!claimed) {
|
|
362
|
+
await handle.close();
|
|
363
|
+
await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const originalClose = handle.close;
|
|
367
|
+
handle.close = async () => {
|
|
368
|
+
await originalClose();
|
|
369
|
+
await removeWebLock(lockFile);
|
|
370
|
+
};
|
|
371
|
+
return handle;
|
|
372
|
+
}
|
|
373
|
+
warn("Could not determine the web monitor lock ownership. Continuing without the web server.");
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
var DETACHED_CLAIM_TIMEOUT_MS = 5e3;
|
|
377
|
+
var DETACHED_CLAIM_POLL_MS = 150;
|
|
378
|
+
async function ensureWebMonitor(options, monitorOptions = {}) {
|
|
379
|
+
const warn = options.warn ?? printWarning;
|
|
380
|
+
const info = options.info ?? printInfo;
|
|
381
|
+
let lockFile;
|
|
382
|
+
try {
|
|
383
|
+
lockFile = getWebLockFile(monitorOptions);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
warn(
|
|
386
|
+
`Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`
|
|
387
|
+
);
|
|
388
|
+
return startWebServer(options);
|
|
389
|
+
}
|
|
390
|
+
const active = await detectActiveInstance(lockFile);
|
|
391
|
+
if (active !== null) {
|
|
392
|
+
info(`Reusing existing web monitor at ${reusedHandle(active).url}`);
|
|
393
|
+
return reusedHandle(active);
|
|
394
|
+
}
|
|
395
|
+
const execPath = monitorOptions.execPath ?? process.execPath;
|
|
396
|
+
const entryScript = monitorOptions.entryScript ?? process.argv[1];
|
|
397
|
+
if (entryScript === void 0) {
|
|
398
|
+
warn("Web monitor: could not determine the CLI entry point to spawn a detached server.");
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const args = [
|
|
402
|
+
entryScript,
|
|
403
|
+
"web",
|
|
404
|
+
"serve",
|
|
405
|
+
"--port",
|
|
406
|
+
String(options.port),
|
|
407
|
+
"--host",
|
|
408
|
+
options.host
|
|
409
|
+
];
|
|
410
|
+
if (options.refreshSeconds !== void 0) {
|
|
411
|
+
args.push("--refresh", String(options.refreshSeconds));
|
|
412
|
+
}
|
|
413
|
+
const spawnFn = monitorOptions.spawn ?? nodeSpawn;
|
|
414
|
+
try {
|
|
415
|
+
const child = spawnFn(execPath, args, { detached: true, stdio: "ignore" });
|
|
416
|
+
child.unref();
|
|
417
|
+
} catch (err) {
|
|
418
|
+
warn(
|
|
419
|
+
`Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`
|
|
420
|
+
);
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;
|
|
424
|
+
const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;
|
|
425
|
+
const deadline = Date.now() + timeoutMs;
|
|
426
|
+
while (Date.now() < deadline) {
|
|
427
|
+
const claimed = await detectActiveInstance(lockFile);
|
|
428
|
+
if (claimed !== null) {
|
|
429
|
+
info(`Web monitor running at ${chalk2.bold.cyan(reusedHandle(claimed).url)}`);
|
|
430
|
+
return reusedHandle(claimed);
|
|
431
|
+
}
|
|
432
|
+
await sleep(pollMs);
|
|
433
|
+
}
|
|
434
|
+
warn(
|
|
435
|
+
"Web monitor: the detached server did not start in time. Continuing without the web server."
|
|
436
|
+
);
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export {
|
|
441
|
+
getWebLockFile,
|
|
442
|
+
readWebLock,
|
|
443
|
+
removeWebLock,
|
|
444
|
+
ensureSingleWebServer,
|
|
445
|
+
ensureWebMonitor
|
|
446
|
+
};
|
|
447
|
+
//# sourceMappingURL=chunk-JOK7VXRV.js.map
|