@codedology/code-forge 0.1.0
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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/code-forge.mjs +145 -0
- package/docs/README.md +37 -0
- package/docs/reference/blocks.json +288 -0
- package/docs/reference/config.md +162 -0
- package/examples/first-use/README.md +11 -0
- package/examples/first-use/clamp.acceptance.yml +8 -0
- package/examples/first-use/clamp.brief.md +16 -0
- package/examples/first-use/coder/src/math.mjs +31 -0
- package/examples/first-use/coder/test/clamp.test.mjs +26 -0
- package/examples/node-lib/.env.example +3 -0
- package/examples/node-lib/.gitattributes +3 -0
- package/examples/node-lib/.github/workflows/test.yml +11 -0
- package/examples/node-lib/CHANGELOG.md +13 -0
- package/examples/node-lib/README.md +17 -0
- package/examples/node-lib/gitignore +3 -0
- package/examples/node-lib/package.json +19 -0
- package/examples/node-lib/src/list.mjs +23 -0
- package/examples/node-lib/src/math.mjs +21 -0
- package/examples/node-lib/src/strings.mjs +34 -0
- package/examples/node-lib/test/list.test.mjs +12 -0
- package/examples/node-lib/test/math.test.mjs +13 -0
- package/examples/node-lib/test/package.test.mjs +18 -0
- package/examples/node-lib/test/strings.test.mjs +28 -0
- package/package.json +52 -0
- package/schema/code-forge.schema.json +331 -0
- package/schema/facts.schema.json +28 -0
- package/scripts/first-use.mjs +517 -0
- package/scripts/gen-config-doc.mjs +241 -0
- package/skill/SKILL.md +79 -0
- package/skill/references/adapters/claude-code.md +19 -0
- package/skill/references/adapters/solo.md +34 -0
- package/skill/references/adapters/subprocess.md +15 -0
- package/skill/references/code.md +64 -0
- package/skill/references/continuity.md +31 -0
- package/skill/references/decisions.md +50 -0
- package/skill/references/degraded.md +40 -0
- package/skill/references/harden.md +26 -0
- package/skill/references/ledger.md +23 -0
- package/skill/references/plan.md +43 -0
- package/skill/references/proof.md +34 -0
- package/skill/references/review.md +42 -0
- package/skill/references/rules-core.md +72 -0
- package/skill/references/security.md +29 -0
- package/skill/scripts/forge +40 -0
- package/skill/templates/block-record.md +19 -0
- package/skill/templates/facts-sheet.md +22 -0
- package/skill/templates/plan.md +53 -0
- package/src/cli/author.mjs +87 -0
- package/src/cli/block.mjs +262 -0
- package/src/cli/doctor.mjs +74 -0
- package/src/cli/facts.mjs +124 -0
- package/src/cli/gates.mjs +280 -0
- package/src/cli/help.mjs +48 -0
- package/src/cli/init.mjs +14 -0
- package/src/cli/jev.mjs +175 -0
- package/src/cli/keys.mjs +170 -0
- package/src/cli/ledger.mjs +160 -0
- package/src/cli/list.mjs +68 -0
- package/src/cli/models.mjs +131 -0
- package/src/cli/plan.mjs +70 -0
- package/src/cli/proof.mjs +316 -0
- package/src/cli/remove.mjs +76 -0
- package/src/cli/report.mjs +73 -0
- package/src/cli/resolve.mjs +79 -0
- package/src/cli/review-file.mjs +152 -0
- package/src/cli/run.mjs +159 -0
- package/src/cli/s2.mjs +93 -0
- package/src/cli/spawn.mjs +104 -0
- package/src/cli/upgrade.mjs +103 -0
- package/src/cli/validate.mjs +108 -0
- package/src/cli/version.mjs +18 -0
- package/src/cli/worker.mjs +76 -0
- package/src/config/defaults/anthropic.mjs +18 -0
- package/src/config/defaults/index.mjs +24 -0
- package/src/config/defaults/openai.mjs +16 -0
- package/src/config/defaults/xai.mjs +18 -0
- package/src/config/known-ids.mjs +171 -0
- package/src/config/load.mjs +135 -0
- package/src/config/migrate.mjs +46 -0
- package/src/config/refresh.mjs +276 -0
- package/src/config/schema-compile.mjs +211 -0
- package/src/config/schema-paths.mjs +84 -0
- package/src/config/secret-patterns.mjs +85 -0
- package/src/config/validate.mjs +694 -0
- package/src/decide/author-loop.mjs +90 -0
- package/src/decide/escalation.mjs +262 -0
- package/src/decide/fallback-rules.mjs +172 -0
- package/src/decide/jev-client.mjs +216 -0
- package/src/decide/questions.mjs +161 -0
- package/src/decide/thresholds.mjs +199 -0
- package/src/doctor/index.mjs +105 -0
- package/src/doctor/local.mjs +162 -0
- package/src/doctor/probes.mjs +358 -0
- package/src/doctor/rows.mjs +51 -0
- package/src/doctor/worker-probe.mjs +125 -0
- package/src/engines/alias-map.mjs +78 -0
- package/src/engines/builders/claude.mjs +224 -0
- package/src/engines/builders/codex.mjs +242 -0
- package/src/engines/builders/grok.mjs +132 -0
- package/src/engines/builders/index.mjs +35 -0
- package/src/engines/builders/validate-params.mjs +34 -0
- package/src/engines/codex-home.mjs +230 -0
- package/src/engines/isolation.mjs +84 -0
- package/src/engines/probe.mjs +116 -0
- package/src/engines/provider-cli.mjs +24 -0
- package/src/engines/render-rules.mjs +41 -0
- package/src/engines/sentinel.mjs +133 -0
- package/src/engines/usage-parse.mjs +149 -0
- package/src/gates/acceptance.mjs +95 -0
- package/src/gates/detect.mjs +187 -0
- package/src/gates/invariants.mjs +43 -0
- package/src/gates/run.mjs +72 -0
- package/src/gates/safe-edit.mjs +266 -0
- package/src/gates/scope.mjs +154 -0
- package/src/gates/secret-scan.mjs +79 -0
- package/src/gates/transcript-grep.mjs +27 -0
- package/src/install/agent-env.mjs +76 -0
- package/src/install/detect.mjs +121 -0
- package/src/install/harnesses.mjs +131 -0
- package/src/install/link.mjs +463 -0
- package/src/install/wizard/answers.mjs +389 -0
- package/src/install/wizard/flags.mjs +248 -0
- package/src/install/wizard/project-file.mjs +128 -0
- package/src/install/wizard/run.mjs +366 -0
- package/src/install/wizard/steps.mjs +201 -0
- package/src/keys/backends/constants.mjs +7 -0
- package/src/keys/backends/file.mjs +99 -0
- package/src/keys/backends/keychain.mjs +81 -0
- package/src/keys/backends/security-cli.mjs +105 -0
- package/src/keys/onepassword.mjs +46 -0
- package/src/keys/store.mjs +347 -0
- package/src/ledger/calibration.mjs +109 -0
- package/src/ledger/export.mjs +52 -0
- package/src/ledger/outcome.mjs +397 -0
- package/src/ledger/paths.mjs +30 -0
- package/src/ledger/prices.mjs +49 -0
- package/src/ledger/report.mjs +250 -0
- package/src/ledger/write.mjs +279 -0
- package/src/proof/export.mjs +358 -0
- package/src/proof/lock.mjs +138 -0
- package/src/proof/red-green.mjs +519 -0
- package/src/proof/red-parse.mjs +103 -0
- package/src/proof/tiers.mjs +62 -0
- package/src/review/budget.mjs +241 -0
- package/src/review/context.mjs +156 -0
- package/src/review/engine.mjs +264 -0
- package/src/review/finding.schema.json +47 -0
- package/src/review/fixloop.mjs +467 -0
- package/src/review/gate-check.mjs +249 -0
- package/src/review/lenses/A.md +22 -0
- package/src/review/lenses/B.md +23 -0
- package/src/review/lenses/full.md +23 -0
- package/src/review/lenses/judge.md +21 -0
- package/src/review/lenses/quick.md +21 -0
- package/src/review/lenses/recheck.md +23 -0
- package/src/review/packet.mjs +355 -0
- package/src/review/triage.mjs +149 -0
- package/src/review/validate-review.mjs +70 -0
- package/src/session/author.mjs +228 -0
- package/src/session/facts.mjs +855 -0
- package/src/session/plan-check.mjs +294 -0
- package/src/session/s2.mjs +164 -0
- package/src/session/spawn.mjs +610 -0
- package/src/state/block.mjs +289 -0
- package/src/state/cli-args.mjs +67 -0
- package/src/state/paths.mjs +92 -0
- package/src/state/registry.mjs +189 -0
- package/src/state/run.mjs +266 -0
- package/src/state/signer.mjs +132 -0
- package/src/util/exec.mjs +315 -0
- package/src/util/forbidden.mjs +787 -0
- package/src/util/git.mjs +381 -0
- package/src/util/log.mjs +100 -0
- package/src/util/reaper.mjs +300 -0
- package/src/util/redact.mjs +212 -0
- package/src/util/tmp.mjs +409 -0
- package/src/worker/engine.mjs +338 -0
- package/src/worker/loop.mjs +392 -0
- package/src/worker/queue.mjs +186 -0
- package/src/worker/review-state.mjs +145 -0
- package/src/worker/ticket.mjs +195 -0
package/src/util/git.mjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every git call the package makes goes through here. The split is deliberate and load-bearing
|
|
3
|
+
* for `test/util/git.test.mjs`: each helper has a pure `*Argv` builder (no process spawned) plus
|
|
4
|
+
* a thin async wrapper that runs it through `./exec.mjs`. `commit` and `push` are the ONLY
|
|
5
|
+
* writing verbs this module exposes (plan §10.2 acceptance 6) — everything else reads.
|
|
6
|
+
*
|
|
7
|
+
* `git rm` is treated as a write too (it deletes a tracked file, which includes deleting a test
|
|
8
|
+
* file) even though the package never calls it: it is listed only so `./forbidden.mjs` and any
|
|
9
|
+
* future helper here classify it the same way. Force-push, reset --hard, checkout --, restore,
|
|
10
|
+
* clean, stash, and branch -D/--delete --force are simply not modeled as helpers here, because
|
|
11
|
+
* the package has no legitimate reason to run them. This module does NOT run a forbidden-list
|
|
12
|
+
* check before `exec`: what keeps `commit`/`push` inside the §8.4 rules is this module's own
|
|
13
|
+
* argument validation — fixed argv shapes, a branch that must be a plain git branch name (no
|
|
14
|
+
* leading `-` or `+`, no `:` or other refspec syntax — see `assertValidBranchName`), and typed
|
|
15
|
+
* string checks on every value.
|
|
16
|
+
*
|
|
17
|
+
* `fetch` is classified READ, not WRITE, even though it touches the network and updates
|
|
18
|
+
* remote-tracking refs and the object database: the write/read split here is specifically about
|
|
19
|
+
* "can this alter the local branch history or working tree a coder's diff depends on", which is
|
|
20
|
+
* what acceptance clause (6) exists to guard, and `fetch` cannot — it never moves a local branch
|
|
21
|
+
* or touches the working tree. `test/util/git.test.mjs` asserts this classification explicitly
|
|
22
|
+
* rather than leaving it implicit.
|
|
23
|
+
*
|
|
24
|
+
* Every positional ref/path argument that lands in argv WITHOUT a preceding `--` end-of-options
|
|
25
|
+
* marker is validated to not start with `-` (`assertNoLeadingDash`) — a value the caller merely
|
|
26
|
+
* expected to be a git ref (e.g. `base` in `diffNameOnlyArgv`) is exactly how `git diff
|
|
27
|
+
* --upload-pack=evil` style option injection would otherwise reach git. `diffArgv`'s `path` is
|
|
28
|
+
* the one exception: it already sits after a literal `--` in the built argv, so git treats it as
|
|
29
|
+
* a pathspec no matter what it starts with.
|
|
30
|
+
*
|
|
31
|
+
* Every wrapper runs git with `process.env` minus the repository-context variables git itself
|
|
32
|
+
* lists in `git rev-parse --local-env-vars` (`GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`,
|
|
33
|
+
* `GIT_PREFIX`, `GIT_OBJECT_DIRECTORY`, …). Git hooks export them, so a code-forge call made from
|
|
34
|
+
* inside a hook would otherwise act on the hook's repository instead of `cwd` (plan V8).
|
|
35
|
+
* Everything else — `GIT_SSH_COMMAND`, `GIT_AUTHOR_*`, the user's global config — is kept.
|
|
36
|
+
* `fetch`/`push` additionally set `GIT_TERMINAL_PROMPT=0`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { exec } from './exec.mjs';
|
|
40
|
+
|
|
41
|
+
/** Git subcommands this module treats as read-only (see the `fetch` note above). */
|
|
42
|
+
export const READ_COMMANDS = Object.freeze([
|
|
43
|
+
'status',
|
|
44
|
+
'diff',
|
|
45
|
+
'show',
|
|
46
|
+
'rev-parse',
|
|
47
|
+
'merge-base',
|
|
48
|
+
'fetch',
|
|
49
|
+
'archive',
|
|
50
|
+
'log',
|
|
51
|
+
'ls-files',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Git subcommands this module treats as writes. `commit` and `push` are the only ones exposed as helpers. */
|
|
55
|
+
export const WRITE_COMMANDS = Object.freeze(['commit', 'push', 'rm']);
|
|
56
|
+
|
|
57
|
+
/** Network helpers (`fetch`, `push`) get a default timeout so a credential prompt can't hang forever. */
|
|
58
|
+
const DEFAULT_NETWORK_TIMEOUT_MS = 30000;
|
|
59
|
+
|
|
60
|
+
/** Output of `git rev-parse --local-env-vars` (git 2.5x): the variables that pick a repository. */
|
|
61
|
+
const LOCAL_REPO_ENV_VARS = Object.freeze([
|
|
62
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
63
|
+
'GIT_CONFIG',
|
|
64
|
+
'GIT_CONFIG_PARAMETERS',
|
|
65
|
+
'GIT_CONFIG_COUNT',
|
|
66
|
+
'GIT_OBJECT_DIRECTORY',
|
|
67
|
+
'GIT_DIR',
|
|
68
|
+
'GIT_WORK_TREE',
|
|
69
|
+
'GIT_IMPLICIT_WORK_TREE',
|
|
70
|
+
'GIT_GRAFT_FILE',
|
|
71
|
+
'GIT_INDEX_FILE',
|
|
72
|
+
'GIT_NO_REPLACE_OBJECTS',
|
|
73
|
+
'GIT_REPLACE_REF_BASE',
|
|
74
|
+
'GIT_PREFIX',
|
|
75
|
+
'GIT_SHALLOW_FILE',
|
|
76
|
+
'GIT_COMMON_DIR',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `process.env` without the repository-context variables, plus `extra`.
|
|
81
|
+
* @param {Record<string, string>} [extra]
|
|
82
|
+
* @returns {NodeJS.ProcessEnv}
|
|
83
|
+
*/
|
|
84
|
+
function gitEnv(extra = {}) {
|
|
85
|
+
/** @type {NodeJS.ProcessEnv} */
|
|
86
|
+
const env = { ...process.env };
|
|
87
|
+
for (const name of LOCAL_REPO_ENV_VARS) {
|
|
88
|
+
delete env[name];
|
|
89
|
+
}
|
|
90
|
+
return { ...env, ...extra };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} subcommand
|
|
95
|
+
* @param {string[]} args
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
function gitArgv(subcommand, args) {
|
|
99
|
+
return ['git', subcommand, ...args];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reject a positional argument that isn't sitting after a `--` marker and starts with `-` — git
|
|
104
|
+
* would otherwise parse it as an option instead of the ref/path/branch it's meant to be.
|
|
105
|
+
* @param {unknown} value
|
|
106
|
+
* @param {string} paramName
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
function assertNoLeadingDash(value, paramName) {
|
|
110
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
111
|
+
throw new TypeError(`git.mjs: ${paramName} must be a non-empty string`);
|
|
112
|
+
}
|
|
113
|
+
if (value.startsWith('-')) {
|
|
114
|
+
throw new TypeError(`git.mjs: ${paramName} must not start with "-" (got ${JSON.stringify(value)}) — option injection`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {unknown} value
|
|
121
|
+
* @param {string} paramName
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
function assertNonEmptyString(value, paramName) {
|
|
125
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
126
|
+
throw new TypeError(`git.mjs: ${paramName} must be a non-empty string`);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string[]} args
|
|
133
|
+
* @param {string} paramName
|
|
134
|
+
* @returns {string[]}
|
|
135
|
+
*/
|
|
136
|
+
function assertNoLeadingDashes(args, paramName) {
|
|
137
|
+
for (const arg of args) {
|
|
138
|
+
assertNoLeadingDash(arg, `an element of ${paramName}`);
|
|
139
|
+
}
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Read-only argv builders ─────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/** @returns {string[]} */
|
|
146
|
+
export function statusArgv() {
|
|
147
|
+
return gitArgv('status', ['--porcelain', '--untracked-files=all']);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @param {string} base @returns {string[]} */
|
|
151
|
+
export function diffNameOnlyArgv(base) {
|
|
152
|
+
return gitArgv('diff', ['--name-only', assertNoLeadingDash(base, 'base')]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `path` sits after a literal `--`, so it is always treated as a pathspec by git regardless of
|
|
157
|
+
* its first character — only `base` needs the leading-dash guard here.
|
|
158
|
+
* @param {string} base @param {string} path @returns {string[]}
|
|
159
|
+
*/
|
|
160
|
+
export function diffArgv(base, path) {
|
|
161
|
+
return gitArgv('diff', [assertNoLeadingDash(base, 'base'), '--', assertNonEmptyString(path, 'path')]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `git diff --no-index /dev/null <path>` — every line of a new/untracked file as a `+` hunk.
|
|
166
|
+
* Exits 1 when differences are found (the expected case), which is not a failure.
|
|
167
|
+
* @param {string} path @returns {string[]}
|
|
168
|
+
*/
|
|
169
|
+
export function diffNoIndexArgv(path) {
|
|
170
|
+
return gitArgv('diff', ['--no-index', '/dev/null', assertNoLeadingDash(path, 'path')]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @param {string} ref @param {string} path @returns {string[]} */
|
|
174
|
+
export function showArgv(ref, path) {
|
|
175
|
+
return gitArgv('show', [`${assertNoLeadingDash(ref, 'ref')}:${assertNonEmptyString(path, 'path')}`]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @param {string} ref @returns {string[]} */
|
|
179
|
+
export function revParseArgv(ref) {
|
|
180
|
+
return gitArgv('rev-parse', [assertNoLeadingDash(ref, 'ref')]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Exits 0 when `ancestor` is an ancestor of `descendant`, 1 when it is not — both normal answers.
|
|
185
|
+
* @param {string} ancestor @param {string} descendant @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
export function isAncestorArgv(ancestor, descendant) {
|
|
188
|
+
return gitArgv('merge-base', [
|
|
189
|
+
'--is-ancestor',
|
|
190
|
+
assertNoLeadingDash(ancestor, 'ancestor'),
|
|
191
|
+
assertNoLeadingDash(descendant, 'descendant'),
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @returns {string[]} */
|
|
196
|
+
export function fetchArgv() {
|
|
197
|
+
return gitArgv('fetch', []);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @param {string} ref @returns {string[]} */
|
|
201
|
+
export function archiveArgv(ref) {
|
|
202
|
+
return gitArgv('archive', [assertNoLeadingDash(ref, 'ref')]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `args` is a raw passthrough today (no block/block owns a caller yet); every element is
|
|
207
|
+
* guarded against a leading `-` for the same option-injection reason as the single-ref builders
|
|
208
|
+
* above, which also closes the `--output=<file>`/`-o`/`--exec=<cmd>` "read helper writes a file
|
|
209
|
+
* or runs a command" hole. A future caller that needs a real git-log flag extends this guard
|
|
210
|
+
* (or moves to typed options) deliberately, rather than this staying an open passthrough.
|
|
211
|
+
* @param {string[]} [args] @returns {string[]}
|
|
212
|
+
*/
|
|
213
|
+
export function logArgv(args = []) {
|
|
214
|
+
return gitArgv('log', assertNoLeadingDashes(args, 'args'));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @param {string[]} [args] @returns {string[]} */
|
|
218
|
+
export function lsFilesArgv(args = []) {
|
|
219
|
+
return gitArgv('ls-files', assertNoLeadingDashes(args, 'args'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── The two allowed writes ──────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/** @param {string} message @returns {string[]} */
|
|
225
|
+
export function commitArgv(message) {
|
|
226
|
+
return gitArgv('commit', ['-m', assertNonEmptyString(message, 'message')]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Why a branch value is not a plain git branch name, or null when it is. Mirrors
|
|
231
|
+
* `git check-ref-format --branch` (git-check-ref-format(1)), plus two refspec-specific refusals:
|
|
232
|
+
* `git push origin <branch>` reads the value as a REFSPEC, so a leading `+` is a force-push and a
|
|
233
|
+
* `:` deletes (`:main`) or pushes a different ref (`src:dst`).
|
|
234
|
+
* @param {string} branch
|
|
235
|
+
* @returns {string | null}
|
|
236
|
+
*/
|
|
237
|
+
function branchNameProblem(branch) {
|
|
238
|
+
if (branch.startsWith('-')) {
|
|
239
|
+
return 'must not start with "-" — option injection (--force/--mirror/--delete)';
|
|
240
|
+
}
|
|
241
|
+
if (branch.startsWith('+')) {
|
|
242
|
+
return 'must not start with "+" — a +refspec is a force-push';
|
|
243
|
+
}
|
|
244
|
+
if (branch.includes(':')) {
|
|
245
|
+
return 'must not contain ":" — refspec syntax (":main" deletes, "a:b" pushes another ref)';
|
|
246
|
+
}
|
|
247
|
+
// Space, DEL and ASCII control characters, plus git's forbidden ref characters.
|
|
248
|
+
if (/[\u0000- \u007f~^?*[\\]/.test(branch)) {
|
|
249
|
+
return 'must not contain whitespace, control characters or any of ~ ^ ? * [ \\';
|
|
250
|
+
}
|
|
251
|
+
if (branch.includes('..') || branch.includes('@{') || branch === '@') {
|
|
252
|
+
return 'must not contain "..", "@{" or be "@"';
|
|
253
|
+
}
|
|
254
|
+
if (branch.endsWith('/') || branch.endsWith('.') || branch.includes('//')) {
|
|
255
|
+
return 'must not end with "/" or ".", or contain "//"';
|
|
256
|
+
}
|
|
257
|
+
if (branch.split('/').some((part) => part.startsWith('.') || part.endsWith('.lock'))) {
|
|
258
|
+
return 'no path component may start with "." or end with ".lock"';
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {unknown} branch
|
|
265
|
+
* @returns {string}
|
|
266
|
+
*/
|
|
267
|
+
function assertValidBranchName(branch) {
|
|
268
|
+
if (typeof branch !== 'string' || branch.trim().length === 0) {
|
|
269
|
+
throw new TypeError('git.mjs: pushArgv branch must be a non-empty string');
|
|
270
|
+
}
|
|
271
|
+
const problem = branchNameProblem(branch);
|
|
272
|
+
if (problem) {
|
|
273
|
+
throw new TypeError(`git.mjs: pushArgv branch ${JSON.stringify(branch)} ${problem}`);
|
|
274
|
+
}
|
|
275
|
+
return branch;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {{branch?: string, setUpstream?: boolean}} [opts]
|
|
280
|
+
* @returns {string[]}
|
|
281
|
+
* @throws {TypeError} if `branch` is not a plain git branch name (option injection or refspec
|
|
282
|
+
* syntax — see `branchNameProblem`), or if `setUpstream` is true without a `branch`.
|
|
283
|
+
*/
|
|
284
|
+
export function pushArgv(opts = {}) {
|
|
285
|
+
const { branch, setUpstream = false } = opts;
|
|
286
|
+
if (setUpstream && !branch) {
|
|
287
|
+
throw new TypeError('git.mjs: pushArgv({ setUpstream: true }) requires a branch');
|
|
288
|
+
}
|
|
289
|
+
if (branch === undefined) {
|
|
290
|
+
return gitArgv('push', []);
|
|
291
|
+
}
|
|
292
|
+
assertValidBranchName(branch);
|
|
293
|
+
return setUpstream ? gitArgv('push', ['-u', 'origin', branch]) : gitArgv('push', ['origin', branch]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Async wrappers ──────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/** @param {string} cwd */
|
|
299
|
+
export function status(cwd) {
|
|
300
|
+
return exec(statusArgv(), { cwd, env: gitEnv() });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** @param {string} base @param {string} cwd */
|
|
304
|
+
export function diffNameOnly(base, cwd) {
|
|
305
|
+
return exec(diffNameOnlyArgv(base), { cwd, env: gitEnv() });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** @param {string} base @param {string} path @param {string} cwd */
|
|
309
|
+
export function diff(base, path, cwd) {
|
|
310
|
+
return exec(diffArgv(base, path), { cwd, env: gitEnv() });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** @param {string} path @param {string} cwd */
|
|
314
|
+
export function diffNoIndex(path, cwd) {
|
|
315
|
+
return exec(diffNoIndexArgv(path), { cwd, env: gitEnv(), okExitCodes: [0, 1] });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @param {string} ref @param {string} path @param {string} cwd */
|
|
319
|
+
export function show(ref, path, cwd) {
|
|
320
|
+
return exec(showArgv(ref, path), { cwd, env: gitEnv() });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {string} ref @param {string} cwd */
|
|
324
|
+
export function revParse(ref, cwd) {
|
|
325
|
+
return exec(revParseArgv(ref), { cwd, env: gitEnv() });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @param {string} ancestor @param {string} descendant @param {string} cwd
|
|
330
|
+
* @returns {Promise<boolean>}
|
|
331
|
+
* @throws {Error} if the underlying `git merge-base --is-ancestor` call errors for a reason
|
|
332
|
+
* other than "not an ancestor" (exit 1) — e.g. exit 128 on an unknown ref — so that case is
|
|
333
|
+
* never silently reported as `false`.
|
|
334
|
+
*/
|
|
335
|
+
export async function isAncestor(ancestor, descendant, cwd) {
|
|
336
|
+
const res = await exec(isAncestorArgv(ancestor, descendant), { cwd, env: gitEnv(), okExitCodes: [0, 1] });
|
|
337
|
+
if (res.result !== 'ok') {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`git.mjs: isAncestor(${ancestor}, ${descendant}) failed (code ${res.code}): ${res.stderr || res.error || 'unknown error'}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return res.code === 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {string} cwd
|
|
347
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
348
|
+
*/
|
|
349
|
+
export function fetch(cwd, opts = {}) {
|
|
350
|
+
const { timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS } = opts;
|
|
351
|
+
return exec(fetchArgv(), { cwd, timeoutMs, env: gitEnv({ GIT_TERMINAL_PROMPT: '0' }) });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** @param {string} ref @param {string} cwd */
|
|
355
|
+
export function archive(ref, cwd) {
|
|
356
|
+
return exec(archiveArgv(ref), { cwd, env: gitEnv() });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @param {string} cwd @param {string[]} [args] */
|
|
360
|
+
export function log(cwd, args) {
|
|
361
|
+
return exec(logArgv(args), { cwd, env: gitEnv() });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** @param {string} cwd @param {string[]} [args] */
|
|
365
|
+
export function lsFiles(cwd, args) {
|
|
366
|
+
return exec(lsFilesArgv(args), { cwd, env: gitEnv() });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @param {string} message @param {string} cwd */
|
|
370
|
+
export function commit(message, cwd) {
|
|
371
|
+
return exec(commitArgv(message), { cwd, env: gitEnv() });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @param {string} cwd
|
|
376
|
+
* @param {{branch?: string, setUpstream?: boolean, timeoutMs?: number}} [opts]
|
|
377
|
+
*/
|
|
378
|
+
export function push(cwd, opts = {}) {
|
|
379
|
+
const { timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS, ...pushOpts } = opts;
|
|
380
|
+
return exec(pushArgv(pushOpts), { cwd, timeoutMs, env: gitEnv({ GIT_TERMINAL_PROMPT: '0' }) });
|
|
381
|
+
}
|
package/src/util/log.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The leveled logger — the "log path" of `./redact.mjs`'s three guarded output paths (the other
|
|
3
|
+
* two are `redact.writeSafe` for raw stream writes and `redact.redactJSON` for structured dumps).
|
|
4
|
+
* Every line printed here is redacted first; nothing in this module bypasses that.
|
|
5
|
+
*
|
|
6
|
+
* Each argument is redacted according to its own shape, not by JSON-serializing everything and
|
|
7
|
+
* redacting the joined string as one blob:
|
|
8
|
+
* - a `string` goes through `redact()` directly.
|
|
9
|
+
* - an `Error` (including one nested as `.cause`) is formatted to `name: message` + stack +
|
|
10
|
+
* cause chain, each part redacted.
|
|
11
|
+
* - anything else goes through `redactJSON()` (structural — also redacts by key, once
|
|
12
|
+
* `redact.mjs` does; a plain post-hoc string redact of an already-serialized object cannot).
|
|
13
|
+
* That split exists because a value-blind, string-only redact of a value the caller is trying to
|
|
14
|
+
* see can otherwise vanish outright: `JSON.stringify(undefined)` is `undefined`, not `"undefined"`
|
|
15
|
+
* — joining it into a message silently drops the value instead of printing it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { redact, redactJSON } from './redact.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function safeJSONString(value) {
|
|
25
|
+
const serialized = JSON.stringify(value);
|
|
26
|
+
return typeof serialized === 'string' ? serialized : String(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Error} err
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function formatError(err) {
|
|
34
|
+
const parts = [err.stack ?? `${err.name}: ${err.message}`];
|
|
35
|
+
if (err.cause !== undefined) {
|
|
36
|
+
parts.push(`Caused by: ${err.cause instanceof Error ? formatError(err.cause) : safeJSONString(err.cause)}`);
|
|
37
|
+
}
|
|
38
|
+
return parts.join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {unknown} value
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function stringifyArg(value) {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
return redact(value);
|
|
48
|
+
}
|
|
49
|
+
if (value instanceof Error) {
|
|
50
|
+
return redact(formatError(value));
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return redactJSON(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return redact(safeJSONString(value));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {NodeJS.WritableStream} stream
|
|
61
|
+
* @param {string} level
|
|
62
|
+
* @param {unknown[]} args
|
|
63
|
+
*/
|
|
64
|
+
function write(stream, level, args) {
|
|
65
|
+
const message = args.map(stringifyArg).join(' ');
|
|
66
|
+
// Prefix EVERY line, not just the first: an unprefixed line inside a value (a real newline in
|
|
67
|
+
// a caller's string, or a multi-line stack trace) would otherwise look like a second, forged
|
|
68
|
+
// log entry — log-line injection.
|
|
69
|
+
const prefixed = message
|
|
70
|
+
.split('\n')
|
|
71
|
+
.map((line) => `[code-forge:${level}] ${line}`)
|
|
72
|
+
.join('\n');
|
|
73
|
+
stream.write(`${prefixed}\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {...unknown} args */
|
|
77
|
+
export function info(...args) {
|
|
78
|
+
write(process.stdout, 'info', args);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @param {...unknown} args */
|
|
82
|
+
export function warn(...args) {
|
|
83
|
+
write(process.stderr, 'warn', args);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {...unknown} args */
|
|
87
|
+
export function error(...args) {
|
|
88
|
+
write(process.stderr, 'error', args);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Values that turn CODE_FORGE_DEBUG on; anything else (including "0" or "false") leaves it off. */
|
|
92
|
+
const DEBUG_ENABLED_VALUES = new Set(['1', 'true', 'yes']);
|
|
93
|
+
|
|
94
|
+
/** @param {...unknown} args */
|
|
95
|
+
export function debug(...args) {
|
|
96
|
+
const flag = process.env.CODE_FORGE_DEBUG;
|
|
97
|
+
if (flag !== undefined && DEBUG_ENABLED_VALUES.has(flag.toLowerCase())) {
|
|
98
|
+
write(process.stderr, 'debug', args);
|
|
99
|
+
}
|
|
100
|
+
}
|