autonomous-sdlc-harness 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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/user_reviews/README.md +9 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's one way to ask a question.
|
|
3
|
+
*
|
|
4
|
+
* **The rule, stated here and in `docs/cli.md` §2 and nowhere else.** Every interactive
|
|
5
|
+
* decision has three parts:
|
|
6
|
+
*
|
|
7
|
+
* 1. **a flag that supplies the answer**, which is the actual interface;
|
|
8
|
+
* 2. **a documented non-interactive default**, which is exactly the behaviour the release
|
|
9
|
+
* already had before the decision became askable;
|
|
10
|
+
* 3. **a prompt**, shown only when the flag was absent *and* both stdin and stdout are a TTY.
|
|
11
|
+
*
|
|
12
|
+
* **A run that cannot be prompted never blocks.** It takes the default. Which of the two
|
|
13
|
+
* reasons it could not be prompted decides who says so: with no terminal, this module prints
|
|
14
|
+
* one line naming the flag that chooses otherwise; under `--non-interactive` or `--quiet` —
|
|
15
|
+
* which force that same default-taking path even on a terminal, for a CI runner that allocates
|
|
16
|
+
* one — it returns the default **silently**, and a caller that needs the decision narrated
|
|
17
|
+
* prints its own note (`commands/init.ts`'s analyze offer and `qa.driver` do).
|
|
18
|
+
*
|
|
19
|
+
* That is what keeps `init` deterministic: with no terminal, its output is a function of its
|
|
20
|
+
* flags and the repository alone, so a second `init` over the same repository produces the same
|
|
21
|
+
* result and the recorded preset stays re-derivable from the detection table by hand. No prompt
|
|
22
|
+
* may exist without its flag and its default — a prompt whose answer has no flag would make an
|
|
23
|
+
* unattended run's result unreachable rather than merely defaulted.
|
|
24
|
+
*
|
|
25
|
+
* **Every question this module prints is preceded by one blank line**, so no caller adds its own:
|
|
26
|
+
* the interactive prompt, each re-ask, and the no-terminal note that stands in for a question that
|
|
27
|
+
* could not be put. A question printed hard against the narration above it reads as one more
|
|
28
|
+
* statement of fact rather than as something to answer.
|
|
29
|
+
*
|
|
30
|
+
* Two implementation constraints, both inherited:
|
|
31
|
+
*
|
|
32
|
+
* - **Nothing here writes through `console`.** The question goes out through the `Reporter`
|
|
33
|
+
* like every other line the CLI prints, so `--quiet` behaves the same here as everywhere.
|
|
34
|
+
* The reporter is line-oriented, so the answer is typed on the line *below* the question
|
|
35
|
+
* rather than after it — worth the plain, diffable output.
|
|
36
|
+
* - **No runtime dependency.** The package declares none and one line of input is not worth
|
|
37
|
+
* the first, so the answer is read from fd 0 with `node:fs` and decoded once at the end.
|
|
38
|
+
*/
|
|
39
|
+
import { readSync } from 'node:fs';
|
|
40
|
+
import { isatty } from 'node:tty';
|
|
41
|
+
/** Standard input. Read directly rather than through `process.stdin`, which is asynchronous. */
|
|
42
|
+
const STDIN_FD = 0;
|
|
43
|
+
/** Standard output, named as a descriptor for the same reason: the probe below must not open a stream. */
|
|
44
|
+
const STDOUT_FD = 1;
|
|
45
|
+
/** How many times an unrecognised yes/no answer is re-asked before the default is taken. */
|
|
46
|
+
const REPROMPT_LIMIT = 2;
|
|
47
|
+
/** How long, and how many times, an `EAGAIN` read is retried before input is given up on. */
|
|
48
|
+
const EAGAIN_RETRY_LIMIT = 100;
|
|
49
|
+
const EAGAIN_RETRY_MS = 10;
|
|
50
|
+
/**
|
|
51
|
+
* True when a question can actually be put to someone: a terminal on **both** ends. stdout
|
|
52
|
+
* alone is not enough (the question would print into a pipe with nobody to answer it) and
|
|
53
|
+
* stdin alone is not enough (the answer would be typed against an invisible prompt).
|
|
54
|
+
*
|
|
55
|
+
* **Asked of the descriptors rather than of `process.stdin.isTTY`, and that is load-bearing
|
|
56
|
+
* rather than stylistic.** `process.stdin` is a lazy getter: the first access constructs a
|
|
57
|
+
* `tty.ReadStream` on fd 0, which on POSIX leaves that descriptor **non-blocking**. The very
|
|
58
|
+
* next thing this module does with fd 0 is {@link readLineSync}'s `readSync`, which then
|
|
59
|
+
* throws `EAGAIN` on a terminal nobody has typed into yet — so the probe would manufacture
|
|
60
|
+
* exactly the condition the retry loop below exists to absorb, and every prompt would take
|
|
61
|
+
* its default after a second of retries. `isatty` asks libuv what the descriptor is and
|
|
62
|
+
* changes nothing about it.
|
|
63
|
+
*/
|
|
64
|
+
function isInteractiveTerminal() {
|
|
65
|
+
return isatty(STDIN_FD) && isatty(STDOUT_FD);
|
|
66
|
+
}
|
|
67
|
+
/** Sleep without a timer, so the read loop below can stay synchronous. */
|
|
68
|
+
function sleepSync(milliseconds) {
|
|
69
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read one line from fd 0, a byte at a time, and decode it once. Returns `undefined` at end of
|
|
73
|
+
* input or on a read error — both mean no answer is coming, so the caller takes the default
|
|
74
|
+
* rather than re-asking into a closed stream.
|
|
75
|
+
*/
|
|
76
|
+
function readLineSync() {
|
|
77
|
+
const byte = Buffer.alloc(1);
|
|
78
|
+
const bytes = [];
|
|
79
|
+
let eagainRetries = 0;
|
|
80
|
+
for (;;) {
|
|
81
|
+
let read;
|
|
82
|
+
try {
|
|
83
|
+
read = readSync(STDIN_FD, byte, 0, 1, null);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// A terminal left in non-blocking mode by the parent process has nothing to read *yet*,
|
|
87
|
+
// which is not the same as having nothing to read. Retry briefly, then give up. The
|
|
88
|
+
// parent is the only admissible cause: nothing in this module may touch fd 0 through
|
|
89
|
+
// `process.stdin`, which would make this process the cause and this loop a mask for it.
|
|
90
|
+
if (error.code === 'EAGAIN' && eagainRetries < EAGAIN_RETRY_LIMIT) {
|
|
91
|
+
eagainRetries += 1;
|
|
92
|
+
sleepSync(EAGAIN_RETRY_MS);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (read === 0)
|
|
98
|
+
return bytes.length === 0 ? undefined : Buffer.from(bytes).toString('utf8');
|
|
99
|
+
const value = byte[0];
|
|
100
|
+
if (value === 0x0a)
|
|
101
|
+
return Buffer.from(bytes).toString('utf8');
|
|
102
|
+
bytes.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Why this run cannot prompt, or `undefined` when it can. One place decides it, so `askYesNo`
|
|
107
|
+
* and `askLine` cannot answer the question differently.
|
|
108
|
+
*
|
|
109
|
+
* `--quiet` counts: it suppresses the stdout side of the reporter, so a prompt issued under it
|
|
110
|
+
* would block on a question nobody was shown. Neither flag prints the no-terminal note: under
|
|
111
|
+
* `--quiet` it would be suppressed with the rest of the narration anyway, and under
|
|
112
|
+
* `--non-interactive` the default-taking path is what was asked for, so a caller that needs it
|
|
113
|
+
* narrated owns the line.
|
|
114
|
+
*/
|
|
115
|
+
function nonInteractiveReason(ctx) {
|
|
116
|
+
if (ctx.flags.nonInteractive || ctx.report.quiet)
|
|
117
|
+
return 'flag';
|
|
118
|
+
if (!isInteractiveTerminal())
|
|
119
|
+
return 'no-terminal';
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Can this run actually put a question to someone?
|
|
124
|
+
*
|
|
125
|
+
* The same decision {@link askYesNo} and {@link askLine} take, exported for a caller that has to
|
|
126
|
+
* **narrate** the two paths differently rather than only answer differently — `init`'s `qa.driver`,
|
|
127
|
+
* whose note either records what was chosen or says a run that could not be asked took the
|
|
128
|
+
* documented default and names the flag that chooses otherwise. Exported as this predicate rather
|
|
129
|
+
* than re-derived from the descriptor probe at the call site, so `--non-interactive` and `--quiet`
|
|
130
|
+
* keep one decider: a second one could disagree, and the disagreement would be a prompt issued
|
|
131
|
+
* under `--quiet` blocking on a question nobody was shown.
|
|
132
|
+
*/
|
|
133
|
+
export function canPrompt(ctx) {
|
|
134
|
+
return nonInteractiveReason(ctx) === undefined;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The blank line before a question, emitted through the same `info` the question itself goes out on
|
|
138
|
+
* — so `--quiet` suppresses the separator with the line it separates and the reporter's contract is
|
|
139
|
+
* unchanged. Under `'flag'` neither is printed and neither is this.
|
|
140
|
+
*
|
|
141
|
+
* **Unconditional.** This module holds no cross-call state and the reporter exposes none, so the one
|
|
142
|
+
* question that can be a run's first printed line (`init`'s git-init offer, asked before anything
|
|
143
|
+
* else prints) costs a leading blank line — cheaper than a state flag on the reporter.
|
|
144
|
+
*/
|
|
145
|
+
function separate(ctx) {
|
|
146
|
+
ctx.report.info('');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Ask a yes/no question. Returns `options.defaultAnswer` on every path that cannot prompt, and
|
|
150
|
+
* on a terminal maps `y`/`yes` and `n`/`no` (either case), an empty line to the default, and
|
|
151
|
+
* anything else to a re-ask — {@link REPROMPT_LIMIT} times, then the default.
|
|
152
|
+
*/
|
|
153
|
+
export function askYesNo(options, ctx) {
|
|
154
|
+
const reason = nonInteractiveReason(ctx);
|
|
155
|
+
if (reason === 'flag')
|
|
156
|
+
return options.defaultAnswer;
|
|
157
|
+
if (reason === 'no-terminal') {
|
|
158
|
+
separate(ctx);
|
|
159
|
+
ctx.report.info(`${options.question} — no terminal to ask on, so the default stands: ` +
|
|
160
|
+
`${options.defaultAnswer ? 'yes' : 'no'}. Pass ${options.flag} ${options.flagHint}.`);
|
|
161
|
+
return options.defaultAnswer;
|
|
162
|
+
}
|
|
163
|
+
const suffix = options.defaultAnswer ? '[Y/n]' : '[y/N]';
|
|
164
|
+
for (let attempt = 0; attempt <= REPROMPT_LIMIT; attempt += 1) {
|
|
165
|
+
// Inside the loop: a re-ask is a fresh question, printed under the answer that did not parse.
|
|
166
|
+
separate(ctx);
|
|
167
|
+
ctx.report.info(`${options.question} ${suffix}`);
|
|
168
|
+
const answer = readLineSync();
|
|
169
|
+
if (answer === undefined)
|
|
170
|
+
return options.defaultAnswer;
|
|
171
|
+
const normalised = answer.trim().toLowerCase();
|
|
172
|
+
if (normalised === '')
|
|
173
|
+
return options.defaultAnswer;
|
|
174
|
+
if (normalised === 'y' || normalised === 'yes')
|
|
175
|
+
return true;
|
|
176
|
+
if (normalised === 'n' || normalised === 'no')
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
ctx.report.info(`No yes-or-no answer given, so the default stands: ${options.defaultAnswer ? 'yes' : 'no'}.`);
|
|
180
|
+
return options.defaultAnswer;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Ask for a value. Returns `options.defaultValue` — `undefined` when there is none — on every
|
|
184
|
+
* path that cannot prompt and on an empty line. The answer is trimmed and otherwise returned
|
|
185
|
+
* as typed: what counts as a valid value is the caller's to decide and to report.
|
|
186
|
+
*/
|
|
187
|
+
export function askLine(options, ctx) {
|
|
188
|
+
const reason = nonInteractiveReason(ctx);
|
|
189
|
+
if (reason === 'flag')
|
|
190
|
+
return options.defaultValue;
|
|
191
|
+
if (reason === 'no-terminal') {
|
|
192
|
+
const outcome = options.defaultValue === undefined ? 'nothing is set' : `the default stands: ${options.defaultValue}`;
|
|
193
|
+
separate(ctx);
|
|
194
|
+
ctx.report.info(`${options.question} — no terminal to ask on, so ${outcome}. Pass ${options.flag} to set it.`);
|
|
195
|
+
return options.defaultValue;
|
|
196
|
+
}
|
|
197
|
+
const suffix = options.defaultValue === undefined ? '' : ` [${options.defaultValue}]`;
|
|
198
|
+
separate(ctx);
|
|
199
|
+
ctx.report.info(`${options.question}${suffix}`);
|
|
200
|
+
const answer = readLineSync();
|
|
201
|
+
if (answer === undefined)
|
|
202
|
+
return options.defaultValue;
|
|
203
|
+
const trimmed = answer.trim();
|
|
204
|
+
return trimmed === '' ? options.defaultValue : trimmed;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/core/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAmClC,gGAAgG;AAChG,MAAM,QAAQ,GAAG,CAAC,CAAC;AAEnB,0GAA0G;AAC1G,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB,4FAA4F;AAC5F,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,6FAA6F;AAC7F,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B;;;;;;;;;;;;;GAaG;AACH,SAAS,qBAAqB;IAC5B,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC;AAC/C,CAAC;AAED,0EAA0E;AAC1E,SAAS,SAAS,CAAC,YAAoB;IACrC,OAAO,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC;AAC7E,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY;IACnB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,aAAa,GAAG,CAAC,CAAC;IAEtB,SAAS,CAAC;QACR,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,wFAAwF;YACxF,oFAAoF;YACpF,qFAAqF;YACrF,wFAAwF;YACxF,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,IAAI,aAAa,GAAG,kBAAkB,EAAE,CAAC;gBAC7F,aAAa,IAAI,CAAC,CAAC;gBACnB,SAAS,CAAC,eAAe,CAAC,CAAC;gBAC3B,SAAS;YACX,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC5F,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;QAChC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,oBAAoB,CAAC,GAAkB;IAC9C,IAAI,GAAG,CAAC,KAAK,CAAC,cAAc,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAChE,IAAI,CAAC,qBAAqB,EAAE;QAAE,OAAO,aAAa,CAAC;IACnD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS,CAAC,GAAkB;IAC1C,OAAO,oBAAoB,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;AACjD,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,QAAQ,CAAC,GAAkB;IAClC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAmB,EAAE,GAAkB;IAC9D,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC,aAAa,CAAC;IACpD,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;QAC7B,QAAQ,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,MAAM,CAAC,IAAI,CACb,GAAG,OAAO,CAAC,QAAQ,mDAAmD;YACpE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,GAAG,CACvF,CAAC;QACF,OAAO,OAAO,CAAC,aAAa,CAAC;IAC/B,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IACzD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,cAAc,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC9D,8FAA8F;QAC9F,QAAQ,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,aAAa,CAAC;QAEvD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC/C,IAAI,UAAU,KAAK,EAAE;YAAE,OAAO,OAAO,CAAC,aAAa,CAAC;QACpD,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC;QAC5D,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IAC9D,CAAC;IACD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,qDAAqD,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAC9G,OAAO,OAAO,CAAC,aAAa,CAAC;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,OAAuB,EAAE,GAAkB;IACjE,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC,YAAY,CAAC;IACnD,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,uBAAuB,OAAO,CAAC,YAAY,EAAE,CAAC;QACtH,QAAQ,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,gCAAgC,OAAO,UAAU,OAAO,CAAC,IAAI,aAAa,CAAC,CAAC;QAC/G,OAAO,OAAO,CAAC,YAAY,CAAC;IAC9B,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,YAAY,GAAG,CAAC;IACtF,QAAQ,CAAC,GAAG,CAAC,CAAC;IACd,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,GAAG,MAAM,EAAE,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,YAAY,CAAC;IAEtD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalising the repo-relative paths `harness.config.json` holds.
|
|
3
|
+
*
|
|
4
|
+
* **The rule this module exists to enforce: there is one normalisation, and the variant that
|
|
5
|
+
* resolves `..` is spelled differently from the one that does not.** Every path in the config file
|
|
6
|
+
* is repo-relative (`docs/config.md` §5), so nothing here resolves against the filesystem or against
|
|
7
|
+
* a repository root — these are string functions over already-relative values, which is what makes
|
|
8
|
+
* them safe to apply to a value read from a file that has not been validated yet.
|
|
9
|
+
*
|
|
10
|
+
* They are separate from `core/paths.ts` deliberately: that module's rule is that every path in it is
|
|
11
|
+
* *derived at runtime* — resolved by a `git` call or from the installed package's own location, on
|
|
12
|
+
* every run. A pure normalisation of a configured string is the opposite kind of value, and putting
|
|
13
|
+
* it there would weaken a header that a review reads as a guarantee.
|
|
14
|
+
*
|
|
15
|
+
* ## Why there are two, and why they are not called the same thing
|
|
16
|
+
*
|
|
17
|
+
* They were, once. Seven modules carried a private copy of this function; six had the lax body and
|
|
18
|
+
* one had the strict one, and two of those seven had given their copy the *same name* under an
|
|
19
|
+
* identical doc comment. A caller moved between those two files silently changed behaviour on a path
|
|
20
|
+
* containing `..` — no compile error, no test, and the only visible symptom would have been two
|
|
21
|
+
* writes landing on one target. The names below differ so that can never be true again: a reader who
|
|
22
|
+
* sees `Strict` knows a `..` is resolved, and one who sees {@link normalizeRepoDir} knows it is not.
|
|
23
|
+
*/
|
|
24
|
+
import { posix } from 'node:path';
|
|
25
|
+
/**
|
|
26
|
+
* A repo-relative directory as `harness.config.json` writes it: forward slashes, no trailing
|
|
27
|
+
* separator, no leading `./`. The repository root normalises to `.`.
|
|
28
|
+
*
|
|
29
|
+
* **This is the default — reach for it unless the caller has the specific reason below.** It leaves
|
|
30
|
+
* a `..` segment exactly where it found it, which is the right answer for the callers that pass the
|
|
31
|
+
* result on to be joined, matched or printed: a `.gitignore` pattern is matched literally rather than
|
|
32
|
+
* resolved, so silently rewriting one would change which files it covers, and a value substituted
|
|
33
|
+
* into a permission entry has to stay the string the guard will actually see.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeRepoDir(value) {
|
|
36
|
+
const forwardSlashed = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
37
|
+
const withoutLeadingDot = forwardSlashed.replace(/^\.\//, '');
|
|
38
|
+
return withoutLeadingDot === '' ? '.' : withoutLeadingDot;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The same normalisation, plus `posix.normalize`: `.` and `..` segments are resolved and repeated
|
|
42
|
+
* slashes collapsed, so every spelling of one location yields one string.
|
|
43
|
+
*
|
|
44
|
+
* **For a caller that keys on the result** — deduplicating configured paths before enqueueing a write
|
|
45
|
+
* at each. Two spellings of one path that survive as two keys become two writes aiming at one target,
|
|
46
|
+
* which the write engine refuses outright, so the resolution has to happen before the map rather than
|
|
47
|
+
* after it.
|
|
48
|
+
*
|
|
49
|
+
* Do not substitute it for {@link normalizeRepoDir} to be "safer": resolving `..` in a value that is
|
|
50
|
+
* about to be matched literally changes what it matches.
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeRepoPathStrict(value) {
|
|
53
|
+
return posix.normalize(value.replace(/\\/g, '/').replace(/\/+$/, ''));
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=repoPaths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repoPaths.js","sourceRoot":"","sources":["../../src/core/repoPaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrE,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9D,OAAO,iBAAiB,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's single output surface.
|
|
3
|
+
*
|
|
4
|
+
* Every command writes through a `Reporter` rather than calling `console` directly, so
|
|
5
|
+
* three properties hold in one place: `--quiet` behaves the same everywhere, the write
|
|
6
|
+
* engine's action log and the human-readable log are the same log, and the output is
|
|
7
|
+
* **plain ASCII** — no colour escapes and no emoji — so a test can diff it verbatim.
|
|
8
|
+
*
|
|
9
|
+
* Markers: `+` something was written (or would be), `~` an existing file was merged into,
|
|
10
|
+
* `=` an existing file was left as it stood, `!` needs attention (a warning or a failure).
|
|
11
|
+
*
|
|
12
|
+
* Streams: `info` / `step` / `ok` / action lines and `summary()` go to stdout and are what
|
|
13
|
+
* `--quiet` suppresses; `warn` and `fail` go to stderr and print whatever the flags are; `result()`
|
|
14
|
+
* goes to stdout and prints whatever the flags are. So `--quiet` suppresses **narration**, not
|
|
15
|
+
* output: a quiet run still prints every command answer its command routed through `result()`.
|
|
16
|
+
*/
|
|
17
|
+
const ACTION_MARKER = {
|
|
18
|
+
created: '+',
|
|
19
|
+
merged: '~',
|
|
20
|
+
kept: '=',
|
|
21
|
+
ensured: '+',
|
|
22
|
+
replaced: '+',
|
|
23
|
+
'would-create': '+',
|
|
24
|
+
'would-merge': '~',
|
|
25
|
+
'would-keep': '=',
|
|
26
|
+
'would-ensure': '+',
|
|
27
|
+
'would-replace': '+',
|
|
28
|
+
};
|
|
29
|
+
const ACTION_LABEL = {
|
|
30
|
+
created: 'created',
|
|
31
|
+
merged: 'merged',
|
|
32
|
+
kept: 'kept',
|
|
33
|
+
ensured: 'ensured',
|
|
34
|
+
replaced: 'replaced',
|
|
35
|
+
'would-create': 'would create',
|
|
36
|
+
'would-merge': 'would merge',
|
|
37
|
+
'would-keep': 'would keep',
|
|
38
|
+
'would-ensure': 'would ensure',
|
|
39
|
+
'would-replace': 'would replace',
|
|
40
|
+
};
|
|
41
|
+
/** Fixed order for the summary counts, so two runs of the same shape render identically. */
|
|
42
|
+
const ACTION_ORDER = [
|
|
43
|
+
'created',
|
|
44
|
+
'merged',
|
|
45
|
+
'kept',
|
|
46
|
+
'ensured',
|
|
47
|
+
'replaced',
|
|
48
|
+
'would-create',
|
|
49
|
+
'would-merge',
|
|
50
|
+
'would-keep',
|
|
51
|
+
'would-ensure',
|
|
52
|
+
'would-replace',
|
|
53
|
+
];
|
|
54
|
+
/** Width of the widest label in {@link ACTION_LABEL}, so action lines column-align. */
|
|
55
|
+
const LABEL_WIDTH = Math.max(...Object.values(ACTION_LABEL).map((label) => label.length));
|
|
56
|
+
export class Reporter {
|
|
57
|
+
#quiet;
|
|
58
|
+
#out;
|
|
59
|
+
#err;
|
|
60
|
+
#actions = [];
|
|
61
|
+
constructor(options = {}) {
|
|
62
|
+
this.#quiet = options.quiet ?? false;
|
|
63
|
+
this.#out = options.out ?? ((line) => console.log(line));
|
|
64
|
+
this.#err = options.err ?? ((line) => console.error(line));
|
|
65
|
+
}
|
|
66
|
+
get quiet() {
|
|
67
|
+
return this.#quiet;
|
|
68
|
+
}
|
|
69
|
+
/** The action log, in the order it was recorded. */
|
|
70
|
+
get actions() {
|
|
71
|
+
return this.#actions;
|
|
72
|
+
}
|
|
73
|
+
/** A plain line of narration. */
|
|
74
|
+
info(message) {
|
|
75
|
+
if (!this.#quiet)
|
|
76
|
+
this.#out(message);
|
|
77
|
+
}
|
|
78
|
+
/** A section heading — one per generator, so a long `init` run reads as a list of stages. */
|
|
79
|
+
step(message) {
|
|
80
|
+
if (!this.#quiet)
|
|
81
|
+
this.#out(`== ${message}`);
|
|
82
|
+
}
|
|
83
|
+
/** Something succeeded. */
|
|
84
|
+
ok(message) {
|
|
85
|
+
if (!this.#quiet)
|
|
86
|
+
this.#out(`+ ${message}`);
|
|
87
|
+
}
|
|
88
|
+
/** Something needs attention but does not stop the command. Always printed, even under `--quiet`. */
|
|
89
|
+
warn(message) {
|
|
90
|
+
this.#err(`! ${message}`);
|
|
91
|
+
}
|
|
92
|
+
/** Something failed. Always printed, even under `--quiet`. Does not set an exit code — see `core/errors.ts`. */
|
|
93
|
+
fail(message) {
|
|
94
|
+
this.#err(`!! ${message}`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A line that is the command's **answer** rather than its narration: printed on stdout even under
|
|
98
|
+
* `--quiet`.
|
|
99
|
+
*
|
|
100
|
+
* `doctor`'s counts summary is what this exists for. Its `--quiet` means "print what needs
|
|
101
|
+
* attention, and the verdict" — and since warnings and failures go to stderr, routing the verdict
|
|
102
|
+
* through {@link info} would leave a quiet run with nothing on stdout to read the result from,
|
|
103
|
+
* while routing it to stderr would put one line on two different streams depending on a flag.
|
|
104
|
+
* Narration stays on {@link info}; this is for the one line a caller came for.
|
|
105
|
+
*/
|
|
106
|
+
result(line) {
|
|
107
|
+
this.#out(line);
|
|
108
|
+
}
|
|
109
|
+
/** Record what happened to one path, and narrate it unless quiet. */
|
|
110
|
+
action(record) {
|
|
111
|
+
this.#actions.push(record);
|
|
112
|
+
if (this.#quiet)
|
|
113
|
+
return;
|
|
114
|
+
const suffix = record.detail === undefined ? '' : ` (${record.detail})`;
|
|
115
|
+
const label = ACTION_LABEL[record.kind].padEnd(LABEL_WIDTH);
|
|
116
|
+
this.#out(`${ACTION_MARKER[record.kind]} ${label} ${record.path}${suffix}`);
|
|
117
|
+
}
|
|
118
|
+
/** How many actions of each kind were recorded. */
|
|
119
|
+
counts() {
|
|
120
|
+
// Typed as a total record, so adding a kind above is a compile error here until it is
|
|
121
|
+
// seeded — the counts cannot silently omit one.
|
|
122
|
+
const counts = {
|
|
123
|
+
created: 0,
|
|
124
|
+
merged: 0,
|
|
125
|
+
kept: 0,
|
|
126
|
+
ensured: 0,
|
|
127
|
+
replaced: 0,
|
|
128
|
+
'would-create': 0,
|
|
129
|
+
'would-merge': 0,
|
|
130
|
+
'would-keep': 0,
|
|
131
|
+
'would-ensure': 0,
|
|
132
|
+
'would-replace': 0,
|
|
133
|
+
};
|
|
134
|
+
for (const record of this.#actions)
|
|
135
|
+
counts[record.kind] += 1;
|
|
136
|
+
return counts;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Render the action counts as one line, print it unless quiet, and return it so a caller
|
|
140
|
+
* or a test can assert on it without capturing the stream.
|
|
141
|
+
*/
|
|
142
|
+
summary() {
|
|
143
|
+
const counts = this.counts();
|
|
144
|
+
const parts = ACTION_ORDER.filter((kind) => counts[kind] > 0).map((kind) => `${counts[kind]} ${ACTION_LABEL[kind]}`);
|
|
145
|
+
const line = parts.length === 0 ? 'Summary: no changes' : `Summary: ${parts.join(', ')}`;
|
|
146
|
+
this.info(line);
|
|
147
|
+
return line;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/core/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `{{token}}` substitution, for every template the CLI renders.
|
|
3
|
+
*
|
|
4
|
+
* **The rule this module exists to enforce: the token syntax has one definition and the
|
|
5
|
+
* "every token has a value" check runs once, here.** Six modules had grown their own copy of the
|
|
6
|
+
* pattern and their own four-line render loop — the wrapper scripts, the context stubs, the
|
|
7
|
+
* repository-root files, the pre-push hook, the permission profile and the daemon units. Six copies
|
|
8
|
+
* of a regular expression is six chances for one of them to admit a character the others do not, and
|
|
9
|
+
* the failure that produces is a token left unsubstituted in a file that then ships: a permission
|
|
10
|
+
* entry matching nothing, a hook that does not parse, a unit installed half-rendered.
|
|
11
|
+
*
|
|
12
|
+
* ## The check runs before substitution, not after
|
|
13
|
+
*
|
|
14
|
+
* {@link renderTemplate} names an unsatisfied token by scanning the **template**, not the output.
|
|
15
|
+
* That is both stricter and more useful than scanning afterwards:
|
|
16
|
+
*
|
|
17
|
+
* - it names the offending token even where a later one would have masked it, and
|
|
18
|
+
* - a *value* that legitimately contains `{{…}}` cannot be mistaken for an unresolved token. That
|
|
19
|
+
* case is real: a wrapper script's body is an adopter's own command line, and an adopter's command
|
|
20
|
+
* line is allowed to contain braces.
|
|
21
|
+
*
|
|
22
|
+
* ## `assertNoneSurvive` is opt-in, and both settings are deliberate
|
|
23
|
+
*
|
|
24
|
+
* The post-substitution backstop catches the one case the pre-check cannot see — a substituted
|
|
25
|
+
* *value* that carried a token of its own, which would write a half-rendered file. Callers whose
|
|
26
|
+
* values are paths, labels and generated markup ask for it. The wrapper-script generator deliberately
|
|
27
|
+
* does **not**: its substituted value is the adopter's raw command line, so a `{{…}}` in the output
|
|
28
|
+
* is content rather than a fault, and failing on it would refuse a legal command.
|
|
29
|
+
*
|
|
30
|
+
* Every failure here is a fault in this CLI or in its shipped assets rather than in the adopting
|
|
31
|
+
* repository — a template and the module that renders it disagreeing about the token set is a
|
|
32
|
+
* packaging fault — so every throw goes through {@link internal} and exits {@link EXIT.INTERNAL}.
|
|
33
|
+
*/
|
|
34
|
+
import { internal } from './errors.js';
|
|
35
|
+
/**
|
|
36
|
+
* A `{{token}}` in a template, and the whole of what one may be named.
|
|
37
|
+
*
|
|
38
|
+
* Global, because both users of it consume every match: `matchAll` for the pre-check and `replace`
|
|
39
|
+
* for the substitution. Neither advances `lastIndex` on this object — `matchAll` iterates a clone and
|
|
40
|
+
* `replace` resets it — but `test` and `exec` would, which is why {@link containsToken} keeps its own
|
|
41
|
+
* non-global copy rather than reusing this one.
|
|
42
|
+
*/
|
|
43
|
+
const TOKEN_PATTERN = /\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g;
|
|
44
|
+
/** {@link TOKEN_PATTERN} without the `g` flag, so a one-shot test carries no `lastIndex` between calls. */
|
|
45
|
+
const TOKEN_PRESENT = /\{\{[A-Za-z][A-Za-z0-9_]*\}\}/;
|
|
46
|
+
/**
|
|
47
|
+
* Does this text still carry a token?
|
|
48
|
+
*
|
|
49
|
+
* For a caller checking an already-rendered string it did not render itself — the permission
|
|
50
|
+
* profile's runnability check, which asks it of every generated entry, because an entry carrying a
|
|
51
|
+
* leftover token matches neither `allow` nor `deny` and stalls an unattended run.
|
|
52
|
+
*/
|
|
53
|
+
export function containsToken(text) {
|
|
54
|
+
return TOKEN_PRESENT.test(text);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Substitute every `{{token}}` in `text`, having first checked that each one has a value.
|
|
58
|
+
*
|
|
59
|
+
* Returns the rendered text. Throws {@link EXIT.INTERNAL} when the text uses a token this caller
|
|
60
|
+
* supplies no value for, and — under {@link RenderTemplateOptions.assertNoneSurvive} — when one
|
|
61
|
+
* survived into the output.
|
|
62
|
+
*
|
|
63
|
+
* It takes the text rather than a path on purpose: its callers read their templates from three
|
|
64
|
+
* different places (the packaged `templates/` tree, the packaged `scripts/` tree, and a JSON
|
|
65
|
+
* document already parsed in memory), and a renderer that also resolved the file would have to know
|
|
66
|
+
* about all three.
|
|
67
|
+
*/
|
|
68
|
+
export function renderTemplate(text, values, { describe, assertNoneSurvive = false, known }) {
|
|
69
|
+
const admissible = known ?? new Set(Object.keys(values));
|
|
70
|
+
for (const match of text.matchAll(TOKEN_PATTERN)) {
|
|
71
|
+
const token = match[1];
|
|
72
|
+
if (!admissible.has(token)) {
|
|
73
|
+
throw internal(`${describe} uses the token {{${token}}}, which this generator supplies no value for, so the template and the generator disagree about the token set`);
|
|
74
|
+
}
|
|
75
|
+
if (!Object.hasOwn(values, token)) {
|
|
76
|
+
throw internal(`${describe} uses the token {{${token}}} in a position this generator has no value for it in, so a half-rendered entry would have been written`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const rendered = text.replace(TOKEN_PATTERN, (_whole, token) => values[token]);
|
|
80
|
+
if (assertNoneSurvive) {
|
|
81
|
+
const unresolved = rendered.match(TOKEN_PATTERN);
|
|
82
|
+
if (unresolved !== null) {
|
|
83
|
+
throw internal(`${describe} still contains ${unresolved[0]} after substitution, so a half-rendered file would have been written: one of the substituted values carries a token of its own`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return rendered;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=templating.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templating.js","sourceRoot":"","sources":["../../src/core/templating.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG,kCAAkC,CAAC;AAEzD,2GAA2G;AAC3G,MAAM,aAAa,GAAG,+BAA+B,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AA8BD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,MAAwC,EACxC,EAAE,QAAQ,EAAE,iBAAiB,GAAG,KAAK,EAAE,KAAK,EAAyB;IAErE,MAAM,UAAU,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAEzD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAW,CAAC;QACjC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,QAAQ,CACZ,GAAG,QAAQ,qBAAqB,KAAK,gHAAgH,CACtJ,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,QAAQ,CACZ,GAAG,QAAQ,qBAAqB,KAAK,0GAA0G,CAChJ,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAW,CAAC,CAAC;IAEjG,IAAI,iBAAiB,EAAE,CAAC;QACtB,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QACjD,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACxB,MAAM,QAAQ,CACZ,GAAG,QAAQ,mBAAmB,UAAU,CAAC,CAAC,CAAC,gIAAgI,CAC5K,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|