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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub App CLI subcommands — `headlesscode provision` and
|
|
3
|
+
* `headlesscode push-pr`.
|
|
4
|
+
*
|
|
5
|
+
* npx tsx src/cli.ts provision --installation-id <id> --owner <o> --repo <r> --target <dir>
|
|
6
|
+
* npx tsx src/cli.ts push-pr --installation-id <id> --owner <o> --repo <r> \
|
|
7
|
+
* --local-dir <path> --branch <name> --title <title> --body <text> [--base <branch>]
|
|
8
|
+
*
|
|
9
|
+
* `provision` is the read half: gets a fresh installation access token (from
|
|
10
|
+
* GITHUB_APP_ID + the App private key), clones the repo into --target, strips
|
|
11
|
+
* the token from the remote URL, and prints the resulting local path — ready
|
|
12
|
+
* to be used as a `--workspace` value. Also `--list-repos <installation-id>`.
|
|
13
|
+
*
|
|
14
|
+
* `push-pr` is the write half: pushes a local branch to the repo with the
|
|
15
|
+
* same installation token (token scrubbed from .git/config immediately), then
|
|
16
|
+
* opens a PR from it — never to the repo's default branch, never auto-merged.
|
|
17
|
+
*
|
|
18
|
+
* Thin dispatch to src/github/ — no logic duplicated here. Exit codes match
|
|
19
|
+
* the project convention: 0 success, 1 runtime failure, 2 usage/config error.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "node:fs"
|
|
23
|
+
import * as path from "node:path"
|
|
24
|
+
|
|
25
|
+
import { createAppAuthClient } from "./app-auth.js"
|
|
26
|
+
import { listInstallationRepos } from "./installations.js"
|
|
27
|
+
import { getRepoDefaultBranch, openPullRequest } from "./pr.js"
|
|
28
|
+
import { provisionRepo } from "./provision.js"
|
|
29
|
+
import { pushBranch } from "./push.js"
|
|
30
|
+
|
|
31
|
+
const PROVISION_USAGE = `headlesscode provision — GitHub App repo provisioning
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
headlesscode provision --installation-id <id> --owner <o> --repo <r> --target <dir>
|
|
35
|
+
Clone <owner>/<repo> into <dir> using a GitHub App installation access
|
|
36
|
+
token, strip the token from the clone's remote URL, and print the
|
|
37
|
+
resulting local path (usable as a --workspace value).
|
|
38
|
+
headlesscode provision --list-repos <installation-id>
|
|
39
|
+
List the repositories this installation can access
|
|
40
|
+
({owner, name, fullName, defaultBranch} lines) — the primitive a
|
|
41
|
+
future "choose a project" UI would call.
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--installation-id <id> GitHub App installation ID (required for clone; also
|
|
45
|
+
the argument to --list-repos)
|
|
46
|
+
--owner <o> Repo owner (required for clone)
|
|
47
|
+
--repo <r> Repo name (required for clone)
|
|
48
|
+
--target <dir> Local directory to clone into (created if missing;
|
|
49
|
+
must be empty if it exists)
|
|
50
|
+
--list-repos <id> List repos accessible to installation <id> instead of cloning
|
|
51
|
+
--help Show this help and exit
|
|
52
|
+
|
|
53
|
+
Environment:
|
|
54
|
+
GITHUB_APP_ID GitHub App numeric ID (required; see docs/github-app-setup.md)
|
|
55
|
+
GITHUB_APP_PRIVATE_KEY The App's private key PEM text (required; never logged)
|
|
56
|
+
GITHUB_APP_PRIVATE_KEY_PATH Alternative: path to a .pem private key file
|
|
57
|
+
GITHUB_API_BASE_URL API base URL override (tests/mocks)
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
interface ProvisionCliOptions {
|
|
61
|
+
installationId?: string
|
|
62
|
+
owner?: string
|
|
63
|
+
repo?: string
|
|
64
|
+
target?: string
|
|
65
|
+
listRepos?: string
|
|
66
|
+
help: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseProvisionArgs(argv: string[]): { options: ProvisionCliOptions; error?: string } {
|
|
70
|
+
const options: ProvisionCliOptions = { help: false }
|
|
71
|
+
for (let i = 0; i < argv.length; i++) {
|
|
72
|
+
const arg = argv[i]
|
|
73
|
+
const eq = arg.indexOf("=")
|
|
74
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq)
|
|
75
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
|
|
76
|
+
const next = (): string | undefined => {
|
|
77
|
+
if (inlineValue !== undefined) {
|
|
78
|
+
return inlineValue
|
|
79
|
+
}
|
|
80
|
+
const v = argv[i + 1]
|
|
81
|
+
if (v === undefined || v.startsWith("--")) {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
i++
|
|
85
|
+
return v
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
switch (flag) {
|
|
89
|
+
case "--installation-id":
|
|
90
|
+
case "--owner":
|
|
91
|
+
case "--repo":
|
|
92
|
+
case "--target":
|
|
93
|
+
case "--list-repos": {
|
|
94
|
+
const value = next()
|
|
95
|
+
if (value === undefined) {
|
|
96
|
+
return { options, error: `Missing value for ${flag}` }
|
|
97
|
+
}
|
|
98
|
+
if (flag === "--installation-id") {
|
|
99
|
+
options.installationId = value
|
|
100
|
+
} else if (flag === "--owner") {
|
|
101
|
+
options.owner = value
|
|
102
|
+
} else if (flag === "--repo") {
|
|
103
|
+
options.repo = value
|
|
104
|
+
} else if (flag === "--target") {
|
|
105
|
+
options.target = value
|
|
106
|
+
} else {
|
|
107
|
+
options.listRepos = value
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
case "--help":
|
|
112
|
+
case "-h":
|
|
113
|
+
options.help = true
|
|
114
|
+
break
|
|
115
|
+
default:
|
|
116
|
+
return { options, error: `Unknown provision argument: ${arg}` }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { options }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Load App credentials from env ($GITHUB_APP_ID + private key or key path). */
|
|
123
|
+
function appCredentialsFromEnv(): { appId: string; privateKey: string } {
|
|
124
|
+
const appId = process.env.GITHUB_APP_ID
|
|
125
|
+
if (!appId) {
|
|
126
|
+
throw new Error("GITHUB_APP_ID is not set (see docs/github-app-setup.md)")
|
|
127
|
+
}
|
|
128
|
+
const inline = process.env.GITHUB_APP_PRIVATE_KEY
|
|
129
|
+
if (inline) {
|
|
130
|
+
return { appId, privateKey: inline }
|
|
131
|
+
}
|
|
132
|
+
const keyPath = process.env.GITHUB_APP_PRIVATE_KEY_PATH
|
|
133
|
+
if (!keyPath) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"no App private key: set GITHUB_APP_PRIVATE_KEY (PEM text) or GITHUB_APP_PRIVATE_KEY_PATH (.pem file path)",
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return { appId, privateKey: fs.readFileSync(path.resolve(keyPath), "utf-8") }
|
|
140
|
+
} catch (err) {
|
|
141
|
+
throw new Error(`cannot read GITHUB_APP_PRIVATE_KEY_PATH '${keyPath}': ${err instanceof Error ? err.message : String(err)}`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Shared client builder for both sub-modes (keeps env + DI in one place). */
|
|
146
|
+
export function buildProvisionClient(): ReturnType<typeof createAppAuthClient> {
|
|
147
|
+
const { appId, privateKey } = appCredentialsFromEnv()
|
|
148
|
+
return createAppAuthClient({
|
|
149
|
+
appId,
|
|
150
|
+
privateKey,
|
|
151
|
+
...(process.env.GITHUB_API_BASE_URL ? { baseUrl: process.env.GITHUB_API_BASE_URL } : {}),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function provisionMain(argv: string[]): Promise<number> {
|
|
156
|
+
const { options, error } = parseProvisionArgs(argv)
|
|
157
|
+
if (error) {
|
|
158
|
+
process.stderr.write(`headlesscode provision: ${error}\n\n${PROVISION_USAGE}`)
|
|
159
|
+
return 2
|
|
160
|
+
}
|
|
161
|
+
if (options.help) {
|
|
162
|
+
process.stdout.write(PROVISION_USAGE)
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
165
|
+
if (options.listRepos) {
|
|
166
|
+
let client
|
|
167
|
+
try {
|
|
168
|
+
client = buildProvisionClient()
|
|
169
|
+
} catch (err) {
|
|
170
|
+
process.stderr.write(`headlesscode provision: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
171
|
+
return 2
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const repos = await listInstallationRepos(options.listRepos, {
|
|
175
|
+
getToken: () => client.getInstallationToken(options.listRepos!),
|
|
176
|
+
...(process.env.GITHUB_API_BASE_URL ? { baseUrl: process.env.GITHUB_API_BASE_URL } : {}),
|
|
177
|
+
})
|
|
178
|
+
for (const repo of repos) {
|
|
179
|
+
process.stdout.write(`${repo.fullName}\t${repo.owner}\t${repo.name}\t${repo.defaultBranch}\n`)
|
|
180
|
+
}
|
|
181
|
+
process.stdout.write(`[provision] ${repos.length} repo(s) accessible to installation ${options.listRepos}\n`)
|
|
182
|
+
return 0
|
|
183
|
+
} catch (err) {
|
|
184
|
+
process.stderr.write(`headlesscode provision: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
185
|
+
return 1
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!options.installationId || !options.owner || !options.repo || !options.target) {
|
|
190
|
+
process.stderr.write(
|
|
191
|
+
`headlesscode provision: --installation-id, --owner, --repo and --target are required\n\n${PROVISION_USAGE}`,
|
|
192
|
+
)
|
|
193
|
+
return 2
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let client
|
|
197
|
+
try {
|
|
198
|
+
client = buildProvisionClient()
|
|
199
|
+
} catch (err) {
|
|
200
|
+
process.stderr.write(`headlesscode provision: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
201
|
+
return 2
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const localPath = await provisionRepo({
|
|
206
|
+
installationId: options.installationId,
|
|
207
|
+
owner: options.owner,
|
|
208
|
+
repo: options.repo,
|
|
209
|
+
targetDir: options.target,
|
|
210
|
+
getToken: () => client.getInstallationToken(options.installationId!),
|
|
211
|
+
})
|
|
212
|
+
process.stdout.write(`${localPath}\n`)
|
|
213
|
+
return 0
|
|
214
|
+
} catch (err) {
|
|
215
|
+
process.stderr.write(`headlesscode provision: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
216
|
+
return 1
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const PUSH_PR_USAGE = `headlesscode push-pr — push a branch + open a pull request
|
|
221
|
+
|
|
222
|
+
Usage:
|
|
223
|
+
headlesscode push-pr --installation-id <id> --owner <o> --repo <r> \\
|
|
224
|
+
--local-dir <path> --branch <name> --title <title> --body <text> [--base <branch>]
|
|
225
|
+
Push the local <branch> (the current HEAD of <local-dir>, which must
|
|
226
|
+
already have the work committed) to <owner>/<repo> using a GitHub App
|
|
227
|
+
installation access token, then open a pull request from <branch> into
|
|
228
|
+
--base (default: the repo's ACTUAL default branch, resolved from the
|
|
229
|
+
repo API — never hardcoded). Prints "#<n>: <url>".
|
|
230
|
+
The token is scrubbed from the origin URL immediately after the push
|
|
231
|
+
(it never persists in .git/config) and push-pr NEVER pushes to the
|
|
232
|
+
repo's default branch — opening the PR is the whole deliverable,
|
|
233
|
+
merging is a human's job.
|
|
234
|
+
|
|
235
|
+
Options:
|
|
236
|
+
--installation-id <id> GitHub App installation ID (required)
|
|
237
|
+
--owner <o> Repo owner (required)
|
|
238
|
+
--repo <r> Repo name (required)
|
|
239
|
+
--local-dir <path> Local repo to push from (required; work committed)
|
|
240
|
+
--branch <name> Branch to push + open the PR from. Must NOT be the
|
|
241
|
+
repo's default branch (required)
|
|
242
|
+
--title <text> PR title (required)
|
|
243
|
+
--body <text> PR body (required)
|
|
244
|
+
--base <branch> PR target branch (default: the repo's default branch)
|
|
245
|
+
--help Show this help and exit
|
|
246
|
+
|
|
247
|
+
Environment:
|
|
248
|
+
GITHUB_APP_ID GitHub App numeric ID (required; see docs/github-app-setup.md)
|
|
249
|
+
GITHUB_APP_PRIVATE_KEY The App's private key PEM text (required; never logged)
|
|
250
|
+
GITHUB_APP_PRIVATE_KEY_PATH Alternative: path to a .pem private key file
|
|
251
|
+
GITHUB_API_BASE_URL API base URL override (tests/mocks)
|
|
252
|
+
`
|
|
253
|
+
|
|
254
|
+
interface PushPrCliOptions {
|
|
255
|
+
installationId?: string
|
|
256
|
+
owner?: string
|
|
257
|
+
repo?: string
|
|
258
|
+
localDir?: string
|
|
259
|
+
branch?: string
|
|
260
|
+
title?: string
|
|
261
|
+
body?: string
|
|
262
|
+
base?: string
|
|
263
|
+
help: boolean
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function parsePushPrArgs(argv: string[]): { options: PushPrCliOptions; error?: string } {
|
|
267
|
+
const options: PushPrCliOptions = { help: false }
|
|
268
|
+
for (let i = 0; i < argv.length; i++) {
|
|
269
|
+
const arg = argv[i]
|
|
270
|
+
const eq = arg.indexOf("=")
|
|
271
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq)
|
|
272
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
|
|
273
|
+
const next = (): string | undefined => {
|
|
274
|
+
if (inlineValue !== undefined) {
|
|
275
|
+
return inlineValue
|
|
276
|
+
}
|
|
277
|
+
const v = argv[i + 1]
|
|
278
|
+
if (v === undefined || v.startsWith("--")) {
|
|
279
|
+
return undefined
|
|
280
|
+
}
|
|
281
|
+
i++
|
|
282
|
+
return v
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
switch (flag) {
|
|
286
|
+
case "--installation-id":
|
|
287
|
+
case "--owner":
|
|
288
|
+
case "--repo":
|
|
289
|
+
case "--local-dir":
|
|
290
|
+
case "--branch":
|
|
291
|
+
case "--title":
|
|
292
|
+
case "--body":
|
|
293
|
+
case "--base": {
|
|
294
|
+
const value = next()
|
|
295
|
+
if (value === undefined) {
|
|
296
|
+
return { options, error: `Missing value for ${flag}` }
|
|
297
|
+
}
|
|
298
|
+
switch (flag) {
|
|
299
|
+
case "--installation-id":
|
|
300
|
+
options.installationId = value
|
|
301
|
+
break
|
|
302
|
+
case "--owner":
|
|
303
|
+
options.owner = value
|
|
304
|
+
break
|
|
305
|
+
case "--repo":
|
|
306
|
+
options.repo = value
|
|
307
|
+
break
|
|
308
|
+
case "--local-dir":
|
|
309
|
+
options.localDir = value
|
|
310
|
+
break
|
|
311
|
+
case "--branch":
|
|
312
|
+
options.branch = value
|
|
313
|
+
break
|
|
314
|
+
case "--title":
|
|
315
|
+
options.title = value
|
|
316
|
+
break
|
|
317
|
+
case "--body":
|
|
318
|
+
options.body = value
|
|
319
|
+
break
|
|
320
|
+
case "--base":
|
|
321
|
+
options.base = value
|
|
322
|
+
break
|
|
323
|
+
}
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
case "--help":
|
|
327
|
+
case "-h":
|
|
328
|
+
options.help = true
|
|
329
|
+
break
|
|
330
|
+
default:
|
|
331
|
+
return { options, error: `Unknown push-pr argument: ${arg}` }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { options }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Injectable seams for pushPrMain (tests); defaults are the real modules. */
|
|
338
|
+
export interface PushPrDeps {
|
|
339
|
+
buildClient?: () => ReturnType<typeof createAppAuthClient>
|
|
340
|
+
pushBranch?: typeof pushBranch
|
|
341
|
+
openPullRequest?: typeof openPullRequest
|
|
342
|
+
getRepoDefaultBranch?: typeof getRepoDefaultBranch
|
|
343
|
+
stdout?: (text: string) => void
|
|
344
|
+
stderr?: (text: string) => void
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function pushPrMain(argv: string[], deps: PushPrDeps = {}): Promise<number> {
|
|
348
|
+
const buildClient = deps.buildClient ?? buildProvisionClient
|
|
349
|
+
const push = deps.pushBranch ?? pushBranch
|
|
350
|
+
const openPr = deps.openPullRequest ?? openPullRequest
|
|
351
|
+
const repoDefaultBranch = deps.getRepoDefaultBranch ?? getRepoDefaultBranch
|
|
352
|
+
const out = deps.stdout ?? ((text: string) => process.stdout.write(text))
|
|
353
|
+
const errOut = deps.stderr ?? ((text: string) => process.stderr.write(text))
|
|
354
|
+
|
|
355
|
+
const { options, error } = parsePushPrArgs(argv)
|
|
356
|
+
if (error) {
|
|
357
|
+
errOut(`headlesscode push-pr: ${error}\n\n${PUSH_PR_USAGE}`)
|
|
358
|
+
return 2
|
|
359
|
+
}
|
|
360
|
+
if (options.help) {
|
|
361
|
+
out(PUSH_PR_USAGE)
|
|
362
|
+
return 0
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
!options.installationId ||
|
|
366
|
+
!options.owner ||
|
|
367
|
+
!options.repo ||
|
|
368
|
+
!options.localDir ||
|
|
369
|
+
!options.branch ||
|
|
370
|
+
options.title === undefined ||
|
|
371
|
+
options.body === undefined
|
|
372
|
+
) {
|
|
373
|
+
errOut(
|
|
374
|
+
`headlesscode push-pr: --installation-id, --owner, --repo, --local-dir, --branch, ` +
|
|
375
|
+
`--title and --body are required\n\n${PUSH_PR_USAGE}`,
|
|
376
|
+
)
|
|
377
|
+
return 2
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let client
|
|
381
|
+
try {
|
|
382
|
+
client = buildClient()
|
|
383
|
+
} catch (err) {
|
|
384
|
+
errOut(`headlesscode push-pr: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
385
|
+
return 2
|
|
386
|
+
}
|
|
387
|
+
const getToken = () => client.getInstallationToken(options.installationId!)
|
|
388
|
+
|
|
389
|
+
// Resolve the PR target branch UP FRONT (the repo's real default branch
|
|
390
|
+
// unless --base is given) so the hard rule can be enforced BEFORE pushing:
|
|
391
|
+
// push-pr never pushes to the repo's default branch.
|
|
392
|
+
let base: string
|
|
393
|
+
try {
|
|
394
|
+
base =
|
|
395
|
+
options.base ??
|
|
396
|
+
(await repoDefaultBranch({
|
|
397
|
+
owner: options.owner!,
|
|
398
|
+
repo: options.repo!,
|
|
399
|
+
getToken,
|
|
400
|
+
...(process.env.GITHUB_API_BASE_URL ? { baseUrl: process.env.GITHUB_API_BASE_URL } : {}),
|
|
401
|
+
}))
|
|
402
|
+
} catch (err) {
|
|
403
|
+
errOut(`headlesscode push-pr: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
404
|
+
return 1
|
|
405
|
+
}
|
|
406
|
+
if (base === options.branch) {
|
|
407
|
+
errOut(
|
|
408
|
+
`headlesscode push-pr: refusing to push '${options.branch}' — it IS the repo's default branch. ` +
|
|
409
|
+
`push-pr never pushes to the default branch; use a feature branch name.\n`,
|
|
410
|
+
)
|
|
411
|
+
return 2
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
await push({
|
|
416
|
+
installationId: options.installationId,
|
|
417
|
+
owner: options.owner!,
|
|
418
|
+
repo: options.repo!,
|
|
419
|
+
localDir: options.localDir!,
|
|
420
|
+
branchName: options.branch!,
|
|
421
|
+
getToken,
|
|
422
|
+
})
|
|
423
|
+
} catch (err) {
|
|
424
|
+
errOut(`headlesscode push-pr: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
425
|
+
return 1
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let pr
|
|
429
|
+
try {
|
|
430
|
+
pr = await openPr({
|
|
431
|
+
installationId: options.installationId,
|
|
432
|
+
owner: options.owner!,
|
|
433
|
+
repo: options.repo!,
|
|
434
|
+
head: options.branch!,
|
|
435
|
+
base,
|
|
436
|
+
title: options.title!,
|
|
437
|
+
body: options.body!,
|
|
438
|
+
getToken,
|
|
439
|
+
...(process.env.GITHUB_API_BASE_URL ? { baseUrl: process.env.GITHUB_API_BASE_URL } : {}),
|
|
440
|
+
})
|
|
441
|
+
} catch (err) {
|
|
442
|
+
errOut(`headlesscode push-pr: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
443
|
+
return 1
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
out(`PR #${pr.number}: ${pr.url}\n`)
|
|
447
|
+
return 0
|
|
448
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List repositories accessible to a GitHub App installation.
|
|
3
|
+
*
|
|
4
|
+
* `GET /installation/repositories` — the endpoint a future "choose a project"
|
|
5
|
+
* UI would call to populate a repo picker. This task only needs the
|
|
6
|
+
* underlying function + a way to exercise it (the `provision` CLI prints the
|
|
7
|
+
* list with --list-repos; a future UI can call listInstallationRepos directly).
|
|
8
|
+
*
|
|
9
|
+
* The response shape is documented by GitHub
|
|
10
|
+
* (https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation):
|
|
11
|
+
* `{ total_count, repositories: [{ name, full_name, default_branch, ... }] }`.
|
|
12
|
+
* We normalize to a minimal shape — callers never see raw GitHub payloads.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A repository an installation can access (normalized, minimal). */
|
|
16
|
+
export interface InstallationRepo {
|
|
17
|
+
owner: string
|
|
18
|
+
name: string
|
|
19
|
+
fullName: string
|
|
20
|
+
defaultBranch: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_PAGE_SIZE = 100
|
|
24
|
+
|
|
25
|
+
interface ListInstallationReposOptions {
|
|
26
|
+
/** The function that supplies a fresh installation access token. */
|
|
27
|
+
getToken: () => Promise<string>
|
|
28
|
+
baseUrl?: string
|
|
29
|
+
fetchImpl?: typeof fetch
|
|
30
|
+
/** Hard cap on total repos fetched (bounds pagination runtime). */
|
|
31
|
+
maxTotal?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RawRepo {
|
|
35
|
+
name?: unknown
|
|
36
|
+
full_name?: unknown
|
|
37
|
+
default_branch?: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Normalize one raw repository payload; undefined for garbage. */
|
|
41
|
+
export function toInstallationRepo(raw: unknown): InstallationRepo | undefined {
|
|
42
|
+
if (raw === null || typeof raw !== "object") {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
const r = raw as RawRepo
|
|
46
|
+
if (typeof r.name !== "string" || r.name === "") {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
if (typeof r.full_name !== "string" || !r.full_name.includes("/")) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
const [owner, name] = r.full_name.split("/")
|
|
53
|
+
if (!owner || name !== r.name) {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
owner,
|
|
58
|
+
name: r.name,
|
|
59
|
+
fullName: r.full_name,
|
|
60
|
+
defaultBranch: typeof r.default_branch === "string" && r.default_branch !== "" ? r.default_branch : "main",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* List repositories accessible to the app installation, paginated
|
|
66
|
+
* (per_page=100 until a short page), capped at `maxTotal` to bound runtime.
|
|
67
|
+
* Requires the App's Metadata (read-only) permission — the only permission
|
|
68
|
+
* GitHub requires for this endpoint beyond the ones already granted.
|
|
69
|
+
*/
|
|
70
|
+
export async function listInstallationRepos(
|
|
71
|
+
installationId: number | string,
|
|
72
|
+
options: ListInstallationReposOptions,
|
|
73
|
+
): Promise<InstallationRepo[]> {
|
|
74
|
+
const { getToken, baseUrl, fetchImpl = fetch, maxTotal = 1000 } = options
|
|
75
|
+
const token = await getToken()
|
|
76
|
+
|
|
77
|
+
const apiBase = (baseUrl ?? "https://api.github.com").replace(/\/+$/, "")
|
|
78
|
+
const repos: InstallationRepo[] = []
|
|
79
|
+
let page = 1
|
|
80
|
+
|
|
81
|
+
while (repos.length < maxTotal) {
|
|
82
|
+
const url = `${apiBase}/installation/repositories?per_page=${DEFAULT_PAGE_SIZE}&page=${page}`
|
|
83
|
+
let response: Response
|
|
84
|
+
try {
|
|
85
|
+
response = await fetchImpl(url, {
|
|
86
|
+
method: "GET",
|
|
87
|
+
headers: {
|
|
88
|
+
Accept: "application/vnd.github+json",
|
|
89
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
90
|
+
Authorization: `Bearer ${token}`,
|
|
91
|
+
"User-Agent": "headlesscode-provision",
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`GitHub API network error (GET ${url}): ${err instanceof Error ? err.message : String(err)}`,
|
|
97
|
+
{ cause: err },
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
let message = `GitHub API HTTP ${response.status} for GET ${url}`
|
|
103
|
+
try {
|
|
104
|
+
const payload = (await response.json()) as { message?: unknown }
|
|
105
|
+
if (typeof payload?.message === "string" && payload.message !== "") {
|
|
106
|
+
message = `${payload.message} (HTTP ${response.status})`
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// body is not JSON — keep the generic message
|
|
110
|
+
}
|
|
111
|
+
throw new Error(message)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const raw = (await response.json()) as { repositories?: unknown }
|
|
115
|
+
const batch = Array.isArray(raw?.repositories) ? raw.repositories : []
|
|
116
|
+
for (const entry of batch) {
|
|
117
|
+
const repo = toInstallationRepo(entry)
|
|
118
|
+
if (repo) {
|
|
119
|
+
repos.push(repo)
|
|
120
|
+
if (repos.length >= maxTotal) {
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// A short page (or an empty one) means we've reached the end.
|
|
126
|
+
if (batch.length < DEFAULT_PAGE_SIZE) {
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
page++
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return repos
|
|
133
|
+
}
|