@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge review-file` (plan §4.1, §4.8, V4; block B11) — the coder's side of async review.
|
|
3
|
+
*
|
|
4
|
+
* review-file <path> --block <id> [--run <id>] enqueue; prints {ticket, status: queued|done, file}
|
|
5
|
+
* review-file --wait <ticket> [--max <90s>] poll; prints {ticket, status: done, result} or
|
|
6
|
+
* {ticket, status: pending} when --max elapses
|
|
7
|
+
*
|
|
8
|
+
* The same content enqueued again after an `unavailable` result is a fresh attempt (`retry: true`),
|
|
9
|
+
* not the old result.
|
|
10
|
+
* A done result that is not approved and carries open findings adds `fix_list` (what the coder
|
|
11
|
+
* fixes before re-running `review-file <path>`, which is then a round-2 recheck of the fix hunks,
|
|
12
|
+
* §4.11); a result the loop stopped adds `stop` with the reason (`review_cap`, …) — the coder
|
|
13
|
+
* stops and reports; only the orchestrator and the human act on it.
|
|
14
|
+
*
|
|
15
|
+
* Everything is a file read/write inside the workspace (a sandboxed coder has no network). The
|
|
16
|
+
* path is given relative to the current directory and stored relative to the REPOSITORY ROOT
|
|
17
|
+
* (V4); an absolute path or a `..` segment is refused with `bad-path`. With no live worker the
|
|
18
|
+
* answer is `worker_down` — never approval — and nothing is enqueued; the coder must then print
|
|
19
|
+
* `===BLOCK <id> FAILED: worker down===`. Output is one JSON line on stdout.
|
|
20
|
+
* Exit codes: 0 queued/done/pending, 1 bad-path or another refusal, 2 usage, 3 worker_down.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, rmSync } from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { enqueue, isDone, liveWorker, queueDir } from '../worker/queue.mjs';
|
|
26
|
+
import { assertBlockId, assertTicketId, normalizeRequestPath, repoRootOf, WorkerError } from '../worker/ticket.mjs';
|
|
27
|
+
import { parseFlags } from '../state/cli-args.mjs';
|
|
28
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
29
|
+
|
|
30
|
+
const USAGE = 'usage: code-forge review-file <path> --block <id> [--run <id>] | review-file --wait <ticket> [--max <seconds>s]\n';
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_WAIT_MS = 90_000;
|
|
33
|
+
const POLL_MS = 200;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `90s`, `90`, `1500ms` → milliseconds.
|
|
37
|
+
* @param {unknown} raw @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
export function parseMax(raw) {
|
|
40
|
+
const m = typeof raw === 'string' ? /^(\d{1,7})(ms|s)?$/.exec(raw) : null;
|
|
41
|
+
if (!m) throw new WorkerError('usage', '--max must look like 90s or 1500ms');
|
|
42
|
+
const n = Number(m[1]);
|
|
43
|
+
return m[2] === 'ms' ? n : n * 1000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} repoRoot @param {string} ticket
|
|
48
|
+
* @returns {Record<string, any> | null} the worker's result named by the done marker.
|
|
49
|
+
*/
|
|
50
|
+
function doneResult(repoRoot, ticket) {
|
|
51
|
+
try {
|
|
52
|
+
const marker = JSON.parse(readFileSync(path.join(queueDir(repoRoot), `${ticket}.done`), 'utf8'));
|
|
53
|
+
const rel = typeof marker.result === 'string' ? marker.result : '';
|
|
54
|
+
if (!rel.startsWith(`.code-forge${path.sep}reviews${path.sep}`) || rel.split(path.sep).includes('..')) return null;
|
|
55
|
+
return JSON.parse(readFileSync(path.join(repoRoot, rel), 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The coder-facing summary of a done result: the fix list and the stop reason, when any.
|
|
63
|
+
* @param {Record<string, any> | null} result
|
|
64
|
+
* @returns {Record<string, any>}
|
|
65
|
+
*/
|
|
66
|
+
export function fixList(result) {
|
|
67
|
+
if (!result || result.approved === true) return {};
|
|
68
|
+
/** @type {Record<string, any>} */
|
|
69
|
+
const out = {};
|
|
70
|
+
if (Array.isArray(result.findings) && result.findings.length > 0) {
|
|
71
|
+
out.fix_list = result.findings.map((/** @type {Record<string, any>} */ f) => ({
|
|
72
|
+
id: f?.id ?? null,
|
|
73
|
+
severity: f?.severity ?? null,
|
|
74
|
+
lines: `${f?.line_start ?? '?'}-${f?.line_end ?? '?'}`,
|
|
75
|
+
claim: f?.claim ?? '',
|
|
76
|
+
fix: f?.fix ?? '',
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
if (typeof result.stopped === 'string') out.stop = result.stopped;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string[]} args
|
|
85
|
+
* @param {{stdout?: {write: (s: string) => unknown}, stderr?: {write: (s: string) => unknown}, cwd?: string, pollMs?: number}} [deps]
|
|
86
|
+
* @returns {Promise<number>}
|
|
87
|
+
*/
|
|
88
|
+
export async function runReviewFile(args, deps = {}) {
|
|
89
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
90
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
91
|
+
const say = (/** @type {Record<string, any>} */ obj) => writeSafe(stdout, `${JSON.stringify(obj)}\n`);
|
|
92
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
93
|
+
try {
|
|
94
|
+
const { flags, positionals } = parseFlags(args, { values: ['block', 'run', 'wait', 'max'] });
|
|
95
|
+
const repoRoot = await repoRootOf(cwd);
|
|
96
|
+
|
|
97
|
+
if (typeof flags.wait === 'string') {
|
|
98
|
+
if (positionals.length > 0 || flags.block !== undefined) throw new WorkerError('usage', '--wait takes only a ticket and --max');
|
|
99
|
+
const ticket = assertTicketId(flags.wait);
|
|
100
|
+
const maxMs = flags.max === undefined ? DEFAULT_WAIT_MS : parseMax(flags.max);
|
|
101
|
+
const deadline = Date.now() + maxMs;
|
|
102
|
+
for (;;) {
|
|
103
|
+
if (isDone(repoRoot, ticket)) {
|
|
104
|
+
const result = doneResult(repoRoot, ticket);
|
|
105
|
+
say({ ticket, status: 'done', result, ...fixList(result) });
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
if (!liveWorker(repoRoot)) {
|
|
109
|
+
say({ ticket, status: 'worker_down' });
|
|
110
|
+
return 3;
|
|
111
|
+
}
|
|
112
|
+
if (Date.now() >= deadline) {
|
|
113
|
+
say({ ticket, status: 'pending' });
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, deps.pollMs ?? POLL_MS));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (positionals.length !== 1 || typeof flags.block !== 'string' || flags.max !== undefined) throw new WorkerError('usage', 'give one path and --block');
|
|
121
|
+
const block = assertBlockId(flags.block);
|
|
122
|
+
const file = normalizeRequestPath(positionals[0], cwd, repoRoot);
|
|
123
|
+
const worker = liveWorker(repoRoot);
|
|
124
|
+
if (!worker || (typeof flags.run === 'string' && flags.run !== worker.run)) {
|
|
125
|
+
say({ status: 'worker_down', file });
|
|
126
|
+
return 3;
|
|
127
|
+
}
|
|
128
|
+
const queued = enqueue({ repoRoot, run: worker.run, block, file });
|
|
129
|
+
if (queued.status === 'done' && doneResult(repoRoot, queued.ticket)?.status === 'unavailable') {
|
|
130
|
+
// `unavailable` is never approval AND never final: drop the done marker so the worker runs
|
|
131
|
+
// the ticket again (the fix loop re-runs its pending round); the signed result is replaced.
|
|
132
|
+
rmSync(path.join(queueDir(repoRoot), `${queued.ticket}.done`), { force: true });
|
|
133
|
+
say({ ...enqueue({ repoRoot, run: worker.run, block, file }), retry: true });
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
say(queued);
|
|
137
|
+
return 0;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const code = err instanceof WorkerError || err?.name === 'StateError' ? err.code : 'error';
|
|
140
|
+
if (code === 'usage') {
|
|
141
|
+
writeSafe(stderr, `review-file: ${err.message}\n${USAGE}`);
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
say({ status: 'refused', reason: code, message: err?.message ?? String(err) });
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {string[]} args @returns {Promise<number>} */
|
|
150
|
+
export default async function run(args) {
|
|
151
|
+
return runReviewFile(args);
|
|
152
|
+
}
|
package/src/cli/run.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge run start|status|end` (plan §4.9). The orchestrator runs these; a coder never does
|
|
3
|
+
* (`run start` is on the forbidden list, ★ `code-forge-worker-from-coder`).
|
|
4
|
+
*
|
|
5
|
+
* run start [--cwd <dir>] [--run <id>] [--engine <e>] [--worker-pid <pid>]
|
|
6
|
+
* run start --reattach --run <id> [--worker-pid <pid>]
|
|
7
|
+
* run status --run <id>
|
|
8
|
+
* run end --run <id>
|
|
9
|
+
*
|
|
10
|
+
* B11: from the CLI (the default export), `run start` without `--worker-pid` sweeps dead run
|
|
11
|
+
* roots, launches `code-forge worker --run <id>` detached, waits until it announces itself in the
|
|
12
|
+
* queue and pins it; `run start --reattach` without `--worker-pid` re-pins the live worker the
|
|
13
|
+
* queue announces (`repinWorker`). Called as `runRun(args, deps)` without `deps.startWorker` (the
|
|
14
|
+
* state tests), no worker is launched and none is pinned unless `--worker-pid` is given.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { loadProjectConfig } from '../config/load.mjs';
|
|
19
|
+
import { appendRow } from '../ledger/write.mjs';
|
|
20
|
+
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
21
|
+
import { StateError } from '../state/paths.mjs';
|
|
22
|
+
import { endRun, processStartTime, readRun, reattachWorker, startRun, stopPinnedWorker } from '../state/run.mjs';
|
|
23
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
24
|
+
import { sweepRoots, tmpBase } from '../util/tmp.mjs';
|
|
25
|
+
import { launchWorker, repinWorker } from '../worker/loop.mjs';
|
|
26
|
+
import { repoRootOf } from '../worker/ticket.mjs';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `project.slug` from config, else the workspace directory name as a ledger slug. The configured
|
|
30
|
+
* value is NOT trusted here: `startRun` refuses any slug outside `/^[a-z0-9][a-z0-9-]*$/`
|
|
31
|
+
* (`bad-project`) before anything is written, so `../x` never reaches the ledger path. Run ids
|
|
32
|
+
* are checked the same way by the state layer (`assertRunId`, `bad-run-id`).
|
|
33
|
+
* @param {Record<string, any>} cfg @param {string} workspace
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function slugFor(cfg, workspace) {
|
|
37
|
+
const configured = cfg?.project?.slug;
|
|
38
|
+
if (typeof configured === 'string' && configured.length > 0) return configured;
|
|
39
|
+
const derived = path.basename(workspace).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
40
|
+
return derived || 'project';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {string} slug */
|
|
44
|
+
const ledgerWriter = (slug) => (/** @type {Record<string, any>} */ row) => appendRow(row, { slug });
|
|
45
|
+
|
|
46
|
+
/** @typedef {(pin: {pid: number, started_at: string}, opts: {probe?: import('../state/run.mjs').StartTimeProbe}) => Promise<{stopped: boolean, reason?: string}>} StopWorker */
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A worker that `run start` launched but could not pin must not outlive the command as an orphan:
|
|
50
|
+
* stop it through the pinned-worker stopper (pid > 1, start time re-checked right before the
|
|
51
|
+
* signal, so a recycled pid is never signalled) and name the pid on stderr. `startedAt` is the
|
|
52
|
+
* start time read right after the launch; null means the process was already gone.
|
|
53
|
+
* @param {{pid: number, startedAt: string | null}} launched
|
|
54
|
+
* @param {{stopWorker: StopWorker, probe?: import('../state/run.mjs').StartTimeProbe, err: (s: string) => void}} deps
|
|
55
|
+
*/
|
|
56
|
+
async function stopLaunched({ pid, startedAt }, { stopWorker, probe, err }) {
|
|
57
|
+
/** @type {{stopped: boolean, reason?: string}} */
|
|
58
|
+
let outcome;
|
|
59
|
+
try {
|
|
60
|
+
outcome = startedAt === null ? { stopped: false, reason: 'not running' } : await stopWorker({ pid, started_at: startedAt }, { ...(probe ? { probe } : {}) });
|
|
61
|
+
} catch (thrown) {
|
|
62
|
+
outcome = { stopped: false, reason: thrown?.message ?? String(thrown) };
|
|
63
|
+
}
|
|
64
|
+
err(`run start: launched worker pid ${pid} was not pinned; ${outcome.stopped ? 'stopped it' : `not stopped (${outcome.reason ?? 'unknown'})`}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string[]} args
|
|
69
|
+
* @param {{
|
|
70
|
+
* stdout?: {write: (s: string) => unknown}, stderr?: {write: (s: string) => unknown},
|
|
71
|
+
* probe?: import('../state/run.mjs').StartTimeProbe,
|
|
72
|
+
* stopWorker?: StopWorker,
|
|
73
|
+
* startWorker?: (opts: {runId: string, workspace: string}) => Promise<{pid: number, repoRoot: string}>,
|
|
74
|
+
* }} [deps] - `startWorker` (the CLI passes B11's `launchWorker`) launches the worker on a plain
|
|
75
|
+
* `run start`; `stopWorker` defaults to the state layer's `stopPinnedWorker` (pid > 1 and a
|
|
76
|
+
* re-verified start time before any signal) — `run end` uses it, and so does a `run start` whose
|
|
77
|
+
* launched worker could not be pinned.
|
|
78
|
+
* @returns {Promise<number>}
|
|
79
|
+
*/
|
|
80
|
+
export async function runRun(args, deps = {}) {
|
|
81
|
+
const { stdout = process.stdout, stderr = process.stderr, probe, stopWorker, startWorker } = deps;
|
|
82
|
+
const out = (/** @type {string} */ s) => writeSafe(stdout, s);
|
|
83
|
+
const err = (/** @type {string} */ s) => writeSafe(stderr, s);
|
|
84
|
+
const [sub, ...rest] = args;
|
|
85
|
+
if (!['start', 'status', 'end'].includes(sub)) {
|
|
86
|
+
err(USAGE);
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const { flags } = parseFlags(rest, { values: ['run', 'cwd', 'engine', 'worker-pid'], booleans: ['reattach'] });
|
|
91
|
+
const runId = typeof flags.run === 'string' ? flags.run : undefined;
|
|
92
|
+
const workerPid = intFlag(flags['worker-pid'], 'worker-pid');
|
|
93
|
+
|
|
94
|
+
if (sub === 'start' && flags.reattach) {
|
|
95
|
+
if (!runId) throw new StateError('usage', 'run start --reattach needs --run');
|
|
96
|
+
const { project, workspace } = await readRun(runId);
|
|
97
|
+
if (workerPid !== undefined) {
|
|
98
|
+
await reattachWorker({ runId, workerPid, writeRow: ledgerWriter(project), probe });
|
|
99
|
+
out(`run ${runId}: worker re-pinned to pid ${workerPid}\n`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
const repinned = await repinWorker({ runId, repoRoot: await repoRootOf(workspace), writeRow: ledgerWriter(project), ...(probe ? { probe } : {}) });
|
|
103
|
+
out(`run ${runId}: worker re-pinned to pid ${repinned.worker.pid}\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
if (sub === 'start') {
|
|
107
|
+
const workspace = path.resolve(typeof flags.cwd === 'string' ? flags.cwd : process.cwd());
|
|
108
|
+
const loaded = await loadProjectConfig(workspace);
|
|
109
|
+
if (!loaded.ok) {
|
|
110
|
+
err(`run start: ${loaded.message}\n`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
const project = slugFor(loaded.config, workspace);
|
|
114
|
+
const engine = typeof flags.engine === 'string' ? flags.engine : undefined;
|
|
115
|
+
const tmpRoot = typeof loaded.config?.tmp?.root === 'string' ? loaded.config.tmp.root : undefined;
|
|
116
|
+
await sweepRoots({ root: tmpBase(tmpRoot) }); // dead runs' temp roots and orphans (§9.6, V12)
|
|
117
|
+
let record = await startRun({ workspace, project, config: loaded.config, engine, runId, workerPid, writeRow: ledgerWriter(project), probe });
|
|
118
|
+
if (!record.worker && startWorker) {
|
|
119
|
+
const launched = await startWorker({ runId: record.run_id, workspace });
|
|
120
|
+
const startedAt = await (probe ?? processStartTime)(launched.pid); // what the stop below re-checks against
|
|
121
|
+
try {
|
|
122
|
+
// pin ONLY the pid we launched: an older announced worker must not be pinned over it
|
|
123
|
+
record = await repinWorker({ runId: record.run_id, repoRoot: launched.repoRoot, writeRow: ledgerWriter(project), expectPid: launched.pid, ...(probe ? { probe } : {}) });
|
|
124
|
+
} catch (thrown) {
|
|
125
|
+
await stopLaunched({ pid: launched.pid, startedAt }, { stopWorker: stopWorker ?? stopPinnedWorker, probe, err });
|
|
126
|
+
throw thrown;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
out(`run ${record.run_id} started · engine ${record.engine} · worker ${record.worker ? `pid ${record.worker.pid}` : 'not pinned'}\n`);
|
|
130
|
+
if (!record.worker) err('run start: no worker pinned — start the worker, then `run start --reattach --run <id> --worker-pid <pid>`\n');
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
if (sub === 'status' && runId) {
|
|
134
|
+
const record = await readRun(runId);
|
|
135
|
+
const { run_id, status, engine, blocks, orphans } = record;
|
|
136
|
+
out(`${JSON.stringify({ run_id, status, engine, worker_pinned: Boolean(record.worker), blocks, orphans }, null, 2)}\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
if (sub === 'end' && runId) {
|
|
140
|
+
const { project } = await readRun(runId);
|
|
141
|
+
const record = await endRun({ runId, writeRow: ledgerWriter(project), stopWorker, probe });
|
|
142
|
+
out(`run ${record.run_id} ended\n`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
} catch (thrown) {
|
|
146
|
+
err(`run ${sub ?? ''}: ${thrown?.message ?? String(thrown)}\n`);
|
|
147
|
+
return thrown instanceof StateError && thrown.code === 'usage' ? 2 : 1;
|
|
148
|
+
}
|
|
149
|
+
err(USAGE);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const USAGE =
|
|
154
|
+
'usage: code-forge run start [--cwd <dir>] [--run <id>] [--engine <e>] [--worker-pid <pid>] | start --reattach --run <id> [--worker-pid <pid>] | status --run <id> | end --run <id>\n';
|
|
155
|
+
|
|
156
|
+
/** @param {string[]} args @returns {Promise<number>} */
|
|
157
|
+
export default async function run(args) {
|
|
158
|
+
return runRun(args, { startWorker: launchWorker });
|
|
159
|
+
}
|
package/src/cli/s2.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge s2 --packet <file.json> [--run <id>] [--block <id>] [--timeout <s>]` (plan §3.4;
|
|
3
|
+
* block B9a). The packet file is `{question, context?, options?, s1?}`; the verb prints the S2
|
|
4
|
+
* answer `{decision, confidence, reason, overrule, ask_human, human_question}` as one JSON line.
|
|
5
|
+
* Exit codes: 0 answered, 1 failed or invalid answer, 2 usage, 3 L3 and every fallback
|
|
6
|
+
* unavailable (S2 ⇒ stop and ask the human, §5.6).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { loadProjectConfig } from '../config/load.mjs';
|
|
12
|
+
import { runS2 } from '../session/s2.mjs';
|
|
13
|
+
import { runRootFor, SessionError } from '../session/spawn.mjs';
|
|
14
|
+
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
15
|
+
import { StateError } from '../state/paths.mjs';
|
|
16
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
17
|
+
import { slugFor } from './run.mjs';
|
|
18
|
+
import { exitCodeFor } from './spawn.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} file @param {string} flag
|
|
22
|
+
* @returns {Promise<any>}
|
|
23
|
+
* @throws {SessionError} `usage` naming the flag only — never the file's content.
|
|
24
|
+
*/
|
|
25
|
+
async function readJSONFlag(file, flag) {
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
text = await readFile(path.resolve(file), 'utf8');
|
|
29
|
+
} catch {
|
|
30
|
+
throw new SessionError('usage', `--${flag} cannot be read`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
} catch {
|
|
35
|
+
throw new SessionError('usage', `--${flag} is not a valid JSON file`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const USAGE = 'usage: code-forge s2 --packet <file.json> [--run <id>] [--block <id>] [--timeout <seconds>]\n';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string[]} args
|
|
43
|
+
* @param {import('../session/spawn.mjs').SessionDeps & {stdout?: {write: (s: string) => unknown}}} [deps]
|
|
44
|
+
* @returns {Promise<number>}
|
|
45
|
+
*/
|
|
46
|
+
export async function runS2Verb(args, deps = {}) {
|
|
47
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
48
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
49
|
+
try {
|
|
50
|
+
const { flags, positionals } = parseFlags(args, { values: ['packet', 'run', 'block', 'timeout'] });
|
|
51
|
+
if (positionals.length > 0 || typeof flags.packet !== 'string') {
|
|
52
|
+
writeSafe(stderr, USAGE);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
const loaded = await loadProjectConfig(process.cwd());
|
|
56
|
+
if (!loaded.ok) {
|
|
57
|
+
writeSafe(stderr, `s2: ${loaded.message}\n`);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
const cfg = loaded.config;
|
|
61
|
+
const timeout = intFlag(flags.timeout, 'timeout');
|
|
62
|
+
const packet = await readJSONFlag(flags.packet, 'packet');
|
|
63
|
+
const result = await runS2(
|
|
64
|
+
{
|
|
65
|
+
cfg,
|
|
66
|
+
packet,
|
|
67
|
+
run: typeof flags.run === 'string' ? flags.run : undefined,
|
|
68
|
+
block: typeof flags.block === 'string' ? flags.block : undefined,
|
|
69
|
+
runRoot: typeof flags.run === 'string' ? runRootFor(flags.run, cfg?.tmp?.root) : undefined,
|
|
70
|
+
slug: slugFor(cfg, process.cwd()),
|
|
71
|
+
timeoutMs: timeout === undefined ? undefined : timeout * 1000,
|
|
72
|
+
},
|
|
73
|
+
{ ...deps, stderr },
|
|
74
|
+
);
|
|
75
|
+
if (result.status === 'ok') {
|
|
76
|
+
writeSafe(stdout, `${JSON.stringify(result.answer)}\n`);
|
|
77
|
+
} else {
|
|
78
|
+
writeSafe(stderr, `s2: ${result.status}${result.reason ? ` (${result.reason})` : ''}\n`);
|
|
79
|
+
}
|
|
80
|
+
return exitCodeFor(result);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof StateError || err instanceof SessionError) {
|
|
83
|
+
writeSafe(stderr, `s2: ${/** @type {Error} */ (err).message}\n`);
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {string[]} args */
|
|
91
|
+
export default async function s2Verb(args) {
|
|
92
|
+
return runS2Verb(args);
|
|
93
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge spawn --level L<n> --role <role> --brief <file> [--schema <file>] [--cwd <dir>]
|
|
3
|
+
* [--run <id>] [--block <id>] [--timeout <s>] [--background]` (plan §5.2; block B9a).
|
|
4
|
+
*
|
|
5
|
+
* Runs one isolated session through `src/session/spawn.mjs` and prints its result as one JSON line
|
|
6
|
+
* on stdout; the `level=… provider=… model=… effort=… fallback_step=…` line goes to stderr.
|
|
7
|
+
* Exit codes: 0 ok / started, 1 failed (timeout, bad output, refused), 2 usage, 3 unavailable
|
|
8
|
+
* (every step of the ladder reported unavailability).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { loadProjectConfig } from '../config/load.mjs';
|
|
14
|
+
import { ROLES, runRootFor, SessionError, spawnSession } from '../session/spawn.mjs';
|
|
15
|
+
import { intFlag, parseFlags } from '../state/cli-args.mjs';
|
|
16
|
+
import { StateError } from '../state/paths.mjs';
|
|
17
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
18
|
+
import { slugFor } from './run.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} file @param {string} flag
|
|
22
|
+
* @returns {Promise<any>}
|
|
23
|
+
* @throws {SessionError} `usage` naming the flag only — never the file's content.
|
|
24
|
+
*/
|
|
25
|
+
async function readJSONFlag(file, flag) {
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
text = await readFile(path.resolve(file), 'utf8');
|
|
29
|
+
} catch {
|
|
30
|
+
throw new SessionError('usage', `--${flag} cannot be read`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
} catch {
|
|
35
|
+
throw new SessionError('usage', `--${flag} is not a valid JSON file`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const USAGE = `usage: code-forge spawn --level L0|L1|L2|L3 --role ${ROLES.join('|')} --brief <file> [--schema <file>] [--cwd <dir>] [--run <id>] [--block <id>] [--timeout <seconds>] [--background]\n`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{status: string}} result
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
export function exitCodeFor(result) {
|
|
46
|
+
if (result.status === 'ok' || result.status === 'started') return 0;
|
|
47
|
+
return result.status === 'unavailable' ? 3 : 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string[]} args
|
|
52
|
+
* @param {import('../session/spawn.mjs').SessionDeps & {stdout?: {write: (s: string) => unknown}}} [deps]
|
|
53
|
+
* @returns {Promise<number>}
|
|
54
|
+
*/
|
|
55
|
+
export async function runSpawn(args, deps = {}) {
|
|
56
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
57
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
58
|
+
try {
|
|
59
|
+
const { flags, positionals } = parseFlags(args, { values: ['level', 'role', 'brief', 'schema', 'cwd', 'run', 'block', 'timeout'], booleans: ['background'] });
|
|
60
|
+
if (positionals.length > 0 || typeof flags.level !== 'string' || typeof flags.role !== 'string' || typeof flags.brief !== 'string') {
|
|
61
|
+
writeSafe(stderr, USAGE);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
const loaded = await loadProjectConfig(process.cwd());
|
|
65
|
+
if (!loaded.ok) {
|
|
66
|
+
writeSafe(stderr, `spawn: ${loaded.message}\n`);
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
const cfg = loaded.config;
|
|
70
|
+
const timeout = intFlag(flags.timeout, 'timeout');
|
|
71
|
+
const schema = typeof flags.schema === 'string' ? await readJSONFlag(flags.schema, 'schema') : undefined;
|
|
72
|
+
const result = await spawnSession(
|
|
73
|
+
{
|
|
74
|
+
cfg,
|
|
75
|
+
level: /** @type {any} */ (flags.level),
|
|
76
|
+
role: /** @type {any} */ (flags.role),
|
|
77
|
+
promptPath: path.resolve(flags.brief),
|
|
78
|
+
cwd: typeof flags.cwd === 'string' ? path.resolve(flags.cwd) : undefined,
|
|
79
|
+
schema,
|
|
80
|
+
timeoutMs: timeout === undefined ? undefined : timeout * 1000,
|
|
81
|
+
background: flags.background === true,
|
|
82
|
+
runRoot: typeof flags.run === 'string' ? runRootFor(flags.run, cfg?.tmp?.root) : undefined,
|
|
83
|
+
run: typeof flags.run === 'string' ? flags.run : undefined,
|
|
84
|
+
block: typeof flags.block === 'string' ? flags.block : undefined,
|
|
85
|
+
slug: slugFor(cfg, process.cwd()),
|
|
86
|
+
},
|
|
87
|
+
{ ...deps, stderr },
|
|
88
|
+
);
|
|
89
|
+
const { text: _text, ...shown } = /** @type {Record<string, any>} */ (result);
|
|
90
|
+
writeSafe(stdout, `${JSON.stringify(shown)}\n`);
|
|
91
|
+
return exitCodeFor(result);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof StateError || err instanceof SessionError) {
|
|
94
|
+
writeSafe(stderr, `spawn: ${/** @type {Error} */ (err).message}\n`);
|
|
95
|
+
return err instanceof SessionError && err.code === 'forbidden' ? 1 : 2;
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {string[]} args */
|
|
102
|
+
export default async function spawnVerb(args) {
|
|
103
|
+
return runSpawn(args);
|
|
104
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge upgrade [--source <path>]` — re-points every recorded install at a skill source
|
|
3
|
+
* (plan §10.3 acceptance: "`upgrade` re-points all 3 recorded links"; research/installer-patterns.md
|
|
4
|
+
* point 8, "a self `upgrade` command"). A symlink record is re-created to point at the new
|
|
5
|
+
* source; a copy record is re-copied from it. With no `--source`, defaults to this running
|
|
6
|
+
* package's own `skill/` directory (the normal case: `npm update` moved the package, `upgrade`
|
|
7
|
+
* re-syncs every harness to it) — `--source` exists for pointing at an explicit path instead
|
|
8
|
+
* (tests, or a power user working from a git checkout of the skill).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { stat } from 'node:fs/promises';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
16
|
+
import { installsPath, upgradeAll } from '../install/link.mjs';
|
|
17
|
+
|
|
18
|
+
const USAGE = 'usage: code-forge upgrade [--source <path>]\n';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
/** This package's own skill directory: `<package root>/skill`, i.e. two levels above `src/cli/`. */
|
|
22
|
+
export const DEFAULT_SKILL_SOURCE = path.join(__dirname, '..', '..', 'skill');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strict parse: `[--source <path>]` only.
|
|
26
|
+
* @param {string[]} args
|
|
27
|
+
* @returns {{source?: string} | null}
|
|
28
|
+
*/
|
|
29
|
+
export function parseUpgradeArgs(args) {
|
|
30
|
+
if (args.length === 0) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const [flag, value, ...extra] = args;
|
|
34
|
+
if (flag !== '--source' || typeof value !== 'string' || value.length === 0 || extra.length > 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return { source: value };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string[]} args
|
|
42
|
+
* @param {object} deps
|
|
43
|
+
* @param {{write: (s: string) => unknown}} [deps.stdout]
|
|
44
|
+
* @param {{write: (s: string) => unknown}} [deps.stderr]
|
|
45
|
+
* @param {string} [deps.home]
|
|
46
|
+
* @param {string} [deps.defaultSource]
|
|
47
|
+
* @param {string} [deps.cwd] - a relative `--source` is resolved against this, never against the
|
|
48
|
+
* link's own directory (which `symlink()` would otherwise silently use).
|
|
49
|
+
* @returns {Promise<number>}
|
|
50
|
+
*/
|
|
51
|
+
export async function runUpgrade(
|
|
52
|
+
args,
|
|
53
|
+
{
|
|
54
|
+
stdout = process.stdout,
|
|
55
|
+
stderr = process.stderr,
|
|
56
|
+
home = os.homedir(),
|
|
57
|
+
defaultSource = DEFAULT_SKILL_SOURCE,
|
|
58
|
+
cwd = process.cwd(),
|
|
59
|
+
} = {},
|
|
60
|
+
) {
|
|
61
|
+
const out = (/** @type {string} */ s) => writeSafe(stdout, s);
|
|
62
|
+
const err = (/** @type {string} */ s) => writeSafe(stderr, s);
|
|
63
|
+
|
|
64
|
+
const parsed = parseUpgradeArgs(args);
|
|
65
|
+
if (!parsed) {
|
|
66
|
+
err(USAGE);
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Resolved BEFORE use: a relative --source (e.g. "./skill") must mean "relative to where the
|
|
71
|
+
// user ran this command", never get stored/symlinked as a relative string (which `readlink`
|
|
72
|
+
// would then resolve against the LINK's own directory, not the user's cwd).
|
|
73
|
+
const resolvedSource = path.resolve(cwd, parsed.source ?? defaultSource);
|
|
74
|
+
|
|
75
|
+
let sourceStat;
|
|
76
|
+
try {
|
|
77
|
+
sourceStat = await stat(resolvedSource);
|
|
78
|
+
} catch {
|
|
79
|
+
err(`upgrade: source "${resolvedSource}" does not exist\n`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
if (!sourceStat.isDirectory()) {
|
|
83
|
+
err(`upgrade: source "${resolvedSource}" is not a directory\n`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const updated = await upgradeAll({ installsFile: installsPath(home), source: resolvedSource });
|
|
88
|
+
for (const record of updated) {
|
|
89
|
+
out(`upgraded ${record.harness} (${record.scope}) -> ${record.source}\n`);
|
|
90
|
+
}
|
|
91
|
+
if (updated.length === 0) {
|
|
92
|
+
out('nothing to upgrade\n');
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string[]} args
|
|
99
|
+
* @returns {Promise<number>}
|
|
100
|
+
*/
|
|
101
|
+
export default async function upgrade(args) {
|
|
102
|
+
return runUpgrade(args, {});
|
|
103
|
+
}
|