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/sweep-trees.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
// What a Sweep reads of a repository's branches and worktrees: where each worktree lies, what it has checked
|
|
2
|
-
// out, its lock, the files that hold it and the operation paused in it, and the way off the main checkout's
|
|
3
|
-
|
|
2
|
+
// out, its lock, the files that hold it and the operation paused in it, and the way off the main checkout's;
|
|
3
|
+
// beside them, the commands the dev runs for what the Sweep keeps.
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { repositoryFreeEnv } from "./git-env.js";
|
|
6
|
+
import { asked, git, must } from "./git.js";
|
|
7
7
|
import { isLanded } from "./landed.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
import { pausedIn } from "./paused-operation.js";
|
|
9
|
+
import { shellLine } from "./shell-line.js";
|
|
10
|
+
/** The forced delete of `branch`, the dev's to run. */
|
|
11
|
+
export function forcedDelete(branch) {
|
|
12
|
+
return shellLine(["git", "branch", "-D", branch]);
|
|
13
|
+
}
|
|
14
|
+
/** The forced removal of the worktree at `path`, the dev's to run; git wants --force twice for one still locked. */
|
|
15
|
+
export function forcedRemoval(path, locked = false) {
|
|
16
|
+
return shellLine(["git", "worktree", "remove", "--force", ...(locked ? ["--force"] : []), path]);
|
|
17
|
+
}
|
|
18
|
+
/** The unlock of the worktree at `path`, the dev's to run. */
|
|
19
|
+
export function unlockCommand(path) {
|
|
20
|
+
return shellLine(["git", "worktree", "unlock", path]);
|
|
21
|
+
}
|
|
22
|
+
/** The commit the local `branch` points at, or undefined once it is gone. */
|
|
23
|
+
export function tipOf(root, branch) {
|
|
24
|
+
return asked(root, ["rev-parse", "--verify", "-q", `refs/heads/${branch}^{commit}`]);
|
|
18
25
|
}
|
|
19
26
|
/** The local branches of the repository at `root`. */
|
|
20
27
|
export function branchesOf(root) {
|
|
@@ -22,11 +29,11 @@ export function branchesOf(root) {
|
|
|
22
29
|
.split("\n")
|
|
23
30
|
.filter((name) => name !== "");
|
|
24
31
|
}
|
|
25
|
-
/** Every worktree of the repository, the main checkout first. */
|
|
32
|
+
/** Every worktree of the repository, the main checkout first; -z leaves a path or a lock's reason unquoted. */
|
|
26
33
|
export function worktreesOf(root) {
|
|
27
34
|
const trees = [];
|
|
28
35
|
let tree;
|
|
29
|
-
for (const line of must(root, ["worktree", "list", "--porcelain"]).split("\
|
|
36
|
+
for (const line of must(root, ["worktree", "list", "--porcelain", "-z"]).split("\0")) {
|
|
30
37
|
if (line.startsWith("worktree ")) {
|
|
31
38
|
tree = { path: line.slice("worktree ".length), head: "", branch: undefined, lock: undefined, prunable: false };
|
|
32
39
|
trees.push(tree);
|
|
@@ -48,14 +55,12 @@ export function worktreesOf(root) {
|
|
|
48
55
|
}
|
|
49
56
|
/** The files `git status` lists in the worktree at `path`, ignored ones aside, or why it cannot be read. */
|
|
50
57
|
export function dirtyFiles(path) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const said = result.stderr.split("\n").find((line) => line.trim() !== "");
|
|
55
|
-
return { error: `git status: ${said?.trim() ?? `exited ${result.status ?? "on a signal"}`}` };
|
|
58
|
+
const status = git(path, ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { raw: true });
|
|
59
|
+
if ("error" in status) {
|
|
60
|
+
return status;
|
|
56
61
|
}
|
|
57
62
|
const files = [];
|
|
58
|
-
const entries =
|
|
63
|
+
const entries = status.out.split("\0").filter((entry) => entry !== "");
|
|
59
64
|
for (let index = 0; index < entries.length; index++) {
|
|
60
65
|
const entry = entries[index] ?? "";
|
|
61
66
|
files.push(entry.slice(3));
|
|
@@ -65,18 +70,49 @@ export function dirtyFiles(path) {
|
|
|
65
70
|
}
|
|
66
71
|
return files;
|
|
67
72
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
/** What in the worktree at `path` a removal could lose, a paused operation or a file git status lists, or null. */
|
|
74
|
+
export function whyUnclean(path, locked) {
|
|
75
|
+
const operation = pausedOperation(path);
|
|
76
|
+
if (operation !== undefined) {
|
|
77
|
+
return { reason: `a ${operation} is paused in it`, command: null };
|
|
78
|
+
}
|
|
79
|
+
const files = dirtyFiles(path);
|
|
80
|
+
if ("error" in files) {
|
|
81
|
+
return { reason: files.error, command: null };
|
|
82
|
+
}
|
|
83
|
+
return files.length === 0
|
|
84
|
+
? null
|
|
85
|
+
: { reason: `modified or untracked: ${files.join(", ")}`, command: forcedRemoval(path, locked) };
|
|
86
|
+
}
|
|
76
87
|
/** The operation paused in the worktree at `path`: its git dir holds the state git resumes it from. */
|
|
77
88
|
export function pausedOperation(path) {
|
|
78
89
|
const dir = asked(path, ["rev-parse", "--absolute-git-dir"]);
|
|
79
|
-
return dir === undefined ? undefined :
|
|
90
|
+
return dir === undefined ? undefined : pausedIn(dir);
|
|
91
|
+
}
|
|
92
|
+
/** The branch a rebase paused in the worktree at `path` works on; git lists that worktree as detached. */
|
|
93
|
+
function rebasedBranch(path) {
|
|
94
|
+
const dir = asked(path, ["rev-parse", "--absolute-git-dir"]);
|
|
95
|
+
if (dir === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
99
|
+
const file = join(dir, state, "head-name");
|
|
100
|
+
const name = existsSync(file) ? readFileSync(file, "utf8").trim() : "";
|
|
101
|
+
if (name.startsWith("refs/heads/")) {
|
|
102
|
+
return name.slice("refs/heads/".length);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/** The branch git keeps from a delete for the worktree `tree`, and why: checked out there, or a rebase of it paused there. */
|
|
108
|
+
export function heldBranch(tree) {
|
|
109
|
+
if (tree.branch !== undefined) {
|
|
110
|
+
return { branch: tree.branch, reason: `checked out in the worktree at ${tree.path}` };
|
|
111
|
+
}
|
|
112
|
+
const branch = rebasedBranch(tree.path);
|
|
113
|
+
return branch === undefined
|
|
114
|
+
? undefined
|
|
115
|
+
: { branch, reason: `a rebase paused in the worktree at ${tree.path} holds it` };
|
|
80
116
|
}
|
|
81
117
|
/** The dev's way off `branch`, checked out in the main checkout at `root`: null until it is Landed and nothing there could be lost. */
|
|
82
118
|
export function switchOff(root, base, branch) {
|
package/dist/worker/herdr.d.ts
CHANGED
|
@@ -1,24 +1,68 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { type Called } from "../exec.ts";
|
|
2
|
+
/** A terminal as `hc ls --json` lists it. */
|
|
3
|
+
export type Listed = {
|
|
4
|
+
address: string;
|
|
5
|
+
/** Its name, else its address. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The terminal that started it. */
|
|
8
|
+
parent: string | undefined;
|
|
9
|
+
/** herdr's state: live or finished. */
|
|
10
|
+
state: string;
|
|
11
|
+
/** The word it reports, undefined before it reports one. */
|
|
12
|
+
status: string | undefined;
|
|
13
|
+
/** The folder it was launched in. */
|
|
14
|
+
folder: string;
|
|
15
|
+
/** The place herdr shows it at: its space and tab. */
|
|
16
|
+
place: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** A terminal as `hc get --json` reports it. */
|
|
19
|
+
export type Reading = {
|
|
20
|
+
/** herdr's state: live or finished. */
|
|
21
|
+
state: string | undefined;
|
|
22
|
+
/** The state its program reports. */
|
|
23
|
+
status: string | undefined;
|
|
24
|
+
/** The herdr space it sits in, its `herdr.space` label. */
|
|
25
|
+
space: string | undefined;
|
|
26
|
+
/** Whether herdr sent its first message, and why not when it did not. */
|
|
27
|
+
firstMessage: {
|
|
28
|
+
sent: boolean;
|
|
29
|
+
reason: string | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
/** The folder its foreground program works in. */
|
|
32
|
+
folder: string | undefined;
|
|
33
|
+
/** That program's pid. */
|
|
34
|
+
pid: number | undefined;
|
|
35
|
+
};
|
|
36
|
+
/** `herdr-app args`, the window's CLI; one not on PATH, or past its limit, stops the command. */
|
|
37
|
+
export declare function herdrApp(args: string[]): Called;
|
|
38
|
+
/** The terminals `hc ls` lists: the live ones, every one with `all`, only those labelled `label` when given. */
|
|
39
|
+
export declare function listTerminals(filter?: {
|
|
40
|
+
all?: boolean;
|
|
41
|
+
label?: string;
|
|
42
|
+
}): Listed[];
|
|
43
|
+
/** What `hc get` reports of the terminal at `address`, or `gone`, in hc's words, when herdr has no such terminal. */
|
|
44
|
+
export declare function readTerminal(address: string): Reading | {
|
|
45
|
+
gone: string;
|
|
46
|
+
};
|
|
47
|
+
export type Start = {
|
|
48
|
+
name: string;
|
|
49
|
+
/** Its labels, each passed as `--label key=value` in order. */
|
|
50
|
+
labels: Record<string, string>;
|
|
51
|
+
/** The folder it starts in. */
|
|
52
|
+
cwd: string;
|
|
53
|
+
/** The program and its arguments. */
|
|
54
|
+
argv: string[];
|
|
55
|
+
};
|
|
56
|
+
/** Runs `argv` in a terminal of its own in the background, outliving its caller; the address hc gives it. */
|
|
57
|
+
export declare function startTerminal({ name, labels, cwd, argv }: Start): string;
|
|
58
|
+
/** Stops the terminal at `address`: hc's words when it refuses, undefined once it is stopped or had finished. */
|
|
59
|
+
export declare function stopTerminal(address: string): string | undefined;
|
|
60
|
+
/** Removes the terminal at `address`: hc's words when it refuses, undefined once it is gone. */
|
|
61
|
+
export declare function removeTerminal(address: string): string | undefined;
|
|
62
|
+
/** Types `message` into the terminal at `address`: `sent`, `blocked` while it sits on a dialog, else hc's words. */
|
|
63
|
+
export declare function sendTo(address: string, message: string): "sent" | "blocked" | {
|
|
64
|
+
refused: string;
|
|
11
65
|
};
|
|
12
|
-
/** The first line of a tool's output that says anything, or the exit it ended with. */
|
|
13
|
-
export declare function said(called: Called): string;
|
|
14
|
-
/** `tool args` as a child process; a tool that is not on PATH stops the command. */
|
|
15
|
-
export declare function call(tool: "hc" | "herdr-app", args: string[]): Called;
|
|
16
|
-
/** The JSON object `hc <args>` prints; a failing call or a reply that is no object stops the command. */
|
|
17
|
-
export declare function hcObject(args: string[]): JsonObject;
|
|
18
|
-
/** What an error says, whatever was thrown. */
|
|
19
|
-
export declare function describe(error: unknown): string;
|
|
20
|
-
/** Reports a `Refusal` of `command` on stderr and answers exit 2; any other error is a bug and goes on up. */
|
|
21
|
-
export declare function refuse(command: string, error: unknown): number;
|
|
22
66
|
/** The option `flag` as a whole number from `min`, `fallback` when the command line leaves it out. */
|
|
23
67
|
export declare function wholeNumber(flag: string, value: string | undefined, fallback: number, min: number): number | {
|
|
24
68
|
error: string;
|
|
@@ -38,11 +82,11 @@ export type Worker = {
|
|
|
38
82
|
/** Whether herdr still runs the Worker's terminal; the one test of it every command shares. */
|
|
39
83
|
export declare function isLive(worker: Worker): boolean;
|
|
40
84
|
/**
|
|
41
|
-
* The Workers with the folder each live one works in, its worktree, which `hc get` reports
|
|
42
|
-
*
|
|
85
|
+
* The Workers with the folder each live one works in, its worktree, which `hc get` reports and `hc ls`
|
|
86
|
+
* leaves out. A Worker herdr cannot tell it for keeps its launch folder.
|
|
43
87
|
*/
|
|
44
88
|
export declare function workingFolders(workers: Worker[]): Worker[];
|
|
45
|
-
/** The Workers `
|
|
89
|
+
/** The Workers `leadId` started, in the order herdr lists them; `all` adds the finished ones. */
|
|
46
90
|
export declare function workersOf(leadId: string, all: boolean): Worker[];
|
|
47
91
|
/** A table of rows, every column but the last padded to its widest cell and two spaces apart. */
|
|
48
92
|
export declare function table(rows: string[][]): string;
|
package/dist/worker/herdr.js
CHANGED
|
@@ -1,66 +1,126 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// herdr's CLI, `hc`, run from this one place, each reply parsed once into a typed value: a read that fails
|
|
2
|
+
// stops the command through `CannotRun` with hc's words, an action answers its refusal. Beside it:
|
|
3
|
+
// `herdr-app`, the Lead a command acts for, and the Workers herdr lists for it.
|
|
4
|
+
import { CannotRun } from "../cannot-run.js";
|
|
5
|
+
import { need, whyFailed } from "../exec.js";
|
|
4
6
|
import { isObject, parseObject } from "../json-text.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const REMEDIES = {
|
|
8
|
+
missing: "install herdr, or run the command in a terminal herdr started",
|
|
9
|
+
stalled: "check herdr is responding, then call again"
|
|
10
|
+
};
|
|
11
|
+
/** The remedy for a reply in a shape this module does not read. */
|
|
12
|
+
const SHAPE = "check hc --version";
|
|
13
|
+
/** hc's exit when it has no such terminal. */
|
|
14
|
+
const NO_TERMINAL = 3;
|
|
15
|
+
/** `hc send`'s exit when the target sits on a permission or a question. */
|
|
16
|
+
const BLOCKED = 9;
|
|
17
|
+
/** `hc kill`'s exit when the terminal finished before the stop. */
|
|
18
|
+
const FINISHED = 11;
|
|
19
|
+
/** `hc args`; one not on PATH, or past its limit, stops the command. */
|
|
20
|
+
function hc(args) {
|
|
21
|
+
return need("hc", args, REMEDIES);
|
|
12
22
|
}
|
|
13
|
-
/**
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
return line?.trim() ?? `exited ${called.status}`;
|
|
23
|
+
/** `herdr-app args`, the window's CLI; one not on PATH, or past its limit, stops the command. */
|
|
24
|
+
export function herdrApp(args) {
|
|
25
|
+
return need("herdr-app", args, REMEDIES);
|
|
17
26
|
}
|
|
18
|
-
/** `
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
/** The JSON object `hc <args>` prints, or why it printed none; `shown` names the call, as `hc get t3`. */
|
|
28
|
+
function answer(args, shown) {
|
|
29
|
+
const called = hc(args);
|
|
30
|
+
if (called.status !== 0) {
|
|
31
|
+
return { status: called.status, problem: `${shown} failed: ${whyFailed(called)}` };
|
|
22
32
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
33
|
+
const object = parseObject(called.stdout);
|
|
34
|
+
return object === undefined
|
|
35
|
+
? { status: 0, problem: `${shown} answered something that is no JSON object`, remedy: SHAPE }
|
|
36
|
+
: { object };
|
|
37
|
+
}
|
|
38
|
+
/** The object `answered` holds; a call that answered none stops the command. */
|
|
39
|
+
function objectOf(answered) {
|
|
40
|
+
if ("object" in answered) {
|
|
41
|
+
return answered.object;
|
|
28
42
|
}
|
|
43
|
+
throw new CannotRun(answered.problem, answered.remedy);
|
|
29
44
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
throw
|
|
45
|
+
/** A string that says something, else undefined. */
|
|
46
|
+
function word(value) {
|
|
47
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
function listedOf(entry) {
|
|
50
|
+
const address = isObject(entry) ? word(entry.address) : undefined;
|
|
51
|
+
if (!isObject(entry) || address === undefined || typeof entry.state !== "string" || typeof entry.cwd !== "string") {
|
|
52
|
+
throw new CannotRun("hc ls answered a terminal without an address, state or folder", SHAPE);
|
|
38
53
|
}
|
|
39
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
address,
|
|
56
|
+
name: word(entry.name) ?? address,
|
|
57
|
+
parent: word(entry.parent),
|
|
58
|
+
state: entry.state,
|
|
59
|
+
status: word(entry.status),
|
|
60
|
+
folder: entry.cwd,
|
|
61
|
+
place: word(entry.where)
|
|
62
|
+
};
|
|
40
63
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
function readingOf({ state, status, labels, first_message: first, foreground }) {
|
|
65
|
+
const program = isObject(foreground) ? foreground : {};
|
|
66
|
+
return {
|
|
67
|
+
state: word(state),
|
|
68
|
+
status: isObject(status) ? word(status.state) : undefined,
|
|
69
|
+
space: isObject(labels) ? word(labels["herdr.space"]) : undefined,
|
|
70
|
+
firstMessage: isObject(first) && typeof first.sent === "boolean" ? { sent: first.sent, reason: word(first.reason) } : undefined,
|
|
71
|
+
folder: word(program.cwd),
|
|
72
|
+
pid: typeof program.pid === "number" ? program.pid : undefined
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** The terminals `hc ls` lists: the live ones, every one with `all`, only those labelled `label` when given. */
|
|
76
|
+
export function listTerminals(filter = {}) {
|
|
77
|
+
const all = filter.all === true ? ["--all"] : [];
|
|
78
|
+
const label = filter.label === undefined ? [] : ["--label", filter.label];
|
|
79
|
+
const { terminals } = objectOf(answer(["ls", "--json", ...all, ...label], "hc ls"));
|
|
80
|
+
if (!Array.isArray(terminals)) {
|
|
81
|
+
throw new CannotRun("hc ls answered no list of terminals", SHAPE);
|
|
46
82
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
return terminals.map(listedOf);
|
|
84
|
+
}
|
|
85
|
+
/** What `hc get` reports of the terminal at `address`, or `gone`, in hc's words, when herdr has no such terminal. */
|
|
86
|
+
export function readTerminal(address) {
|
|
87
|
+
const answered = answer(["get", address, "--json"], `hc get ${address}`);
|
|
88
|
+
if ("status" in answered && answered.status === NO_TERMINAL) {
|
|
89
|
+
return { gone: answered.problem };
|
|
50
90
|
}
|
|
51
|
-
return
|
|
91
|
+
return readingOf(objectOf(answered));
|
|
52
92
|
}
|
|
53
|
-
/**
|
|
54
|
-
export function
|
|
55
|
-
|
|
93
|
+
/** Runs `argv` in a terminal of its own in the background, outliving its caller; the address hc gives it. */
|
|
94
|
+
export function startTerminal({ name, labels, cwd, argv }) {
|
|
95
|
+
const label = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
96
|
+
const args = ["run", "--bg", "--json", "--name", name, ...label, "--outlive-parent", "--cwd", cwd, "--", ...argv];
|
|
97
|
+
const address = word(objectOf(answer(args, "hc run")).address);
|
|
98
|
+
if (address === undefined) {
|
|
99
|
+
throw new CannotRun("hc run answered no address for the terminal it started", "check hc ls before starting it again");
|
|
100
|
+
}
|
|
101
|
+
return address;
|
|
102
|
+
}
|
|
103
|
+
/** Stops the terminal at `address`: hc's words when it refuses, undefined once it is stopped or had finished. */
|
|
104
|
+
export function stopTerminal(address) {
|
|
105
|
+
const called = hc(["kill", address]);
|
|
106
|
+
return called.status === 0 || called.status === FINISHED
|
|
107
|
+
? undefined
|
|
108
|
+
: `hc kill ${address} failed: ${whyFailed(called)}`;
|
|
109
|
+
}
|
|
110
|
+
/** Removes the terminal at `address`: hc's words when it refuses, undefined once it is gone. */
|
|
111
|
+
export function removeTerminal(address) {
|
|
112
|
+
const called = hc(["rm", address]);
|
|
113
|
+
return called.status === 0 || called.status === NO_TERMINAL
|
|
114
|
+
? undefined
|
|
115
|
+
: `hc rm ${address} failed: ${whyFailed(called)}`;
|
|
56
116
|
}
|
|
57
|
-
/**
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
117
|
+
/** Types `message` into the terminal at `address`: `sent`, `blocked` while it sits on a dialog, else hc's words. */
|
|
118
|
+
export function sendTo(address, message) {
|
|
119
|
+
const called = hc(["send", address, message]);
|
|
120
|
+
if (called.status === 0) {
|
|
121
|
+
return "sent";
|
|
61
122
|
}
|
|
62
|
-
|
|
63
|
-
return 2;
|
|
123
|
+
return called.status === BLOCKED ? "blocked" : { refused: `hc send failed: ${whyFailed(called)}` };
|
|
64
124
|
}
|
|
65
125
|
/** The option `flag` as a whole number from `min`, `fallback` when the command line leaves it out. */
|
|
66
126
|
export function wholeNumber(flag, value, fallback, min) {
|
|
@@ -73,7 +133,7 @@ export function wholeNumber(flag, value, fallback, min) {
|
|
|
73
133
|
export function lead() {
|
|
74
134
|
const id = process.env.HC_TERMINAL_ID;
|
|
75
135
|
if (id === undefined || id === "") {
|
|
76
|
-
throw new
|
|
136
|
+
throw new CannotRun("outside herdr: $HC_TERMINAL_ID is not set", "run it in a terminal herdr started, where the Lead has an address");
|
|
77
137
|
}
|
|
78
138
|
return id;
|
|
79
139
|
}
|
|
@@ -82,41 +142,30 @@ export function isLive(worker) {
|
|
|
82
142
|
return worker.state === "live";
|
|
83
143
|
}
|
|
84
144
|
/**
|
|
85
|
-
* The Workers with the folder each live one works in, its worktree, which `hc get` reports
|
|
86
|
-
*
|
|
145
|
+
* The Workers with the folder each live one works in, its worktree, which `hc get` reports and `hc ls`
|
|
146
|
+
* leaves out. A Worker herdr cannot tell it for keeps its launch folder.
|
|
87
147
|
*/
|
|
88
148
|
export function workingFolders(workers) {
|
|
89
149
|
return workers.map((worker) => {
|
|
90
150
|
if (!isLive(worker)) {
|
|
91
151
|
return worker;
|
|
92
152
|
}
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
return typeof cwd === "string" && cwd !== "" ? { ...worker, folder: cwd } : worker;
|
|
153
|
+
const answered = answer(["get", worker.address, "--json"], `hc get ${worker.address}`);
|
|
154
|
+
const folder = "object" in answered ? readingOf(answered.object).folder : undefined;
|
|
155
|
+
return folder === undefined ? worker : { ...worker, folder };
|
|
97
156
|
});
|
|
98
157
|
}
|
|
99
|
-
|
|
100
|
-
return typeof value === "string" ? value : undefined;
|
|
101
|
-
}
|
|
102
|
-
/** The Workers `lead` started, in the order herdr lists them; `all` adds the finished ones. */
|
|
158
|
+
/** The Workers `leadId` started, in the order herdr lists them; `all` adds the finished ones. */
|
|
103
159
|
export function workersOf(leadId, all) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const { address, state, cwd } = entry;
|
|
114
|
-
if (typeof address !== "string" || typeof state !== "string" || typeof cwd !== "string") {
|
|
115
|
-
throw new Refusal("hc ls answered a terminal without an address, state or folder", "check hc --version");
|
|
116
|
-
}
|
|
117
|
-
const status = state === "finished" ? "finished" : (word(entry.status) ?? "unknown");
|
|
118
|
-
return [{ name: word(entry.name) ?? address, address, state, status, folder: cwd }];
|
|
119
|
-
});
|
|
160
|
+
return listTerminals({ all, label: "role=worker" })
|
|
161
|
+
.filter(({ parent }) => parent === leadId)
|
|
162
|
+
.map(({ name, address, state, status, folder }) => ({
|
|
163
|
+
name,
|
|
164
|
+
address,
|
|
165
|
+
state,
|
|
166
|
+
status: state === "finished" ? "finished" : (status ?? "unknown"),
|
|
167
|
+
folder
|
|
168
|
+
}));
|
|
120
169
|
}
|
|
121
170
|
/** A table of rows, every column but the last padded to its widest cell and two spaces apart. */
|
|
122
171
|
export function table(rows) {
|
package/dist/worker/launch.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// What `worker start` and `lead start` share: a session started in a herdr terminal of its own beside the
|
|
2
2
|
// caller, its tab named, and the terminal read back until the session shows it took its first message.
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { CannotRun } from "../cannot-run.js";
|
|
5
|
+
import { readTerminal, startTerminal } from "./herdr.js";
|
|
6
6
|
import { nameTab } from "./tabs.js";
|
|
7
7
|
/** hc names a terminal with 1-32 of a-z, 0-9, - and _. */
|
|
8
8
|
export const NAME = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
@@ -12,42 +12,43 @@ export const TAB_WAIT = 10;
|
|
|
12
12
|
const WORKING = new Set(["working", "blocked", "done"]);
|
|
13
13
|
/** How long the read-back waits between two reads of the terminal. */
|
|
14
14
|
const POLL_MS = 1000;
|
|
15
|
-
/** The status a session reports, if it has reported one. */
|
|
16
|
-
function reportedStatus(terminal) {
|
|
17
|
-
const { status } = terminal;
|
|
18
|
-
return isObject(status) && typeof status.state === "string" ? status.state : undefined;
|
|
19
|
-
}
|
|
20
15
|
/** Why the session, called `who`, can no longer take its first message, or undefined while it still may. */
|
|
21
|
-
function failure(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return typeof first.reason === "string" ? first.reason : "herdr gave no reason";
|
|
16
|
+
function failure({ firstMessage, state, status }, who) {
|
|
17
|
+
if (firstMessage?.sent === false) {
|
|
18
|
+
return firstMessage.reason ?? "herdr gave no reason";
|
|
25
19
|
}
|
|
26
20
|
if (state !== "live") {
|
|
27
21
|
return "the terminal has finished";
|
|
28
22
|
}
|
|
29
|
-
return
|
|
23
|
+
return status === "error" ? `the ${who} reports its status as error` : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The terminal at `address` as `hc get` reports it, or why it could not be read. */
|
|
26
|
+
function readBack(address) {
|
|
27
|
+
try {
|
|
28
|
+
const reading = readTerminal(address);
|
|
29
|
+
return "gone" in reading ? { unread: reading.gone } : reading;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof CannotRun) {
|
|
33
|
+
return { unread: error.message };
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
/** Reads the terminal until the session shows work, for up to `wait` seconds; why it did not, or undefined once it did. */
|
|
32
39
|
async function delivery(address, wait, who) {
|
|
33
40
|
const deadline = Date.now() + wait * 1000;
|
|
34
41
|
for (;;) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
catch (error) {
|
|
40
|
-
if (!(error instanceof Refusal)) {
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
// The session is running by now, so a failed read is a message not confirmed, never a start that did not happen.
|
|
44
|
-
return `the terminal could not be read back: ${describe(error)}`;
|
|
42
|
+
const reading = readBack(address);
|
|
43
|
+
// The session is running by now, so a failed read is a message not confirmed, never a start that did not happen.
|
|
44
|
+
if ("unread" in reading) {
|
|
45
|
+
return `the terminal could not be read back: ${reading.unread}`;
|
|
45
46
|
}
|
|
46
|
-
const failed = failure(
|
|
47
|
+
const failed = failure(reading, who);
|
|
47
48
|
if (failed !== undefined) {
|
|
48
49
|
return failed;
|
|
49
50
|
}
|
|
50
|
-
if (WORKING.has(
|
|
51
|
+
if (WORKING.has(reading.status ?? "")) {
|
|
51
52
|
return undefined;
|
|
52
53
|
}
|
|
53
54
|
if (Date.now() >= deadline) {
|
|
@@ -58,33 +59,19 @@ async function delivery(address, wait, who) {
|
|
|
58
59
|
}
|
|
59
60
|
/** The id of the herdr space the terminal `id` sits in, if it sits in one. */
|
|
60
61
|
export function spaceOf(id) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const reading = readTerminal(id);
|
|
63
|
+
if ("gone" in reading) {
|
|
64
|
+
throw new CannotRun(reading.gone);
|
|
65
|
+
}
|
|
66
|
+
return reading.space;
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
66
69
|
* Runs `hc run` once for `launch`, outliving its caller, then names the tab and reads the terminal back side
|
|
67
70
|
* by side. It never sends the first message again; `reason` says why the session did not take it.
|
|
68
71
|
*/
|
|
69
72
|
export async function launch(command, { name, role, space, cwd, argv, wait }) {
|
|
70
|
-
const {
|
|
71
|
-
|
|
72
|
-
"--bg",
|
|
73
|
-
"--json",
|
|
74
|
-
"--name",
|
|
75
|
-
name,
|
|
76
|
-
"--label",
|
|
77
|
-
`role=${role}`,
|
|
78
|
-
...(space === undefined ? [] : ["--label", `herdr.space=${space}`]),
|
|
79
|
-
"--outlive-parent",
|
|
80
|
-
"--cwd",
|
|
81
|
-
cwd,
|
|
82
|
-
"--",
|
|
83
|
-
...argv
|
|
84
|
-
]);
|
|
85
|
-
if (typeof address !== "string") {
|
|
86
|
-
throw new Refusal("hc run answered no address for the terminal it started", "check hc ls before starting it again");
|
|
87
|
-
}
|
|
73
|
+
const labels = { role, ...(space === undefined ? {} : { "herdr.space": space }) };
|
|
74
|
+
const address = startTerminal({ name, labels, cwd, argv });
|
|
88
75
|
// Side by side, so the wait for the tab overlaps the wait for the session to start.
|
|
89
76
|
const naming = space === undefined ? Promise.resolve("none") : nameTab(address, name, Math.min(TAB_WAIT, wait));
|
|
90
77
|
const [named, reason] = await Promise.all([naming, delivery(address, wait, role === "lead" ? "Lead" : "Worker")]);
|
package/dist/worker/origin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetches origin's default branch, or every branch of origin when no default is known, into the remote-tracking
|
|
3
|
-
* refs at `root`; a clone with no origin has nothing to fetch and is left alone. Throws
|
|
3
|
+
* refs at `root`; a clone with no origin has nothing to fetch and is left alone. Throws `CannotRun` when the
|
|
4
4
|
* fetch fails, since a Worker started on a stale base is the fault this prevents.
|
|
5
5
|
*/
|
|
6
6
|
export declare function freshenOrigin(root: string, baseBranch: string): void;
|