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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading, serializing and merging JSON, for every JSON file the CLI touches.
|
|
3
|
+
*
|
|
4
|
+
* **The rule this module exists to enforce: {@link mergeMissing} is the CLI's only JSON-merge
|
|
5
|
+
* primitive, and it is non-destructive by construction.** `init` writes into files the adopter
|
|
6
|
+
* shares with other tooling — `.claude/settings.json`, `.mcp.json` — where an overwrite would
|
|
7
|
+
* destroy settings the harness knows nothing about. Rather than asking each generator to be
|
|
8
|
+
* careful, the one merge available adds absent keys and can do nothing else: it cannot
|
|
9
|
+
* overwrite a value, cannot delete a key, and cannot reorder the keys already there. A
|
|
10
|
+
* generator that wants different behaviour is asking for a different re-run contract and
|
|
11
|
+
* belongs in the write engine's `--force` path, which writes a `.bak` sibling first.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here writes a file. Every byte this module formats **into an adopter's repository**
|
|
14
|
+
* reaches the filesystem through `core/writer.ts`, which is what keeps `--dry-run`, the re-run
|
|
15
|
+
* policy and the repo-confinement check ahead of every such write rather than beside some of
|
|
16
|
+
* them. The machine-scoped registry `machine/registry.ts` owns is the single exception: it
|
|
17
|
+
* writes {@link formatJson} output itself, outside any plan and outside `--dry-run`, and states
|
|
18
|
+
* why in its own header.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { HarnessError } from './errors.js';
|
|
22
|
+
/**
|
|
23
|
+
* True for a plain JSON object, i.e. not `null` and not an array.
|
|
24
|
+
*
|
|
25
|
+
* The parameter is `unknown` rather than `JsonValue | undefined` so the one predicate serves both
|
|
26
|
+
* kinds of caller — a `JSON.parse` result whose shape is not yet known, and a `JsonValue` being
|
|
27
|
+
* descended through — without either of them casting. It is exported because six modules had
|
|
28
|
+
* written it out for themselves, which is six chances for one of them to drop the `Array.isArray`
|
|
29
|
+
* half and start treating a list as an object.
|
|
30
|
+
*/
|
|
31
|
+
export function isJsonObject(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The CLI's single JSON serialization: two-space indent and a trailing newline, so re-writing
|
|
36
|
+
* unchanged content is byte-identical and an idempotent second `init` produces an empty diff.
|
|
37
|
+
*
|
|
38
|
+
* Exported so a caller that needs the text without writing it — a `--dry-run` preview, a
|
|
39
|
+
* comparison against what is already on disk — produces the same bytes the write engine commits,
|
|
40
|
+
* instead of re-deriving the format and drifting from it.
|
|
41
|
+
*/
|
|
42
|
+
export function formatJson(value) {
|
|
43
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read and parse a JSON file.
|
|
47
|
+
*
|
|
48
|
+
* Returns `undefined` when the file does not exist — absence is an ordinary answer here, since
|
|
49
|
+
* the create-if-absent contract asks "is it there?" before every write. A file that exists but
|
|
50
|
+
* does not parse is *not* ordinary: it throws a `HarnessError` naming the file, because
|
|
51
|
+
* treating unreadable JSON as absent would let a later write clobber a file the adopter merely
|
|
52
|
+
* mistyped.
|
|
53
|
+
*
|
|
54
|
+
* The type parameter is an assertion about the file's shape, not a validation of it; a caller
|
|
55
|
+
* that needs the shape guaranteed validates the parsed value itself.
|
|
56
|
+
*/
|
|
57
|
+
export function readJsonFile(path) {
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
text = readFileSync(path, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error?.code === 'ENOENT')
|
|
64
|
+
return undefined;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new HarnessError(`${path} is not valid JSON: ${detail}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Add to `target` every key `source` has and `target` lacks, recursively, and return the dotted
|
|
77
|
+
* paths of what was added.
|
|
78
|
+
*
|
|
79
|
+
* What it does **not** do, by construction:
|
|
80
|
+
*
|
|
81
|
+
* - **Never overwrites.** A key present in `target` keeps its value, including when that value
|
|
82
|
+
* is `null` or an empty string. Present means present, not truthy.
|
|
83
|
+
* - **Never deletes**, and never reorders the keys already in `target` — additions land after
|
|
84
|
+
* the existing keys, so a merged file's diff is an append.
|
|
85
|
+
* - **Never merges arrays element-wise.** An array is a leaf: added wholesale when absent, left
|
|
86
|
+
* untouched when present. Appending into an adopter's array would duplicate entries on a
|
|
87
|
+
* re-run and reorder a list whose order may matter; a generator that must extend a list — the
|
|
88
|
+
* permission profile's allow entries, say — owns that decision explicitly rather than
|
|
89
|
+
* inheriting it from a merge helper.
|
|
90
|
+
* - **Never changes a type.** Where `target` holds a scalar or an array and `source` holds an
|
|
91
|
+
* object (or the reverse), `target` wins and the subtree is skipped; nothing is recorded,
|
|
92
|
+
* because nothing was added.
|
|
93
|
+
*
|
|
94
|
+
* `target` is mutated in place. Added subtrees are deep-copied out of `source`, so the caller's
|
|
95
|
+
* source object and the merged result never share a reference.
|
|
96
|
+
*
|
|
97
|
+
* The returned paths are the **topmost** key added at each point: a whole subtree absent from
|
|
98
|
+
* `target` is reported once by its root (`permissions`), not leaf by leaf. Segments are joined
|
|
99
|
+
* with `.` for the action log — a key that itself contains a dot is rendered as-is and is
|
|
100
|
+
* therefore ambiguous, which is acceptable for a log line and is not parsed anywhere.
|
|
101
|
+
*/
|
|
102
|
+
export function mergeMissing(target, source) {
|
|
103
|
+
return mergeMissingInto(target, source, '');
|
|
104
|
+
}
|
|
105
|
+
function mergeMissingInto(target, source, prefix) {
|
|
106
|
+
const added = [];
|
|
107
|
+
for (const key of Object.keys(source)) {
|
|
108
|
+
const path = prefix === '' ? key : `${prefix}.${key}`;
|
|
109
|
+
const incoming = source[key];
|
|
110
|
+
if (incoming === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
if (!Object.hasOwn(target, key)) {
|
|
113
|
+
target[key] = structuredClone(incoming);
|
|
114
|
+
added.push(path);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const existing = target[key];
|
|
118
|
+
if (isJsonObject(existing) && isJsonObject(incoming)) {
|
|
119
|
+
added.push(...mergeMissingInto(existing, incoming, path));
|
|
120
|
+
}
|
|
121
|
+
// Anything else: `target` already has a value here and keeps it.
|
|
122
|
+
}
|
|
123
|
+
return added;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/core/json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQ3C;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAkC,IAAY;IACxE,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAAsC,EAAE,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACjF,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,YAAY,CAAC,GAAG,IAAI,uBAAuB,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAkB,EAAE,MAAkB;IACjE,OAAO,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAkB,EAAE,MAAkB,EAAE,MAAc;IAC9E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;QACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAS;QAErC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,SAAS;QACX,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,KAAK,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;QAC5D,CAAC;QACD,iEAAiE;IACnE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which source directories under the application directory no `layers[]` entry covers, and so route
|
|
3
|
+
* to the catch-all row every profile ends with.
|
|
4
|
+
*
|
|
5
|
+
* **The rule this module exists to enforce: there is one derivation of that set.** `doctor` grades it
|
|
6
|
+
* as its `layer-drift` check and `init` reports it at the end of an adoption run, and two copies of
|
|
7
|
+
* the look-in set or of the exclusion list would let one repository be described in two different
|
|
8
|
+
* terms by two commands an adopter runs minutes apart. The grading and the wording stay with each
|
|
9
|
+
* caller; the set does not.
|
|
10
|
+
*
|
|
11
|
+
* It reads directory entries and resolves each candidate against the repository's ignore rules, and
|
|
12
|
+
* writes nothing.
|
|
13
|
+
*/
|
|
14
|
+
import { readdirSync } from 'node:fs';
|
|
15
|
+
import { join, posix } from 'node:path';
|
|
16
|
+
import { DEFAULTS, LAYER_CATCH_ALL_PATH } from '../config/model.js';
|
|
17
|
+
import { pathIsIgnored } from './git.js';
|
|
18
|
+
import { normalizeRepoPathStrict } from './repoPaths.js';
|
|
19
|
+
/** The dependency tree, excluded by name because no adopter's layer profile is ever about it. */
|
|
20
|
+
const VENDOR_DIRECTORY = 'node_modules';
|
|
21
|
+
/**
|
|
22
|
+
* Whether one repo-relative path is another or sits under it. Both arguments are already normalised,
|
|
23
|
+
* so this is the whole containment test — the segment boundary is what stops `srcx` matching `src`.
|
|
24
|
+
*/
|
|
25
|
+
function isWithin(inner, outer) {
|
|
26
|
+
return inner === outer || inner.startsWith(`${outer}/`);
|
|
27
|
+
}
|
|
28
|
+
/** The names of the directories directly inside a path. Never throws — an unreadable one has none. */
|
|
29
|
+
function subdirectoryNames(path) {
|
|
30
|
+
try {
|
|
31
|
+
return readdirSync(path, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isDirectory())
|
|
33
|
+
.map((entry) => entry.name);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The not-graded answer, so the gate below and its callers cannot spell "empty" two ways. */
|
|
40
|
+
function notGraded(appDir) {
|
|
41
|
+
return { graded: false, appDir, covering: [], candidates: [], uncovered: [] };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The uncovered source directories under `appDir`, and the inputs the answer was derived from.
|
|
45
|
+
*
|
|
46
|
+
* **The gate is Finding 38's own amendment**: a profile that is nothing but the catch-all row makes
|
|
47
|
+
* every directory qualify, so this returns `graded: false` there and that state is the caller's to
|
|
48
|
+
* report, once, as itself.
|
|
49
|
+
*
|
|
50
|
+
* **It reads no `detection` field, and a recorded review deliberately does not silence it**: a
|
|
51
|
+
* verdict describes the profile at the moment it was reviewed, and a layer hand-added afterwards is
|
|
52
|
+
* exactly the drift this derivation exists to name.
|
|
53
|
+
*
|
|
54
|
+
* **The directories looked in are two levels of the configuration rather than one.** `init` writes
|
|
55
|
+
* `appDir: "."` with layer paths under `src/` (`generators/harnessConfig.ts`, `detect/presets.ts`),
|
|
56
|
+
* so `appDir`'s only child is `src` and a drifted sibling *inside* `src/` would never be a candidate;
|
|
57
|
+
* adding each configured layer path's proper ancestors to the look-in set is what reaches
|
|
58
|
+
* `src/common`, the shape the finding is filed on.
|
|
59
|
+
*
|
|
60
|
+
* **The exclusions are dot-names, {@link VENDOR_DIRECTORY}, the harness's own four configured
|
|
61
|
+
* directories — `stateDir`, `scriptsDir`, `githooksDir` and the documentation root — and whatever
|
|
62
|
+
* the repository's own ignore rules already exclude, and the list stays that short**: every entry
|
|
63
|
+
* on it is either a directory the harness itself configures or a declaration the repository has
|
|
64
|
+
* already made, where a longer built-in one would be this module inventing an opinion about the
|
|
65
|
+
* adopter's tree, and the gate above is what removes the noise.
|
|
66
|
+
*
|
|
67
|
+
* `config.layers` is read defensively rather than trusted: a value that is not an array yields the
|
|
68
|
+
* not-graded answer, which is the same arm as a profile with no covering row, because in both cases
|
|
69
|
+
* there is nothing to test coverage against. `config/check.ts` is what reports the shape itself.
|
|
70
|
+
*/
|
|
71
|
+
export function layerCoverage(options) {
|
|
72
|
+
const { repoRoot, config } = options;
|
|
73
|
+
const appDir = normalizeRepoPathStrict(config.appDir ?? DEFAULTS.appDir);
|
|
74
|
+
if (!Array.isArray(config.layers))
|
|
75
|
+
return notGraded(appDir);
|
|
76
|
+
// The rows that can cover anything: the catch-all covers everything by construction, which is why
|
|
77
|
+
// it is excluded here rather than treated as coverage. A value that is not a non-empty string is
|
|
78
|
+
// `config/check.ts`'s finding, so it is skipped rather than turned into a second one, and the row
|
|
79
|
+
// is identified by `path` rather than by the name `general` ({@link LAYER_CATCH_ALL_PATH}).
|
|
80
|
+
const covering = [];
|
|
81
|
+
for (const [index, layer] of config.layers.entries()) {
|
|
82
|
+
const path = layer?.path;
|
|
83
|
+
if (typeof path !== 'string' || path.trim() === '')
|
|
84
|
+
continue;
|
|
85
|
+
const normalized = normalizeRepoPathStrict(path);
|
|
86
|
+
if (normalized === LAYER_CATCH_ALL_PATH)
|
|
87
|
+
continue;
|
|
88
|
+
const name = layer?.name;
|
|
89
|
+
covering.push({ name: typeof name === 'string' && name.trim() !== '' ? name : `layers[${index}]`, path: normalized });
|
|
90
|
+
}
|
|
91
|
+
// The gate, before anything is enumerated, and the one arm there is.
|
|
92
|
+
if (covering.length === 0)
|
|
93
|
+
return notGraded(appDir);
|
|
94
|
+
// Step (i): the application directory, plus every proper ancestor of a configured layer path that
|
|
95
|
+
// lies at or below it.
|
|
96
|
+
const lookIn = new Set([appDir]);
|
|
97
|
+
for (const layer of covering) {
|
|
98
|
+
for (let dir = posix.dirname(layer.path); dir !== LAYER_CATCH_ALL_PATH && dir !== '/'; dir = posix.dirname(dir)) {
|
|
99
|
+
// A configured path that leaves the repository is the config check's finding; reading a
|
|
100
|
+
// directory outside the tree is not this module's business.
|
|
101
|
+
if (dir.split('/')[0] === '..')
|
|
102
|
+
break;
|
|
103
|
+
if (appDir === LAYER_CATCH_ALL_PATH || isWithin(dir, appDir))
|
|
104
|
+
lookIn.add(dir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Step (ii): the immediate sub-directories of each, less the harness's own trees. A directory
|
|
108
|
+
// that cannot be read yields nothing — absence is an ordinary answer here.
|
|
109
|
+
const harnessDirs = [
|
|
110
|
+
config.stateDir,
|
|
111
|
+
config.scriptsDir ?? DEFAULTS.scriptsDir,
|
|
112
|
+
config.githooksDir ?? DEFAULTS.githooksDir,
|
|
113
|
+
config.docs?.root,
|
|
114
|
+
]
|
|
115
|
+
.filter((value) => typeof value === 'string' && value.trim() !== '')
|
|
116
|
+
.map((value) => normalizeRepoPathStrict(value));
|
|
117
|
+
const candidates = [];
|
|
118
|
+
for (const dir of lookIn) {
|
|
119
|
+
for (const name of subdirectoryNames(join(repoRoot, dir))) {
|
|
120
|
+
if (name.startsWith('.') || name === VENDOR_DIRECTORY)
|
|
121
|
+
continue;
|
|
122
|
+
const candidate = dir === LAYER_CATCH_ALL_PATH ? name : `${dir}/${name}`;
|
|
123
|
+
if (harnessDirs.some((harness) => isWithin(harness, candidate)))
|
|
124
|
+
continue;
|
|
125
|
+
// A directory the repository itself declares is not source. Asking git is the repository's
|
|
126
|
+
// own answer rather than a built-in opinion about the adopter's tree, and it is what keeps a
|
|
127
|
+
// build output — `dist/`, `coverage/` — from being reported as a layer nobody wrote a row
|
|
128
|
+
// for. Only a definite `true` excludes: `pathIsIgnored` returns `undefined` where git could
|
|
129
|
+
// not answer, and an unanswerable probe leaves the candidate in.
|
|
130
|
+
if (pathIsIgnored(repoRoot, candidate) === true)
|
|
131
|
+
continue;
|
|
132
|
+
if (!candidates.includes(candidate))
|
|
133
|
+
candidates.push(candidate);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Coverage in both directions: a layer at `internal` covers `internal/api`, and `src` is covered
|
|
137
|
+
// by its own child layers.
|
|
138
|
+
const uncovered = candidates.filter((candidate) => !covering.some((layer) => isWithin(layer.path, candidate) || isWithin(candidate, layer.path)));
|
|
139
|
+
return { graded: true, appDir, covering, candidates, uncovered };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=layerCoverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layerCoverage.js","sourceRoot":"","sources":["../../src/core/layerCoverage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one spelling of what an adopter does about a source directory no layer covers, and of the
|
|
3
|
+
* recorded layer-profile review that changes the answer.
|
|
4
|
+
*
|
|
5
|
+
* **The rule this module exists to enforce: the two commands that report the gap give the same
|
|
6
|
+
* remedy.** `init` names the uncovered directories at the end of an adoption run and `doctor` warns
|
|
7
|
+
* about the same set from its `layer-drift` check, both over one derivation
|
|
8
|
+
* (`core/layerCoverage.ts`); a second spelling of the remedy in the second caller would let one
|
|
9
|
+
* repository be told two different things by two commands run minutes apart — which is the
|
|
10
|
+
* asymmetry the shared derivation was extracted to close, re-created one level up in the wording.
|
|
11
|
+
*
|
|
12
|
+
* **A recorded verdict changes the remedy and never the grade.** A review is a statement about the
|
|
13
|
+
* profile at the moment it was reviewed, so a directory that appeared afterwards still drifts
|
|
14
|
+
* (`core/layerCoverage.ts` grades it, and reads no `detection` field at all). What it does change is
|
|
15
|
+
* where the adopter is sent: telling somebody who has already recorded a decision to run the command
|
|
16
|
+
* that reaches that decision is an instruction to re-derive what the file already holds.
|
|
17
|
+
*
|
|
18
|
+
* Every field of a recorded review is optional (`config/model.ts`), so each is rendered only where
|
|
19
|
+
* it is present — no `undefined` token may reach an adopter.
|
|
20
|
+
*
|
|
21
|
+
* Both callers pass their own command spellings rather than importing a constant from here: `init`
|
|
22
|
+
* and `doctor` each keep their own `ANALYZE_COMMAND` and `CLI`, and this module names neither.
|
|
23
|
+
* `init --reset-config` is named by neither arm: it rebuilds the whole file from a fresh detection,
|
|
24
|
+
* which is not a remedy for one uncovered directory.
|
|
25
|
+
*
|
|
26
|
+
* It composes strings and reads nothing.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The recorded review as one clause, or `undefined` where there is nothing recorded to name.
|
|
30
|
+
*
|
|
31
|
+
* The verdict is the whole of what makes a record a record, so a review carrying no `verdict` reads
|
|
32
|
+
* as no review at all; `at` is appended only where it is there. **Capitalised**, because both
|
|
33
|
+
* callers splice it after a full stop and it is therefore always sentence-initial.
|
|
34
|
+
*/
|
|
35
|
+
export function recordedVerdictClause(review) {
|
|
36
|
+
const verdict = review?.verdict;
|
|
37
|
+
if (verdict === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const at = review?.at;
|
|
40
|
+
const recorded = typeof at === 'string' && at.trim() !== '' ? `, recorded ${at}` : '';
|
|
41
|
+
return `A layer-profile review is recorded here — verdict \`${verdict}\`${recorded}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What to do about the uncovered directories, split on whether a review is already recorded.
|
|
45
|
+
*
|
|
46
|
+
* **No record** — the standing remedy: the profile comes from a session that reads the code, and the
|
|
47
|
+
* spelling named is the `skip`-answering one rather than the `--yes` run, which takes `merge` on
|
|
48
|
+
* every already-filled document and rewrites documents nobody asked to touch.
|
|
49
|
+
*
|
|
50
|
+
* **A record** — `config set layers` leads, because it is the direct route for a directory that is a
|
|
51
|
+
* real layer and needs no session at all, and the analyze sentence names the one condition under
|
|
52
|
+
* which re-running is worth anything: the tree has moved since the review.
|
|
53
|
+
*/
|
|
54
|
+
export function layerGapRemedy(options) {
|
|
55
|
+
const { review, analyzeCommand, cli } = options;
|
|
56
|
+
const configSetLayers = `${cli} config set layers`;
|
|
57
|
+
if (review?.verdict === undefined) {
|
|
58
|
+
return `If any of them is a real layer, run \`${analyzeCommand}\` in a session here and answer \`skip\` for every already-filled document; a no-argument run with \`--yes\` takes \`merge\` on all of them and rewrites documents nobody asked to touch. The profile itself is applied through \`${configSetLayers}\``;
|
|
59
|
+
}
|
|
60
|
+
return `If any of them is a real layer, add its row with \`${configSetLayers}\`, which is what applies a profile and is the whole of the change. Re-running \`${analyzeCommand}\` is worth something only where the tree has moved since that review: over the tree it already read it re-derives the decision this record holds. If you do run it, answer \`skip\` for every already-filled document — a no-argument run with \`--yes\` takes \`merge\` on all of them and rewrites documents nobody asked to touch`;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=layerGapRemedy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layerGapRemedy.js","sourceRoot":"","sources":["../../src/core/layerGapRemedy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAcH;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAsC;IAC1E,MAAM,OAAO,GAAG,MAAM,EAAE,OAAO,CAAC;IAChC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,EAAE,GAAG,MAAM,EAAE,EAAE,CAAC;IACtB,MAAM,QAAQ,GAAG,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACtF,OAAO,uDAAuD,OAAO,KAAK,QAAQ,EAAE,CAAC;AACvF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,OAA8B;IAC3D,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAChD,MAAM,eAAe,GAAG,GAAG,GAAG,oBAAoB,CAAC;IAEnD,IAAI,MAAM,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO,yCAAyC,cAAc,qOAAqO,eAAe,IAAI,CAAC;IACzT,CAAC;IAED,OAAO,sDAAsD,eAAe,oFAAoF,cAAc,uUAAuU,CAAC;AACxf,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rendering of "a set of names, on one line" every command reports a name set through.
|
|
3
|
+
*
|
|
4
|
+
* It is its own module because every command that reports a name set renders it through this — the
|
|
5
|
+
* layer-gap pair (`doctor`'s `layer-drift` detail and `init`'s uncovered-directory line, one
|
|
6
|
+
* derivation between them in `core/layerCoverage.ts`) and the write plan's `merged` action qualifier
|
|
7
|
+
* (`core/writer.ts`) included. A private copy in any of them would be free to drift in its separator
|
|
8
|
+
* or its cap and make one repository read as two different answers.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* How many names a count-plus-names detail spells out before it summarises the rest.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately **not** exported: the cap is only meaningful through {@link nameList}, and a caller
|
|
14
|
+
* reading it would be a caller re-implementing the rendering this module exists to hold alone.
|
|
15
|
+
*/
|
|
16
|
+
const DETAIL_NAME_CAP = 6;
|
|
17
|
+
/** `a, b, c, +4 more` — a name list that stays one line however long it gets. */
|
|
18
|
+
export function nameList(names) {
|
|
19
|
+
const head = names.slice(0, DETAIL_NAME_CAP).join(', ');
|
|
20
|
+
const rest = names.length - DETAIL_NAME_CAP;
|
|
21
|
+
return rest > 0 ? `${head}, +${rest} more` : head;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=nameList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nameList.js","sourceRoot":"","sources":["../../src/core/nameList.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,iFAAiF;AACjF,MAAM,UAAU,QAAQ,CAAC,KAAwB;IAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,eAAe,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "derived at runtime" resolution class of `docs/config.md` §1, implemented once — path
|
|
3
|
+
* resolution and the package's own layout.
|
|
4
|
+
*
|
|
5
|
+
* **The rule this module exists to enforce: every path below is resolved on each run and is
|
|
6
|
+
* never written into a template.** A file that hardcodes one of these is wrong even when the
|
|
7
|
+
* hardcoded value happens to be right, because it stops being right the moment the checkout
|
|
8
|
+
* moves or a second worktree appears. Nothing else in the CLI may join a worktree glob by hand
|
|
9
|
+
* or reach for `import.meta.dirname` to find the package's own assets — it calls a function
|
|
10
|
+
* here, so there is one definition of each value to correct.
|
|
11
|
+
*
|
|
12
|
+
* **Nothing here invokes `git`.** The probes that ask a repository where its root is, what
|
|
13
|
+
* branch is checked out and which checkouts exist live in `core/git.ts`, which owns every git
|
|
14
|
+
* invocation that reads repository state or a path — including the two invariants those calls
|
|
15
|
+
* hold to. A path derived from a repository starts with a value from there and is joined here.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
20
|
+
import { EXIT, HarnessError } from './errors.js';
|
|
21
|
+
/** `<work_root>` — the directory holding the repository, and therefore its sibling worktrees. */
|
|
22
|
+
export function workRoot(repoRoot) {
|
|
23
|
+
return dirname(repoRoot);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `<worktree_glob>` — `<work_root>/<projectName>-*`, the pattern that matches the sibling
|
|
27
|
+
* worktree checkouts of one project.
|
|
28
|
+
*
|
|
29
|
+
* It is emitted unconditionally into a generated permission profile: a single-checkout adopter
|
|
30
|
+
* simply has no sibling for it to match, whereas omitting it is what silently skips a phase in
|
|
31
|
+
* a worktree run.
|
|
32
|
+
*/
|
|
33
|
+
export function worktreeGlob(work, projectName) {
|
|
34
|
+
return join(work, `${projectName}-*`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `<project_name>`'s default — the repository directory name. `harness.config.json` may set
|
|
38
|
+
* `projectName` explicitly; this is what `init` seeds when it does not.
|
|
39
|
+
*/
|
|
40
|
+
export function defaultProjectName(repoRoot) {
|
|
41
|
+
return basename(repoRoot);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse a git remote URL to `<owner>/<repository>`, or `undefined` when it carries no such
|
|
45
|
+
* pair. Handles the two forms a hosted remote is written in:
|
|
46
|
+
*
|
|
47
|
+
* - scp-like — `git@host:owner/repo.git`
|
|
48
|
+
* - URL — `https://host/owner/repo`, `ssh://git@host:22/owner/repo.git`
|
|
49
|
+
*
|
|
50
|
+
* A filesystem remote (`/srv/mirrors/repo.git`, `../other-checkout`) has no owner and yields
|
|
51
|
+
* `undefined` rather than a fabricated pair. For a host that nests groups, the last two path
|
|
52
|
+
* segments are taken, which is the pair every slug consumer means.
|
|
53
|
+
*/
|
|
54
|
+
export function parseRepoSlug(remoteUrl) {
|
|
55
|
+
const trimmed = remoteUrl.trim();
|
|
56
|
+
if (trimmed === '')
|
|
57
|
+
return undefined;
|
|
58
|
+
const schemeless = trimmed.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, '');
|
|
59
|
+
let pathPart;
|
|
60
|
+
if (schemeless !== trimmed) {
|
|
61
|
+
// URL form: drop `[user@]host[:port]`.
|
|
62
|
+
const slash = schemeless.indexOf('/');
|
|
63
|
+
if (slash < 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
pathPart = schemeless.slice(slash + 1);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// scp-like form: a colon before any slash separates the host from the path.
|
|
69
|
+
const colon = trimmed.indexOf(':');
|
|
70
|
+
if (colon < 0)
|
|
71
|
+
return undefined;
|
|
72
|
+
const firstSlash = trimmed.indexOf('/');
|
|
73
|
+
if (firstSlash >= 0 && firstSlash < colon)
|
|
74
|
+
return undefined;
|
|
75
|
+
pathPart = trimmed.slice(colon + 1);
|
|
76
|
+
}
|
|
77
|
+
const segments = pathPart
|
|
78
|
+
.replace(/\.git$/, '')
|
|
79
|
+
.split('/')
|
|
80
|
+
.filter((segment) => segment !== '');
|
|
81
|
+
if (segments.length < 2)
|
|
82
|
+
return undefined;
|
|
83
|
+
return segments.slice(-2).join('/');
|
|
84
|
+
}
|
|
85
|
+
/** `<home_root>` — the account home directory. */
|
|
86
|
+
export function homeRoot() {
|
|
87
|
+
return homedir();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* This module compiles to `dist/core/paths.js`, so the package root is two levels above it —
|
|
91
|
+
* one above `dist/`. Resolved from `import.meta.dirname`, which is why the package's Node
|
|
92
|
+
* floor is `>=20.11.0`; do not re-derive it from `import.meta.url`.
|
|
93
|
+
*/
|
|
94
|
+
const PACKAGE_ROOT = resolve(import.meta.dirname, '..', '..');
|
|
95
|
+
/**
|
|
96
|
+
* The root of this installed package — the parent of `dist/`, `templates/` and `scripts/`.
|
|
97
|
+
*
|
|
98
|
+
* The layout holds identically in the working copy and in the published tarball, where
|
|
99
|
+
* `files: ["dist", "scripts", "templates", "README.md"]` makes `templates/` and `scripts/`
|
|
100
|
+
* siblings of `dist/`.
|
|
101
|
+
*/
|
|
102
|
+
export function packageRoot() {
|
|
103
|
+
return PACKAGE_ROOT;
|
|
104
|
+
}
|
|
105
|
+
/** The generator templates shipped with the package. */
|
|
106
|
+
export function templatesDir() {
|
|
107
|
+
return join(PACKAGE_ROOT, 'templates');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The daemon unit templates shipped with the package.
|
|
111
|
+
* Distinct from the adopter's configured `scriptsDir`, which is where `init` *writes* wrapper
|
|
112
|
+
* scripts; this one is read-only and lives inside the installed package.
|
|
113
|
+
*/
|
|
114
|
+
export function packageScriptsDir() {
|
|
115
|
+
return join(PACKAGE_ROOT, 'scripts');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read a template's text, addressed relative to {@link templatesDir}.
|
|
119
|
+
*
|
|
120
|
+
* A missing template throws with the path named and exits {@link EXIT.INTERNAL}: a template
|
|
121
|
+
* absent from the installed package is a packaging fault the adopter cannot act on, and the
|
|
122
|
+
* loud failure is deliberate — a template owned by a later roadmap item must fail at its own
|
|
123
|
+
* writer rather than quietly producing an empty file in the adopter's repository.
|
|
124
|
+
*/
|
|
125
|
+
export function readTemplate(relativePath) {
|
|
126
|
+
const absolute = join(templatesDir(), relativePath);
|
|
127
|
+
try {
|
|
128
|
+
return readFileSync(absolute, 'utf8');
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error?.code === 'ENOENT') {
|
|
132
|
+
throw new HarnessError(`template missing from the installed package: ${absolute}`, EXIT.INTERNAL);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether `candidate` resolves to the repository root or somewhere beneath it.
|
|
139
|
+
*
|
|
140
|
+
* The write engine asks this before every write, so a mis-scoped run cannot escape the
|
|
141
|
+
* repository it was aimed at, and `doctor` asks it of the configured directories. The check is
|
|
142
|
+
* **lexical** — it compares resolved paths and does not follow symlinks — so it answers "does
|
|
143
|
+
* this path name a location inside the repo", not "does this file physically live there".
|
|
144
|
+
*/
|
|
145
|
+
export function insideRepo(repoRoot, candidate) {
|
|
146
|
+
const rel = relative(resolve(repoRoot), resolve(candidate));
|
|
147
|
+
if (rel === '')
|
|
148
|
+
return true;
|
|
149
|
+
if (isAbsolute(rel))
|
|
150
|
+
return false;
|
|
151
|
+
return rel !== '..' && !rel.startsWith(`..${sep}`);
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"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"}
|