dw-sdlc 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentic-config.d.ts +16 -0
- package/dist/agentic-config.js +28 -23
- package/dist/cannot-run.d.ts +27 -0
- package/dist/cannot-run.js +48 -0
- package/dist/catalogue.js +2 -1
- package/dist/checks/agents-map.js +7 -8
- package/dist/checks/check.d.ts +10 -8
- package/dist/checks/check.js +9 -7
- package/dist/checks/decision-records.js +3 -2
- package/dist/checks/file-size-ratchet.js +13 -17
- package/dist/checks/index.d.ts +6 -5
- package/dist/checks/index.js +10 -9
- package/dist/checks/markdown-refs.js +1 -1
- package/dist/checks/primitives.js +5 -5
- package/dist/checks/routed-docs.js +1 -3
- package/dist/checks/skill-sections.js +12 -20
- package/dist/checks/tree.js +7 -7
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +26 -5
- package/dist/commands/check.js +27 -42
- package/dist/commands/config.js +22 -27
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +11 -18
- package/dist/commands/gate.js +35 -40
- package/dist/commands/lead-start.js +28 -42
- package/dist/commands/pr-reviewed.js +41 -59
- package/dist/commands/pr-ship.js +71 -89
- package/dist/commands/primitives.js +38 -40
- package/dist/commands/recap.js +23 -74
- package/dist/commands/references-setup.js +28 -76
- package/dist/commands/references-sync.js +29 -53
- package/dist/commands/setup.d.ts +2 -7
- package/dist/commands/setup.js +38 -90
- package/dist/commands/sweep.js +46 -157
- package/dist/commands/worker-ls.js +15 -31
- package/dist/commands/worker-report.js +36 -52
- package/dist/commands/worker-start.js +43 -57
- package/dist/drift.js +2 -1
- package/dist/exec.d.ts +20 -0
- package/dist/exec.js +37 -4
- package/dist/gate-cache.js +2 -1
- package/dist/git.d.ts +32 -0
- package/dist/git.js +60 -0
- package/dist/json-schema.js +1 -3
- package/dist/landed.js +11 -22
- package/dist/paused-operation.d.ts +2 -0
- package/dist/paused-operation.js +17 -0
- package/dist/pins.js +2 -1
- package/dist/pr/decide.d.ts +4 -16
- package/dist/pr/decide.js +48 -84
- package/dist/pr/delta.d.ts +3 -4
- package/dist/pr/delta.js +10 -19
- package/dist/pr/read.d.ts +27 -30
- package/dist/pr/read.js +94 -182
- package/dist/pr/reply.js +3 -5
- package/dist/pr/route.d.ts +11 -3
- package/dist/pr/route.js +5 -5
- package/dist/pr/standing.d.ts +138 -0
- package/dist/pr/standing.js +181 -0
- package/dist/pr/state.d.ts +10 -69
- package/dist/pr/state.js +42 -72
- package/dist/pr/upstream.d.ts +1 -6
- package/dist/pr/upstream.js +2 -2
- package/dist/primitives-diff.js +1 -1
- package/dist/recap-next.js +34 -47
- package/dist/recap-primitives.js +1 -1
- package/dist/references-key.d.ts +0 -1
- package/dist/references-key.js +3 -4
- package/dist/run-command.d.ts +4 -1
- package/dist/run-command.js +32 -11
- package/dist/setup-gate.d.ts +1 -7
- package/dist/setup-rules.js +2 -6
- package/dist/setup-writer.d.ts +32 -0
- package/dist/setup-writer.js +60 -0
- package/dist/shell-line.d.ts +2 -0
- package/dist/shell-line.js +5 -0
- package/dist/sweep-removal.d.ts +31 -0
- package/dist/sweep-removal.js +133 -0
- package/dist/sweep-terminals.d.ts +1 -1
- package/dist/sweep-terminals.js +21 -57
- package/dist/sweep-trees.d.ts +19 -3
- package/dist/sweep-trees.js +67 -31
- package/dist/worker/herdr.d.ts +67 -23
- package/dist/worker/herdr.js +126 -77
- package/dist/worker/launch.js +32 -45
- package/dist/worker/origin.d.ts +1 -1
- package/dist/worker/origin.js +9 -11
- package/dist/worker/tabs.js +11 -7
- package/dist/working-tree.d.ts +0 -8
- package/dist/working-tree.js +6 -22
- package/dist/worktree-lock.js +1 -1
- package/package.json +1 -1
- package/dist/git-env.d.ts +0 -2
- package/dist/git-env.js +0 -10
package/dist/commands/recap.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
// `dw-sdlc recap`: the facts a Recap rests on and whether the session can be closed, read from git and
|
|
2
2
|
// gh. It writes nothing: the Action of the branch's pull request is the Ship script's decision, read.
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { parseArgs } from "node:util";
|
|
6
3
|
import { findRepository, mainCheckout, resolveConfig } from "../agentic-config.js";
|
|
4
|
+
import { CannotRun, readArgs, usageError } from "../cannot-run.js";
|
|
5
|
+
import { asked, must } from "../git.js";
|
|
6
|
+
import { pausedIn } from "../paused-operation.js";
|
|
7
7
|
import { decide, declaredPolicy, labelOf, lineOf } from "../pr/decide.js";
|
|
8
|
-
import {
|
|
9
|
-
import { holds
|
|
8
|
+
import { defaultBranch, ghReads, now } from "../pr/read.js";
|
|
9
|
+
import { holds } from "../pr/upstream.js";
|
|
10
10
|
import { READY_LABEL, readNext } from "../recap-next.js";
|
|
11
11
|
import { rollup } from "../recap-primitives.js";
|
|
12
|
-
import { isLive,
|
|
13
|
-
import { asked, git } from "../working-tree.js";
|
|
12
|
+
import { isLive, workersOf, workingFolders } from "../worker/herdr.js";
|
|
14
13
|
export const summary = "the facts a Recap rests on, whether the session can be closed and what could come next";
|
|
15
14
|
const USAGE = `Usage: dw-sdlc recap [--json]
|
|
16
15
|
|
|
@@ -54,14 +53,6 @@ Exit 0 the session can be closed, 1 it cannot, 2 when it cannot run.`;
|
|
|
54
53
|
function plural(howMany, word) {
|
|
55
54
|
return `${howMany} ${word}${howMany === 1 ? "" : "s"}`;
|
|
56
55
|
}
|
|
57
|
-
/** `git <args>` at `root`; what it prints, or a stop naming its problem. */
|
|
58
|
-
function must(root, args) {
|
|
59
|
-
const result = git(root, args);
|
|
60
|
-
if ("error" in result) {
|
|
61
|
-
throw new CannotRun(result.error, "check the repository with git status");
|
|
62
|
-
}
|
|
63
|
-
return result.out;
|
|
64
|
-
}
|
|
65
56
|
function count(root, args) {
|
|
66
57
|
return Number(must(root, ["rev-list", "--count", ...args]));
|
|
67
58
|
}
|
|
@@ -77,20 +68,6 @@ function upstreamOf(root, branch) {
|
|
|
77
68
|
const [ref = "", track] = must(root, ["for-each-ref", `--format=${format}`, `refs/heads/${branch}`]).split(" ");
|
|
78
69
|
return ref.startsWith(prefix) && track !== "[gone]" ? ref.slice(prefix.length) : null;
|
|
79
70
|
}
|
|
80
|
-
/** Git's own record of each paused operation, in the git directory a linked worktree keeps apart. */
|
|
81
|
-
const PAUSES = [
|
|
82
|
-
{ record: "rebase-merge", name: "rebase" },
|
|
83
|
-
{ record: "rebase-apply", name: "rebase or am" },
|
|
84
|
-
{ record: "MERGE_HEAD", name: "merge" },
|
|
85
|
-
{ record: "CHERRY_PICK_HEAD", name: "cherry-pick" },
|
|
86
|
-
{ record: "REVERT_HEAD", name: "revert" },
|
|
87
|
-
{ record: "sequencer", name: "cherry-pick or revert" },
|
|
88
|
-
{ record: "BISECT_LOG", name: "bisect" }
|
|
89
|
-
];
|
|
90
|
-
function pausedIn(root) {
|
|
91
|
-
const gitDir = must(root, ["rev-parse", "--absolute-git-dir"]);
|
|
92
|
-
return PAUSES.find(({ record }) => existsSync(join(gitDir, record)))?.name ?? null;
|
|
93
|
-
}
|
|
94
71
|
function readLocal(root) {
|
|
95
72
|
const branch = asked(root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) ?? null;
|
|
96
73
|
const head = asked(root, ["rev-parse", "--verify", "--quiet", "HEAD"]) ?? null;
|
|
@@ -104,7 +81,7 @@ function readLocal(root) {
|
|
|
104
81
|
uncommitted: status === "" ? 0 : status.split("\n").length,
|
|
105
82
|
upstream,
|
|
106
83
|
unpushed: head === null ? 0 : count(root, pushed),
|
|
107
|
-
paused: pausedIn(root),
|
|
84
|
+
paused: pausedIn(must(root, ["rev-parse", "--absolute-git-dir"])) ?? null,
|
|
108
85
|
worktree: { linked: mainCheckout(root) !== undefined, path: root }
|
|
109
86
|
};
|
|
110
87
|
}
|
|
@@ -118,28 +95,18 @@ function liveWorkers() {
|
|
|
118
95
|
return workingFolders(workersOf(lead, false).filter(isLive));
|
|
119
96
|
}
|
|
120
97
|
catch (error) {
|
|
121
|
-
|
|
122
|
-
throw new CannotRun(error.message, error.remedy ?? "check hc ls, then call again");
|
|
123
|
-
}
|
|
124
|
-
throw error;
|
|
98
|
+
throw error instanceof CannotRun ? error.orRemedy("check hc ls, then call again") : error;
|
|
125
99
|
}
|
|
126
100
|
}
|
|
127
101
|
function workersText(workers) {
|
|
128
102
|
return workers.map((worker) => `${worker.name} (${worker.status})`).join(", ");
|
|
129
103
|
}
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
* found by the upstream's name counts only when it holds the head: a branch started from another tracks
|
|
133
|
-
* that branch until its first push.
|
|
134
|
-
*/
|
|
135
|
-
function readShipped(root, branch, head) {
|
|
104
|
+
/** This checkout's pull request and the Action the Ship script's decision gives it, or null when it has none. */
|
|
105
|
+
function readShipped(root) {
|
|
136
106
|
// An invalid config layer resolves to ask and full, as in the Ship script.
|
|
137
107
|
const policy = declaredPolicy(resolveConfig(root).config.pr);
|
|
138
|
-
const found =
|
|
139
|
-
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
return { pr: found.pr, answer: decide(found.pr, now(), policy) };
|
|
108
|
+
const found = ghReads(policy.review.depth === "auto").checkout(root);
|
|
109
|
+
return "problem" in found ? null : { pr: found, answer: decide(found, now(), policy) };
|
|
143
110
|
}
|
|
144
111
|
/**
|
|
145
112
|
* The tracking refs, as `--remotes` names them, that the remote contradicts, which only a fetch would show:
|
|
@@ -309,20 +276,15 @@ function linesOf(local, { shipped, askedGh, workers, next }, primitives, line) {
|
|
|
309
276
|
].join("\n");
|
|
310
277
|
}
|
|
311
278
|
function parseCli(argv) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return { help: values.help === true, json: values.json === true };
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
325
|
-
}
|
|
279
|
+
const { values, positionals } = readArgs({
|
|
280
|
+
args: argv,
|
|
281
|
+
allowPositionals: true,
|
|
282
|
+
options: { help: { type: "boolean", short: "h" }, json: { type: "boolean" } }
|
|
283
|
+
}, USAGE);
|
|
284
|
+
if (positionals.length > 0 && values.help !== true) {
|
|
285
|
+
throw usageError(`it takes no argument, not ${JSON.stringify(positionals[0])}`, USAGE);
|
|
286
|
+
}
|
|
287
|
+
return { help: values.help === true, json: values.json === true };
|
|
326
288
|
}
|
|
327
289
|
function report(json) {
|
|
328
290
|
const root = findRepository(process.cwd());
|
|
@@ -333,7 +295,7 @@ function report(json) {
|
|
|
333
295
|
const { branch, head } = read;
|
|
334
296
|
// A detached HEAD names no branch for gh to look up, and a repository with no remote has no GitHub.
|
|
335
297
|
const asksGh = branch !== null && remotesOf(root).length > 0;
|
|
336
|
-
const shipped = asksGh ? readShipped(root
|
|
298
|
+
const shipped = asksGh ? readShipped(root) : null;
|
|
337
299
|
const held = shipped !== null && holds(shipped.pr, head);
|
|
338
300
|
const local = standing(root, read, shipped, held);
|
|
339
301
|
// An open pull request can still take the head, and a commit-less repository has nothing to merge.
|
|
@@ -369,22 +331,9 @@ function report(json) {
|
|
|
369
331
|
}
|
|
370
332
|
export function run(argv) {
|
|
371
333
|
const cli = parseCli(argv);
|
|
372
|
-
if ("error" in cli) {
|
|
373
|
-
process.stderr.write(`dw-sdlc recap: ${cli.error}\n\n${USAGE}\n`);
|
|
374
|
-
return 2;
|
|
375
|
-
}
|
|
376
334
|
if (cli.help) {
|
|
377
335
|
process.stdout.write(`${USAGE}\n`);
|
|
378
336
|
return 0;
|
|
379
337
|
}
|
|
380
|
-
|
|
381
|
-
return report(cli.json);
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
if (error instanceof CannotRun) {
|
|
385
|
-
process.stderr.write(`dw-sdlc recap: ${error.message} — ${error.remedy}\n`);
|
|
386
|
-
return 2;
|
|
387
|
-
}
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
338
|
+
return report(cli.json);
|
|
390
339
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// `dw-sdlc references setup` sets a repository up to declare References: the files its USAGE names,
|
|
2
2
|
// each written only when it would change. A list gains the entries it lacks at its end; the one removal
|
|
3
3
|
// is a `references` key moving out of the agentic config. A file it cannot read as expected is refused.
|
|
4
|
-
import { existsSync,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { existsSync, readFileSync, symlinkSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { repositoryRoot } from "../agentic-config.js";
|
|
7
|
+
import { readArgs, rootDir } from "../cannot-run.js";
|
|
8
8
|
import { parseObject, removeKey, render } from "../json-text.js";
|
|
9
9
|
import { CONFIG, REFERENCES_FILE, leftoverKey, leftoverProblem, parseReferences, readFile } from "../references-key.js";
|
|
10
10
|
import { renderBlock, withBlock } from "../router-block.js";
|
|
11
|
+
import { stepLines, Writer } from "../setup-writer.js";
|
|
11
12
|
const ROUTER = "references/AGENTS.md";
|
|
12
13
|
/** A symlink to the Router, so Claude Code loads it. */
|
|
13
14
|
const ROUTER_LINK = "references/CLAUDE.md";
|
|
@@ -42,47 +43,6 @@ and leaves the config; with the file there too, both stay and the step is refuse
|
|
|
42
43
|
written only when it would change, and the text around the Router's table never; --dry-run reports
|
|
43
44
|
without writing, and --check does the same and exits 1 when a file would change. Prints one line
|
|
44
45
|
per file. Exit 1 when a file is refused, 2 on a bad argument.`;
|
|
45
|
-
function messageOf(error) {
|
|
46
|
-
return error instanceof Error ? error.message : String(error);
|
|
47
|
-
}
|
|
48
|
-
function fail(message) {
|
|
49
|
-
console.error(`references:setup: ${message}`);
|
|
50
|
-
process.exit(2);
|
|
51
|
-
}
|
|
52
|
-
class Writer {
|
|
53
|
-
steps = [];
|
|
54
|
-
root;
|
|
55
|
-
dryRun;
|
|
56
|
-
constructor(root, dryRun) {
|
|
57
|
-
this.root = root;
|
|
58
|
-
this.dryRun = dryRun;
|
|
59
|
-
}
|
|
60
|
-
record(file, changed, write) {
|
|
61
|
-
if (changed && !this.dryRun) {
|
|
62
|
-
const path = join(this.root, file);
|
|
63
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
64
|
-
write(path);
|
|
65
|
-
}
|
|
66
|
-
this.steps.push({ file, outcome: changed ? "wrote" : "unchanged" });
|
|
67
|
-
}
|
|
68
|
-
/** The text `change` gives `file`, from what `file` holds now; written when the two differ. */
|
|
69
|
-
put(file, change) {
|
|
70
|
-
const path = join(this.root, file);
|
|
71
|
-
const current = existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
72
|
-
const next = change(current);
|
|
73
|
-
if ("refusal" in next) {
|
|
74
|
-
this.steps.push({ file, outcome: "refused", detail: next.refusal });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
this.record(file, next.text !== current, (target) => {
|
|
78
|
-
writeFileSync(target, next.text);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
/** `file`, made by `make` when nothing sits at its path, a dangling symlink included; never replaced. */
|
|
82
|
-
create(file, make) {
|
|
83
|
-
this.record(file, lstatSync(join(this.root, file), { throwIfNoEntry: false }) === undefined, make);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
46
|
/** The settings with claudeMdExcludes holding every glob of EXCLUDES, those it lacks appended. */
|
|
87
47
|
function withExcludes(current) {
|
|
88
48
|
if (current === undefined) {
|
|
@@ -157,59 +117,51 @@ function source(root) {
|
|
|
157
117
|
: { text: removeKey(current, "references") }
|
|
158
118
|
};
|
|
159
119
|
}
|
|
160
|
-
/** The repository `dir` sits in, or `dir` itself outside one. */
|
|
161
|
-
function toplevel(dir) {
|
|
162
|
-
const result = exec("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
|
|
163
|
-
return result.status === 0 ? result.stdout.trim() : dir;
|
|
164
|
-
}
|
|
165
120
|
function setup(root, dryRun) {
|
|
166
|
-
const writer = new Writer(
|
|
121
|
+
const writer = new Writer(dryRun);
|
|
122
|
+
/** `file` of the repository, under its own name, as `change` leaves it. */
|
|
123
|
+
const put = (file, change) => {
|
|
124
|
+
writer.apply(file, join(root, file), change);
|
|
125
|
+
};
|
|
167
126
|
const template = readFileSync(TEMPLATE, "utf8");
|
|
168
127
|
const from = source(root);
|
|
169
|
-
|
|
128
|
+
put(ROUTER, (current) => {
|
|
170
129
|
if ("problem" in from) {
|
|
171
130
|
return { refusal: `${from.problem}; the table comes from ${REFERENCES_FILE}, so fix it first` };
|
|
172
131
|
}
|
|
173
132
|
const next = withBlock(current ?? template, renderBlock(from.references ?? []));
|
|
174
133
|
return "problem" in next ? { refusal: next.problem } : next;
|
|
175
134
|
});
|
|
176
|
-
writer.create(ROUTER_LINK, (path) => {
|
|
135
|
+
writer.create(ROUTER_LINK, join(root, ROUTER_LINK), (path) => {
|
|
177
136
|
symlinkSync("AGENTS.md", path);
|
|
178
137
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
138
|
+
put(SETTINGS, withExcludes);
|
|
139
|
+
put(IGNORE, withIgnoreLines);
|
|
140
|
+
put(REFERENCES_FILE, "problem" in from ? (from.file ?? { refusal: from.problem }) : from.file);
|
|
182
141
|
if (!("problem" in from) && from.config !== undefined) {
|
|
183
|
-
|
|
142
|
+
put(CONFIG, from.config);
|
|
184
143
|
}
|
|
185
144
|
return writer.steps;
|
|
186
145
|
}
|
|
187
146
|
export function run(argv) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
}).values;
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
fail(`${messageOf(error)}\n\n${USAGE}`);
|
|
202
|
-
}
|
|
147
|
+
const { values } = readArgs({
|
|
148
|
+
args: argv,
|
|
149
|
+
options: {
|
|
150
|
+
help: { type: "boolean", short: "h" },
|
|
151
|
+
root: { type: "string" },
|
|
152
|
+
"dry-run": { type: "boolean" },
|
|
153
|
+
check: { type: "boolean" }
|
|
154
|
+
}
|
|
155
|
+
}, USAGE);
|
|
203
156
|
if (values.help === true) {
|
|
204
157
|
console.log(USAGE);
|
|
205
158
|
return 0;
|
|
206
159
|
}
|
|
207
160
|
const check = values.check === true;
|
|
208
161
|
const dryRun = values["dry-run"] === true || check;
|
|
209
|
-
const steps = setup(
|
|
210
|
-
for (const
|
|
211
|
-
|
|
212
|
-
console.log(`${verb.padEnd(11)} ${step.file}${step.detail === undefined ? "" : ` — ${step.detail}`}`);
|
|
162
|
+
const steps = setup(repositoryRoot(rootDir(values.root)), dryRun);
|
|
163
|
+
for (const line of stepLines(steps, dryRun)) {
|
|
164
|
+
console.log(line);
|
|
213
165
|
}
|
|
214
166
|
const refused = steps.some((step) => step.outcome === "refused");
|
|
215
167
|
return refused || (check && steps.some((step) => step.outcome === "wrote")) ? 1 : 0;
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { extname, join } from "node:path";
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { repositoryRoot } from "../agentic-config.js";
|
|
8
|
+
import { CannotRun, messageOf, readArgs } from "../cannot-run.js";
|
|
9
|
+
import { SLOW_LIMIT_MS } from "../exec.js";
|
|
10
|
+
import { asked, git } from "../git.js";
|
|
10
11
|
import { readPins, resolveRef } from "../pins.js";
|
|
11
12
|
import { REFERENCES_FILE, readReferences } from "../references-key.js";
|
|
12
13
|
import { renderBlock, withBlock } from "../router-block.js";
|
|
@@ -29,17 +30,10 @@ tag, holding the top-level files and the paths its only names when it has one, w
|
|
|
29
30
|
its skip names. Prints one line per Reference. Exit 1 when any Reference fails, 2 on a bad key,
|
|
30
31
|
config or argument, in which case nothing is synced. One Sync runs in a checkout at a time; a
|
|
31
32
|
second one waits for it up to ${WAIT_SECONDS} s, or REFERENCES_SYNC_WAIT seconds.`;
|
|
32
|
-
function messageOf(error) {
|
|
33
|
-
return error instanceof Error ? error.message : String(error);
|
|
34
|
-
}
|
|
35
33
|
/** The code a failed system call throws with, as "ENOENT", or undefined. */
|
|
36
34
|
function codeOf(error) {
|
|
37
35
|
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
38
36
|
}
|
|
39
|
-
function fail(message) {
|
|
40
|
-
console.error(`references:sync: ${message}`);
|
|
41
|
-
process.exit(2);
|
|
42
|
-
}
|
|
43
37
|
/** REFERENCES_SYNC_WAIT when it names a number of seconds, else WAIT_SECONDS. */
|
|
44
38
|
function waitSeconds() {
|
|
45
39
|
const given = process.env.REFERENCES_SYNC_WAIT?.trim();
|
|
@@ -134,23 +128,13 @@ async function lock(root, seconds) {
|
|
|
134
128
|
rmSync(own, { force: true });
|
|
135
129
|
}
|
|
136
130
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
.map((line) => line.trim())
|
|
145
|
-
.filter(Boolean)
|
|
146
|
-
.join(" ");
|
|
147
|
-
return { ok: result.status === 0, out: (result.stdout ?? "").trim(), err };
|
|
148
|
-
}
|
|
149
|
-
/** Runs one git step of a sync, `gitOptions` ahead of it; throws with git's error when it fails. */
|
|
150
|
-
function step(args, cwd, gitOptions = []) {
|
|
151
|
-
const result = git([...gitOptions, ...args], cwd);
|
|
152
|
-
if (!result.ok) {
|
|
153
|
-
throw new Error(`git ${args.join(" ")}: ${result.err}`);
|
|
131
|
+
/** Every git call of a Sync: one that fetches may take long, and its error stays one line of the report. */
|
|
132
|
+
const SYNC = { limitMs: SLOW_LIMIT_MS, whole: true };
|
|
133
|
+
/** Runs one git step of a sync from `dir`, `gitOptions` ahead of it; throws with git's error when it fails. */
|
|
134
|
+
function step(args, dir, gitOptions = []) {
|
|
135
|
+
const result = git(dir, [...gitOptions, ...args], SYNC);
|
|
136
|
+
if ("error" in result) {
|
|
137
|
+
throw new Error(`git ${args.join(" ")}: ${result.said}`);
|
|
154
138
|
}
|
|
155
139
|
}
|
|
156
140
|
/** A GitHub repository fetches with the GitHub CLI's credentials, so a private one clones too. */
|
|
@@ -170,11 +154,11 @@ function sparsePatterns({ only, skip }) {
|
|
|
170
154
|
const kept = only.length === 0 ? ["/*"] : ["/*", "!/*/", ...only.map((path) => `/${path}`)];
|
|
171
155
|
return [...kept, ...skip.map((path) => `!/${path}`)];
|
|
172
156
|
}
|
|
173
|
-
/** Whether the tree fetched into `
|
|
174
|
-
function fetchedTreeHas(path,
|
|
175
|
-
const listed = git(["ls-tree", "FETCH_HEAD", "--", path.replace(/\/+$/, "")],
|
|
176
|
-
if (
|
|
177
|
-
throw new Error(`git ls-tree FETCH_HEAD: ${listed.
|
|
157
|
+
/** Whether the tree fetched into `dir` holds `path`, a directory when it ends in `/`. Reads trees only. */
|
|
158
|
+
function fetchedTreeHas(path, dir) {
|
|
159
|
+
const listed = git(dir, ["ls-tree", "FETCH_HEAD", "--", path.replace(/\/+$/, "")], SYNC);
|
|
160
|
+
if ("error" in listed) {
|
|
161
|
+
throw new Error(`git ls-tree FETCH_HEAD: ${listed.said}`);
|
|
178
162
|
}
|
|
179
163
|
// `<mode> <type> <object>\t<path>`, or nothing when the tree lacks the path.
|
|
180
164
|
const type = listed.out.split(" ")[1];
|
|
@@ -192,14 +176,14 @@ function sync(root, reference, ref) {
|
|
|
192
176
|
};
|
|
193
177
|
try {
|
|
194
178
|
if (existsSync(target)) {
|
|
195
|
-
const origin =
|
|
196
|
-
if (!existsSync(join(target, ".git")) ||
|
|
179
|
+
const origin = asked(target, ["remote", "get-url", "origin"], SYNC);
|
|
180
|
+
if (!existsSync(join(target, ".git")) || origin !== repository) {
|
|
197
181
|
return `${CLONES}/${dir} exists and is not a clone of ${repository}; move it aside and run again`;
|
|
198
182
|
}
|
|
199
183
|
}
|
|
200
184
|
else {
|
|
201
185
|
// An empty repository, so the one fetch below brings only the ref, a branch or a tag alike.
|
|
202
|
-
step(["init", "--quiet", target]);
|
|
186
|
+
step(["init", "--quiet", target], root);
|
|
203
187
|
step(["remote", "add", "origin", repository], target);
|
|
204
188
|
}
|
|
205
189
|
fetching(["fetch", "--quiet", "--depth", "1", "--filter=blob:none", "origin", ref]);
|
|
@@ -223,17 +207,8 @@ function sync(root, reference, ref) {
|
|
|
223
207
|
return undefined;
|
|
224
208
|
}
|
|
225
209
|
function parseCli(argv) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
args: argv,
|
|
229
|
-
allowPositionals: true,
|
|
230
|
-
options: { help: { type: "boolean", short: "h" } }
|
|
231
|
-
});
|
|
232
|
-
return { help: values.help === true, dirs: positionals };
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
fail(`${messageOf(error)}\n\n${USAGE}`);
|
|
236
|
-
}
|
|
210
|
+
const { values, positionals } = readArgs({ args: argv, allowPositionals: true, options: { help: { type: "boolean", short: "h" } } }, USAGE);
|
|
211
|
+
return { help: values.help === true, dirs: positionals };
|
|
237
212
|
}
|
|
238
213
|
/**
|
|
239
214
|
* One line when the Router or its table is missing or behind `references`, naming the setup to run,
|
|
@@ -277,7 +252,7 @@ function syncAll(root, selected) {
|
|
|
277
252
|
problem = messageOf(error);
|
|
278
253
|
}
|
|
279
254
|
if (problem === undefined) {
|
|
280
|
-
const head =
|
|
255
|
+
const head = asked(join(root, CLONES, reference.dir), ["rev-parse", "--short", "HEAD"], SYNC) ?? "";
|
|
281
256
|
console.log(`${CLONES}/${reference.dir} ${head} ${reference.repository} ${ref}`);
|
|
282
257
|
}
|
|
283
258
|
else {
|
|
@@ -293,26 +268,27 @@ export async function run(argv) {
|
|
|
293
268
|
console.log(USAGE);
|
|
294
269
|
return 0;
|
|
295
270
|
}
|
|
296
|
-
const
|
|
297
|
-
const root = toplevel.ok ? toplevel.out : process.cwd();
|
|
271
|
+
const root = repositoryRoot(process.cwd());
|
|
298
272
|
const reading = readReferences(root);
|
|
299
273
|
if ("problem" in reading) {
|
|
300
|
-
|
|
274
|
+
throw new CannotRun(`${reading.problem}; nothing synced`);
|
|
301
275
|
}
|
|
302
276
|
const references = reading.references ?? [];
|
|
303
277
|
const unknown = cli.dirs.filter((dir) => !references.some((reference) => reference.dir === dir));
|
|
304
278
|
if (unknown.length > 0) {
|
|
305
|
-
|
|
279
|
+
throw new CannotRun(`not in ${REFERENCES_FILE}: ${unknown.join(", ")}`);
|
|
306
280
|
}
|
|
307
281
|
if (reading.references === undefined) {
|
|
308
282
|
return 0;
|
|
309
283
|
}
|
|
310
284
|
const selected = cli.dirs.length === 0 ? references : references.filter((reference) => cli.dirs.includes(reference.dir));
|
|
311
285
|
const seconds = waitSeconds();
|
|
312
|
-
const attempt = await lock(root, seconds).catch((error) =>
|
|
286
|
+
const attempt = await lock(root, seconds).catch((error) => {
|
|
287
|
+
throw new CannotRun(`${CLONES}/${LOCK}: ${messageOf(error)}; nothing synced`);
|
|
288
|
+
});
|
|
313
289
|
let failed = false;
|
|
314
290
|
if ("heldBy" in attempt) {
|
|
315
|
-
console.error(`references
|
|
291
|
+
console.error(`dw-sdlc references sync: another Sync (pid ${attempt.heldBy}) still holds ${CLONES}/${LOCK} after ${seconds} s; the clones are what it leaves`);
|
|
316
292
|
}
|
|
317
293
|
else {
|
|
318
294
|
try {
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { type CommitConvention, type CommitOnProtected, type ForcePush } from "../agentic-config.ts";
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
export type Step = {
|
|
5
|
-
file: string;
|
|
6
|
-
outcome: Outcome;
|
|
7
|
-
detail?: string;
|
|
8
|
-
};
|
|
2
|
+
import { type Step } from "../setup-writer.ts";
|
|
3
|
+
export type { Step };
|
|
9
4
|
export type SetupOptions = {
|
|
10
5
|
root: string;
|
|
11
6
|
home: string;
|