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.
Files changed (171) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/cli.js +194 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/config.js +561 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/daemon.js +791 -0
  9. package/dist/commands/daemon.js.map +1 -0
  10. package/dist/commands/doctor.js +336 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/init.js +2023 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/registry.js +42 -0
  15. package/dist/commands/registry.js.map +1 -0
  16. package/dist/config/check.js +505 -0
  17. package/dist/config/check.js.map +1 -0
  18. package/dist/config/io.js +177 -0
  19. package/dist/config/io.js.map +1 -0
  20. package/dist/config/model.js +406 -0
  21. package/dist/config/model.js.map +1 -0
  22. package/dist/core/errors.js +71 -0
  23. package/dist/core/errors.js.map +1 -0
  24. package/dist/core/git.js +537 -0
  25. package/dist/core/git.js.map +1 -0
  26. package/dist/core/json.js +125 -0
  27. package/dist/core/json.js.map +1 -0
  28. package/dist/core/layerCoverage.js +141 -0
  29. package/dist/core/layerCoverage.js.map +1 -0
  30. package/dist/core/layerGapRemedy.js +62 -0
  31. package/dist/core/layerGapRemedy.js.map +1 -0
  32. package/dist/core/nameList.js +23 -0
  33. package/dist/core/nameList.js.map +1 -0
  34. package/dist/core/paths.js +153 -0
  35. package/dist/core/paths.js.map +1 -0
  36. package/dist/core/prompt.js +206 -0
  37. package/dist/core/prompt.js.map +1 -0
  38. package/dist/core/repoPaths.js +55 -0
  39. package/dist/core/repoPaths.js.map +1 -0
  40. package/dist/core/report.js +150 -0
  41. package/dist/core/report.js.map +1 -0
  42. package/dist/core/templating.js +88 -0
  43. package/dist/core/templating.js.map +1 -0
  44. package/dist/core/writer.js +479 -0
  45. package/dist/core/writer.js.map +1 -0
  46. package/dist/daemon/backend.js +180 -0
  47. package/dist/daemon/backend.js.map +1 -0
  48. package/dist/daemon/units.js +380 -0
  49. package/dist/daemon/units.js.map +1 -0
  50. package/dist/detect/nestedApplication.js +79 -0
  51. package/dist/detect/nestedApplication.js.map +1 -0
  52. package/dist/detect/presets.js +2033 -0
  53. package/dist/detect/presets.js.map +1 -0
  54. package/dist/detect/signals.js +1368 -0
  55. package/dist/detect/signals.js.map +1 -0
  56. package/dist/doctor/checks.js +3530 -0
  57. package/dist/doctor/checks.js.map +1 -0
  58. package/dist/generators/claudeContext.js +588 -0
  59. package/dist/generators/claudeContext.js.map +1 -0
  60. package/dist/generators/githooks.js +446 -0
  61. package/dist/generators/githooks.js.map +1 -0
  62. package/dist/generators/harnessConfig.js +632 -0
  63. package/dist/generators/harnessConfig.js.map +1 -0
  64. package/dist/generators/notifications.js +191 -0
  65. package/dist/generators/notifications.js.map +1 -0
  66. package/dist/generators/outerLoopScripts.js +165 -0
  67. package/dist/generators/outerLoopScripts.js.map +1 -0
  68. package/dist/generators/permissionProfile.js +1172 -0
  69. package/dist/generators/permissionProfile.js.map +1 -0
  70. package/dist/generators/projectSettings.js +322 -0
  71. package/dist/generators/projectSettings.js.map +1 -0
  72. package/dist/generators/repoRoot.js +417 -0
  73. package/dist/generators/repoRoot.js.map +1 -0
  74. package/dist/generators/scripts.js +557 -0
  75. package/dist/generators/scripts.js.map +1 -0
  76. package/dist/generators/stateDir.js +221 -0
  77. package/dist/generators/stateDir.js.map +1 -0
  78. package/dist/machine/paths.js +111 -0
  79. package/dist/machine/paths.js.map +1 -0
  80. package/dist/machine/plugins.js +224 -0
  81. package/dist/machine/plugins.js.map +1 -0
  82. package/dist/machine/registry.js +330 -0
  83. package/dist/machine/registry.js.map +1 -0
  84. package/package.json +23 -0
  85. package/scripts/README.md +13 -0
  86. package/scripts/daemon/launchd.plist.template +59 -0
  87. package/scripts/daemon/systemd.service.template +58 -0
  88. package/templates/README.md +15 -0
  89. package/templates/claude/CLAUDE.md +54 -0
  90. package/templates/claude/README.md +5 -0
  91. package/templates/claude/context/api.md +29 -0
  92. package/templates/claude/context/conventions.md +23 -0
  93. package/templates/claude/context/data-layer.md +28 -0
  94. package/templates/claude/context/data-storage.md +29 -0
  95. package/templates/claude/context/docs-catalog.md +29 -0
  96. package/templates/claude/context/domain.md +28 -0
  97. package/templates/claude/context/layer.md +20 -0
  98. package/templates/claude/context/module.md +30 -0
  99. package/templates/claude/context/package.md +29 -0
  100. package/templates/claude/context/presentation.md +32 -0
  101. package/templates/claude/context/state-slices.md +28 -0
  102. package/templates/claude/context/tests.md +28 -0
  103. package/templates/claude/harness-task-offer.md +58 -0
  104. package/templates/claude/push-notify.env.example +21 -0
  105. package/templates/claude/qa-accounts.env.example +38 -0
  106. package/templates/claude/qa_test_scenarios.md +110 -0
  107. package/templates/claude/settings.autonomous.json +93 -0
  108. package/templates/claude/settings.autonomous.qa.json +36 -0
  109. package/templates/githooks/README.md +3 -0
  110. package/templates/githooks/pre-push +72 -0
  111. package/templates/repo/README.md +3 -0
  112. package/templates/repo/gitattributes +16 -0
  113. package/templates/repo/gitignore +61 -0
  114. package/templates/repo/gitignore.qa +25 -0
  115. package/templates/repo/mcp.json +17 -0
  116. package/templates/scripts/README.md +5 -0
  117. package/templates/scripts/autonomous-format-stream.sh +95 -0
  118. package/templates/scripts/autonomous-notify.sh +337 -0
  119. package/templates/scripts/autonomous-watcher.sh +3087 -0
  120. package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
  121. package/templates/scripts/commit-on-branch.sh +288 -0
  122. package/templates/scripts/create-worktree.sh +360 -0
  123. package/templates/scripts/deploy.sh +47 -0
  124. package/templates/scripts/lib/harness-run-lib.sh +1481 -0
  125. package/templates/scripts/push-branch.sh +140 -0
  126. package/templates/scripts/refresh-branch.sh +244 -0
  127. package/templates/scripts/restart-watcher.sh +401 -0
  128. package/templates/scripts/scratch-run.sh +302 -0
  129. package/templates/scripts/setup-worktree.sh +262 -0
  130. package/templates/scripts/start-dev-server.sh +99 -0
  131. package/templates/scripts/test.sh +50 -0
  132. package/templates/scripts/typecheck.sh +50 -0
  133. package/templates/state-dir/README-root.md +13 -0
  134. package/templates/state-dir/README.md +9 -0
  135. package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
  136. package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
  137. package/templates/state-dir/architecture_reviews/README.md +9 -0
  138. package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
  139. package/templates/state-dir/autonomous_inbox/README.md +9 -0
  140. package/templates/state-dir/autonomous_logs/README.md +9 -0
  141. package/templates/state-dir/branch_statistics/README.md +9 -0
  142. package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
  143. package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
  144. package/templates/state-dir/business_parity_reviews/README.md +9 -0
  145. package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
  146. package/templates/state-dir/clarification_digests/README.md +9 -0
  147. package/templates/state-dir/clarifications/README.md +9 -0
  148. package/templates/state-dir/code_reviews/README.md +9 -0
  149. package/templates/state-dir/dispatch_additions/README.md +19 -0
  150. package/templates/state-dir/docs_catalog/README.md +9 -0
  151. package/templates/state-dir/flow_progress/README.md +9 -0
  152. package/templates/state-dir/improvement_observations/README.md +19 -0
  153. package/templates/state-dir/improvement_suggestions.md +29 -0
  154. package/templates/state-dir/lessons.md +23 -0
  155. package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
  156. package/templates/state-dir/qa_reviews/README.md +9 -0
  157. package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
  158. package/templates/state-dir/review_plan_reviews/README.md +9 -0
  159. package/templates/state-dir/scratch/README.md +11 -0
  160. package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
  161. package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
  162. package/templates/state-dir/skeptic_reviews/README.md +9 -0
  163. package/templates/state-dir/story_plans/README.md +9 -0
  164. package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
  165. package/templates/state-dir/task_plan_reviews/README.md +9 -0
  166. package/templates/state-dir/task_plans/README.md +9 -0
  167. package/templates/state-dir/task_prompts/README.md +9 -0
  168. package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
  169. package/templates/state-dir/ui_test_plans/README.md +9 -0
  170. package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
  171. 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":"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"}
@@ -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"}