headlesscode 1.0.2
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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* gate-cli.ts — tiny CLI wrapper around the deploy-gate decision logic
|
|
4
|
+
* (src/deploy/gate.ts). scripts/deploy-gate.sh calls this to decide approval,
|
|
5
|
+
* keeping the pure decision logic in TypeScript (unit-tested) and the bash
|
|
6
|
+
* wrapper thin (UI + path safety + invoking the deploy script).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node --import tsx src/deploy/gate-cli.ts \
|
|
10
|
+
* --repo <path> [--batch <name>] [--notes <file>] [--state <file>] \
|
|
11
|
+
* [--token-file <path>] [--approval-file <path>] \
|
|
12
|
+
* [--interactive] [--interactive-input <text>]
|
|
13
|
+
*
|
|
14
|
+
* Environment:
|
|
15
|
+
* DEPLOY_APPROVAL_TOKEN the token to match against the token file
|
|
16
|
+
* DEPLOY_APPROVAL_FILE fallback for --approval-file
|
|
17
|
+
*
|
|
18
|
+
* Output (parsed by the bash wrapper):
|
|
19
|
+
* DEPLOY_SUMMARY_BEGIN
|
|
20
|
+
* <multi-line deployment summary>
|
|
21
|
+
* DEPLOY_SUMMARY_END
|
|
22
|
+
* DEPLOY_APPROVED=yes|no
|
|
23
|
+
* DEPLOY_REASON=<reason>
|
|
24
|
+
*
|
|
25
|
+
* Exit code: 0 when approved, 3 when denied (the gate's "hard stop" code).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as path from "node:path"
|
|
29
|
+
|
|
30
|
+
import { buildDeploySummary, evaluateApproval } from "./gate.js"
|
|
31
|
+
|
|
32
|
+
interface CliArgs {
|
|
33
|
+
repo: string
|
|
34
|
+
batch?: string
|
|
35
|
+
notes?: string
|
|
36
|
+
state?: string
|
|
37
|
+
tokenFile?: string
|
|
38
|
+
approvalFile?: string
|
|
39
|
+
interactive: boolean
|
|
40
|
+
interactiveInput?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
44
|
+
const args: CliArgs = { repo: "", interactive: false }
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const arg = argv[i]
|
|
47
|
+
const next = (): string | undefined => {
|
|
48
|
+
const v = argv[i + 1]
|
|
49
|
+
if (v === undefined || v.startsWith("--")) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
i++
|
|
53
|
+
return v
|
|
54
|
+
}
|
|
55
|
+
switch (arg) {
|
|
56
|
+
case "--repo":
|
|
57
|
+
case "--batch":
|
|
58
|
+
case "--notes":
|
|
59
|
+
case "--state":
|
|
60
|
+
case "--token-file":
|
|
61
|
+
case "--approval-file":
|
|
62
|
+
case "--interactive-input": {
|
|
63
|
+
const v = next()
|
|
64
|
+
if (v === undefined) {
|
|
65
|
+
throw new Error(`Missing value for ${arg}`)
|
|
66
|
+
}
|
|
67
|
+
switch (arg) {
|
|
68
|
+
case "--repo":
|
|
69
|
+
args.repo = v
|
|
70
|
+
break
|
|
71
|
+
case "--batch":
|
|
72
|
+
args.batch = v
|
|
73
|
+
break
|
|
74
|
+
case "--notes":
|
|
75
|
+
args.notes = v
|
|
76
|
+
break
|
|
77
|
+
case "--state":
|
|
78
|
+
args.state = v
|
|
79
|
+
break
|
|
80
|
+
case "--token-file":
|
|
81
|
+
args.tokenFile = v
|
|
82
|
+
break
|
|
83
|
+
case "--approval-file":
|
|
84
|
+
args.approvalFile = v
|
|
85
|
+
break
|
|
86
|
+
case "--interactive-input":
|
|
87
|
+
args.interactiveInput = v
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
case "--interactive":
|
|
93
|
+
args.interactive = true
|
|
94
|
+
break
|
|
95
|
+
case "--help":
|
|
96
|
+
case "-h":
|
|
97
|
+
console.error(
|
|
98
|
+
"gate-cli.ts --repo <path> [--batch <name>] [--notes <file>] [--state <file>] [--token-file <path>] [--approval-file <path>] [--interactive] [--interactive-input <text>]",
|
|
99
|
+
)
|
|
100
|
+
process.exit(0)
|
|
101
|
+
break
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return args
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function main(): void {
|
|
110
|
+
const args = parseArgs(process.argv.slice(2))
|
|
111
|
+
if (!args.repo) {
|
|
112
|
+
throw new Error("--repo <path> is required")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Default artifact paths inside the target repo (mirror scripts/deploy-gate.sh).
|
|
116
|
+
const tokenFile = args.tokenFile ?? path.join(args.repo, ".deploy-approval")
|
|
117
|
+
const batch = args.batch ?? "round"
|
|
118
|
+
const approvalFile =
|
|
119
|
+
args.approvalFile ??
|
|
120
|
+
process.env.DEPLOY_APPROVAL_FILE ??
|
|
121
|
+
path.join(args.repo, ".worktrees", `.deploy-approved-${batch}`)
|
|
122
|
+
const statePath = args.state ?? path.join(args.repo, ".worktrees", ".orchestrator-state.json")
|
|
123
|
+
|
|
124
|
+
// 1. Deployment summary (what's being deployed).
|
|
125
|
+
const summary = buildDeploySummary({
|
|
126
|
+
repo: args.repo,
|
|
127
|
+
batch,
|
|
128
|
+
notesPath: args.notes,
|
|
129
|
+
statePath,
|
|
130
|
+
})
|
|
131
|
+
process.stdout.write(`DEPLOY_SUMMARY_BEGIN\n${summary}\nDEPLOY_SUMMARY_END\n`)
|
|
132
|
+
|
|
133
|
+
// 2. Approval decision (pure logic; file reads happen in evaluateApproval).
|
|
134
|
+
const result = evaluateApproval({
|
|
135
|
+
interactive: args.interactive,
|
|
136
|
+
interactiveInput: args.interactiveInput,
|
|
137
|
+
approvalToken: process.env.DEPLOY_APPROVAL_TOKEN,
|
|
138
|
+
tokenFile,
|
|
139
|
+
approvalFile,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
process.stdout.write(`DEPLOY_APPROVED=${result.approved ? "yes" : "no"}\n`)
|
|
143
|
+
process.stdout.write(`DEPLOY_REASON=${result.reason.replace(/\n/g, " ")}\n`)
|
|
144
|
+
process.exitCode = result.approved ? 0 : 3
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main()
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-approval deploy gate — pure decision logic (Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* The gate exists because deploy-production.sh pushes to production (GitHub
|
|
5
|
+
* Actions triggers / direct SSH deploys to Hetzner). That must never happen
|
|
6
|
+
* without an explicit human decision — NOT an automated "approved because
|
|
7
|
+
* everything passed" rule. The orchestrator may only run the repo's deploy
|
|
8
|
+
* script AFTER this gate returns approved.
|
|
9
|
+
*
|
|
10
|
+
* Two approval modes (documented in docs/phase4-deploy-gate.md):
|
|
11
|
+
*
|
|
12
|
+
* - INTERACTIVE: a human answers "Approve deploy? [y/N]" on a TTY. Any
|
|
13
|
+
* explicit yes (y/yes, case-insensitive) approves; everything else
|
|
14
|
+
* (including empty input) denies.
|
|
15
|
+
* - NON-INTERACTIVE (token/file): exactly ONE of:
|
|
16
|
+
* (a) a one-time approval FILE created by a human, e.g.
|
|
17
|
+
* `touch <repo>/.worktrees/.deploy-approved-<batch>` — its mere
|
|
18
|
+
* presence is the human's explicit action (it does not exist until
|
|
19
|
+
* a human creates it);
|
|
20
|
+
* (b) an env token `DEPLOY_APPROVAL_TOKEN` that strictly matches the
|
|
21
|
+
* token stored in a file like `<repo>/.deploy-approval`.
|
|
22
|
+
* If neither is present, or the token does not match, approval is DENIED.
|
|
23
|
+
*
|
|
24
|
+
* Never auto-approve: with no interactive input, no approval file, and no
|
|
25
|
+
* matching token, the answer is always denied.
|
|
26
|
+
*
|
|
27
|
+
* The decision itself lives in `decideApproval` (pure, unit-tested). The
|
|
28
|
+
* I/O wrapper `evaluateApproval` reads the token/approval files and delegates
|
|
29
|
+
* to it — this is what the bash wrapper (scripts/deploy-gate.sh) calls via
|
|
30
|
+
* the tiny CLI (src/deploy/gate-cli.ts), keeping the logic testable without
|
|
31
|
+
* a network or git.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as fs from "node:fs"
|
|
35
|
+
import * as path from "node:path"
|
|
36
|
+
|
|
37
|
+
/** Everything `decideApproval` needs — resolved values, no file I/O. */
|
|
38
|
+
export interface ApprovalInputs {
|
|
39
|
+
/** True when the caller wants interactive (TTY) approval. */
|
|
40
|
+
interactive: boolean
|
|
41
|
+
/** The line a human typed at the prompt (undefined when not interactive). */
|
|
42
|
+
interactiveInput?: string
|
|
43
|
+
/** Value of the DEPLOY_APPROVAL_TOKEN env var (undefined when unset). */
|
|
44
|
+
approvalToken?: string
|
|
45
|
+
/** Contents of the token file (e.g. <repo>/.deploy-approval), trimmed. */
|
|
46
|
+
tokenFileContent?: string
|
|
47
|
+
/** True when a human-created one-time approval file exists. */
|
|
48
|
+
approvalFileExists: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ApprovalResult {
|
|
52
|
+
approved: boolean
|
|
53
|
+
/** Human-readable reason, safe to print to the operator. */
|
|
54
|
+
reason: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Interactive input counts as approval only for an explicit yes. */
|
|
58
|
+
function isExplicitYes(input: string): boolean {
|
|
59
|
+
return /^\s*y(?:es)?\s*$/i.test(input)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* PURE decision function — unit-tested without any file system.
|
|
64
|
+
*
|
|
65
|
+
* Rules (fail-closed):
|
|
66
|
+
* 1. Interactive + explicit yes → approved.
|
|
67
|
+
* 2. Interactive + anything else → denied (empty, "n", "maybe", …).
|
|
68
|
+
* 3. Non-interactive + one-time approval FILE present → approved.
|
|
69
|
+
* 4. Non-interactive + approvalToken AND tokenFileContent set AND they
|
|
70
|
+
* match exactly (trimmed) → approved.
|
|
71
|
+
* 5. Everything else → denied with a reason explaining what was missing.
|
|
72
|
+
*
|
|
73
|
+
* Interactive mode takes precedence when both interactive and a token are
|
|
74
|
+
* configured (the operator is at the keyboard — their answer is what counts).
|
|
75
|
+
*/
|
|
76
|
+
export function decideApproval(inputs: ApprovalInputs): ApprovalResult {
|
|
77
|
+
if (inputs.interactive) {
|
|
78
|
+
if (inputs.interactiveInput !== undefined && isExplicitYes(inputs.interactiveInput)) {
|
|
79
|
+
return { approved: true, reason: "approved interactively (explicit yes at the prompt)" }
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
approved: false,
|
|
83
|
+
reason:
|
|
84
|
+
inputs.interactiveInput === undefined
|
|
85
|
+
? "DENIED: interactive approval requires an explicit yes ('y'/'yes'); no answer was received"
|
|
86
|
+
: `DENIED: interactive approval requires an explicit yes ('y'/'yes'); got '${inputs.interactiveInput.trim() || "(empty)"}'`,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (inputs.approvalFileExists) {
|
|
91
|
+
return { approved: true, reason: "approved by a human-created one-time approval file" }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (inputs.approvalToken !== undefined && inputs.tokenFileContent !== undefined) {
|
|
95
|
+
if (inputs.approvalToken.trim() !== "" && inputs.approvalToken.trim() === inputs.tokenFileContent.trim()) {
|
|
96
|
+
return { approved: true, reason: "approved: DEPLOY_APPROVAL_TOKEN matched the token file" }
|
|
97
|
+
}
|
|
98
|
+
if (inputs.approvalToken.trim() === "") {
|
|
99
|
+
return {
|
|
100
|
+
approved: false,
|
|
101
|
+
reason: "DENIED: DEPLOY_APPROVAL_TOKEN is set but empty — no token to match",
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
approved: false,
|
|
106
|
+
reason: "DENIED: DEPLOY_APPROVAL_TOKEN does not match the token in the approval file",
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (inputs.approvalToken !== undefined && inputs.tokenFileContent === undefined) {
|
|
111
|
+
return {
|
|
112
|
+
approved: false,
|
|
113
|
+
reason: "DENIED: DEPLOY_APPROVAL_TOKEN is set but no token file was found to match against",
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (inputs.approvalToken === undefined && inputs.tokenFileContent !== undefined) {
|
|
117
|
+
return {
|
|
118
|
+
approved: false,
|
|
119
|
+
reason: "DENIED: a token file exists but DEPLOY_APPROVAL_TOKEN is not set",
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
approved: false,
|
|
124
|
+
reason:
|
|
125
|
+
"DENIED: no interactive input, no one-time approval file, and no matching DEPLOY_APPROVAL_TOKEN — a deploy gate can never auto-approve",
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface EvaluateApprovalOptions {
|
|
130
|
+
interactive: boolean
|
|
131
|
+
interactiveInput?: string
|
|
132
|
+
approvalToken?: string
|
|
133
|
+
/** Path to the token file (e.g. <repo>/.deploy-approval). */
|
|
134
|
+
tokenFile?: string
|
|
135
|
+
/** Path to the one-time approval file (e.g. <repo>/.worktrees/.deploy-approved-<batch>). */
|
|
136
|
+
approvalFile?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Read token/approval files (if any) and delegate to the pure decision. */
|
|
140
|
+
export function evaluateApproval(options: EvaluateApprovalOptions): ApprovalResult {
|
|
141
|
+
let tokenFileContent: string | undefined
|
|
142
|
+
if (options.tokenFile) {
|
|
143
|
+
try {
|
|
144
|
+
tokenFileContent = fs.readFileSync(options.tokenFile, "utf-8").trim()
|
|
145
|
+
} catch {
|
|
146
|
+
tokenFileContent = undefined // missing file = no token to match
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let approvalFileExists = false
|
|
151
|
+
if (options.approvalFile) {
|
|
152
|
+
try {
|
|
153
|
+
approvalFileExists = fs.statSync(options.approvalFile).isFile()
|
|
154
|
+
} catch {
|
|
155
|
+
approvalFileExists = false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return decideApproval({
|
|
160
|
+
interactive: options.interactive,
|
|
161
|
+
interactiveInput: options.interactiveInput,
|
|
162
|
+
approvalToken: options.approvalToken,
|
|
163
|
+
tokenFileContent,
|
|
164
|
+
approvalFileExists,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Deployment summary (what would be deployed) ────────────────────────────
|
|
169
|
+
|
|
170
|
+
export interface DeploySummaryOptions {
|
|
171
|
+
/** Target repo root (shown in the summary header). */
|
|
172
|
+
repo: string
|
|
173
|
+
/** Batch id (default: derived from the state file or "unnamed"). */
|
|
174
|
+
batch?: string
|
|
175
|
+
/** Optional deployment notes file; shown verbatim when present. */
|
|
176
|
+
notesPath?: string
|
|
177
|
+
/** Orchestrator state file (`.worktrees/.orchestrator-state.json`). */
|
|
178
|
+
statePath?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build the human-readable deployment summary printed by the gate BEFORE
|
|
183
|
+
* asking for approval. Sources, in priority order:
|
|
184
|
+
* 1. A provided/generated notes file (verbatim).
|
|
185
|
+
* 2. The orchestrator state file's batch + per-group summary.
|
|
186
|
+
* 3. A generic line (nothing to summarize).
|
|
187
|
+
*/
|
|
188
|
+
export function buildDeploySummary(options: DeploySummaryOptions): string {
|
|
189
|
+
const lines: string[] = []
|
|
190
|
+
lines.push(`Target repo: ${options.repo}`)
|
|
191
|
+
|
|
192
|
+
if (options.notesPath) {
|
|
193
|
+
try {
|
|
194
|
+
const notes = fs.readFileSync(options.notesPath, "utf-8").trim()
|
|
195
|
+
if (notes) {
|
|
196
|
+
lines.push("")
|
|
197
|
+
lines.push(notes)
|
|
198
|
+
return lines.join("\n")
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
lines.push(`(notes file not readable: ${options.notesPath})`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (options.statePath) {
|
|
206
|
+
try {
|
|
207
|
+
const raw = fs.readFileSync(options.statePath, "utf-8")
|
|
208
|
+
const state = JSON.parse(raw) as {
|
|
209
|
+
batch?: string
|
|
210
|
+
groups?: Array<{
|
|
211
|
+
name?: string
|
|
212
|
+
status?: string
|
|
213
|
+
issues?: number[]
|
|
214
|
+
review_verdict?: string
|
|
215
|
+
qa?: { verdict?: string }
|
|
216
|
+
}>
|
|
217
|
+
}
|
|
218
|
+
const batch = options.batch ?? state.batch ?? "unnamed"
|
|
219
|
+
lines.push(`Batch: ${batch}`)
|
|
220
|
+
const groups = Array.isArray(state.groups) ? state.groups : []
|
|
221
|
+
if (groups.length === 0) {
|
|
222
|
+
lines.push("(no groups recorded in the orchestrator state file)")
|
|
223
|
+
} else {
|
|
224
|
+
lines.push(`Groups (${groups.length}):`)
|
|
225
|
+
for (const g of groups) {
|
|
226
|
+
const issues = Array.isArray(g.issues) && g.issues.length > 0 ? ` issues ${g.issues.join(",")}` : ""
|
|
227
|
+
const review = g.review_verdict ? ` review=${g.review_verdict}` : ""
|
|
228
|
+
const qa = g.qa?.verdict ? ` qa=${g.qa.verdict}` : ""
|
|
229
|
+
lines.push(` - ${g.name ?? "(unnamed)"}: status=${g.status ?? "?"}${issues}${review}${qa}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return lines.join("\n")
|
|
233
|
+
} catch {
|
|
234
|
+
// fall through to generic
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
lines.push(options.batch ? `Batch: ${options.batch}` : "Batch: (not provided)")
|
|
239
|
+
lines.push("(no deployment notes and no orchestrator state file available — nothing to summarize)")
|
|
240
|
+
return lines.join("\n")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Default paths for the gate artifacts inside a target repo.
|
|
245
|
+
* - token file: <repo>/.deploy-approval
|
|
246
|
+
* - approval: <repo>/.worktrees/.deploy-approved-<batch>
|
|
247
|
+
*/
|
|
248
|
+
export function defaultTokenFile(repo: string): string {
|
|
249
|
+
return path.join(repo, ".deploy-approval")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function defaultApprovalFile(repo: string, batch: string): string {
|
|
253
|
+
return path.join(repo, ".worktrees", `.deploy-approved-${batch}`)
|
|
254
|
+
}
|