sequant 2.9.0 → 2.10.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +14 -3
- package/dist/bin/cli.js +97 -17
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.js +2 -0
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +25 -3
- package/dist/src/commands/ready.js +39 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +37 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +33 -0
- package/dist/src/lib/cli-flags.js +46 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +62 -0
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/index.d.ts +3 -3
- package/dist/src/lib/locks/index.js +2 -2
- package/dist/src/lib/locks/lock-manager.d.ts +20 -2
- package/dist/src/lib/locks/lock-manager.js +47 -3
- package/dist/src/lib/locks/types.d.ts +34 -3
- package/dist/src/lib/locks/types.js +16 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +14 -0
- package/dist/src/lib/settings.js +61 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +9 -2
- package/dist/src/lib/test-tautology-detector.js +153 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +477 -44
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
- package/dist/src/lib/workflow/config-resolver.js +54 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +44 -1
- package/dist/src/lib/workflow/log-writer.js +75 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +457 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
- package/dist/src/lib/workflow/ready-gate.js +25 -9
- package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
- package/dist/src/lib/workflow/run-log-schema.js +47 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +141 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +126 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
- package/dist/src/lib/workflow/worktree-manager.js +102 -23
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +8 -7
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +75 -10
- package/templates/scripts/cleanup-worktree.sh +182 -19
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +215 -390
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +26 -6
- package/templates/skills/fullsolve/SKILL.md +3 -1
- package/templates/skills/loop/SKILL.md +38 -2
- package/templates/skills/merger/SKILL.md +2 -2
- package/templates/skills/qa/SKILL.md +337 -57
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +1 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- package/templates/skills/test/SKILL.md +1 -1
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
package/dist/src/lib/stacks.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stack detection and configuration
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync } from "fs";
|
|
4
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync } from "fs";
|
|
5
5
|
import { readdir } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
6
7
|
import { fileExists, readFile } from "./fs.js";
|
|
7
8
|
/**
|
|
8
9
|
* Directories to skip during multi-stack detection
|
|
@@ -30,6 +31,7 @@ export const PM_CONFIG = {
|
|
|
30
31
|
exec: "npx",
|
|
31
32
|
install: "npm install",
|
|
32
33
|
installSilent: "npm install --silent",
|
|
34
|
+
ciInstall: "npm ci",
|
|
33
35
|
addPkg: "npm install",
|
|
34
36
|
removePkg: "npm uninstall",
|
|
35
37
|
updatePkg: "npm update",
|
|
@@ -39,34 +41,62 @@ export const PM_CONFIG = {
|
|
|
39
41
|
exec: "bunx",
|
|
40
42
|
install: "bun install",
|
|
41
43
|
installSilent: "bun install --silent",
|
|
44
|
+
ciInstall: "bun install --frozen-lockfile",
|
|
42
45
|
addPkg: "bun add",
|
|
43
46
|
removePkg: "bun remove",
|
|
44
47
|
updatePkg: "bun update",
|
|
45
48
|
},
|
|
49
|
+
// Yarn 1 (classic) and Yarn 2+ (berry) are different CLIs sharing one
|
|
50
|
+
// lockfile name, so no single table can be right for both. This entry is the
|
|
51
|
+
// BERRY baseline; `resolvePackageManagerConfig(pm, root)` swaps in the classic
|
|
52
|
+
// spelling for the fields that differ. Read yarn commands through that
|
|
53
|
+
// resolver, never off this table, at any site that knows which project
|
|
54
|
+
// directory it is acting on (#871).
|
|
55
|
+
//
|
|
56
|
+
// Fields with no major-specific difference — `run`, `install`, `addPkg`,
|
|
57
|
+
// `removePkg` — are shared, so the resolver leaves them alone.
|
|
46
58
|
yarn: {
|
|
47
59
|
run: "yarn",
|
|
60
|
+
// Berry-only: `dlx` does not exist in Yarn 1. Resolver → `npx` for classic.
|
|
48
61
|
exec: "yarn dlx",
|
|
49
62
|
install: "yarn install",
|
|
63
|
+
// Left at the classic spelling deliberately. Berry's `yarn install` takes no
|
|
64
|
+
// `--silent`, but this field has NO consumer anywhere in the codebase, and
|
|
65
|
+
// guessing berry's quiet spelling without a yarn binary to check against
|
|
66
|
+
// would trade a dead-config wart for a real one — the same call
|
|
67
|
+
// new-feature.sh's `pm_quiet_flag` already made for pnpm/bun. Known
|
|
68
|
+
// remaining hybrid, and the one field the resolver does not fix.
|
|
50
69
|
installSilent: "yarn install --silent",
|
|
70
|
+
// Berry-only: Yarn 1 rejects `--immutable` and wants `--frozen-lockfile`.
|
|
71
|
+
// Resolver → YARN_CLASSIC_CI_INSTALL for classic.
|
|
72
|
+
ciInstall: "yarn install --immutable",
|
|
51
73
|
addPkg: "yarn add",
|
|
52
74
|
removePkg: "yarn remove",
|
|
53
|
-
|
|
75
|
+
// Berry renamed `yarn upgrade` to `yarn up`. This was the classic spelling
|
|
76
|
+
// until #871's follow-up, which made the whole entry berry-consistent —
|
|
77
|
+
// meaning the upgrade hint in version-check.ts was wrong for berry users.
|
|
78
|
+
// Resolver → `yarn upgrade` for classic.
|
|
79
|
+
updatePkg: "yarn up",
|
|
54
80
|
},
|
|
55
81
|
pnpm: {
|
|
56
82
|
run: "pnpm run",
|
|
57
83
|
exec: "pnpm dlx",
|
|
58
84
|
install: "pnpm install",
|
|
59
85
|
installSilent: "pnpm install --silent",
|
|
86
|
+
ciInstall: "pnpm install --frozen-lockfile",
|
|
60
87
|
addPkg: "pnpm add",
|
|
61
88
|
removePkg: "pnpm remove",
|
|
62
89
|
updatePkg: "pnpm update",
|
|
63
90
|
},
|
|
64
|
-
// Python package managers
|
|
91
|
+
// Python package managers. None of these has a distinct "frozen lockfile"
|
|
92
|
+
// install mode the way npm/pnpm/yarn/bun do (poetry install already honors
|
|
93
|
+
// poetry.lock), so ciInstall mirrors installSilent rather than inventing a flag.
|
|
65
94
|
pip: {
|
|
66
95
|
run: "python -m",
|
|
67
96
|
exec: "python -m",
|
|
68
97
|
install: "pip install",
|
|
69
98
|
installSilent: "pip install -q",
|
|
99
|
+
ciInstall: "pip install -q",
|
|
70
100
|
addPkg: "pip install",
|
|
71
101
|
removePkg: "pip uninstall",
|
|
72
102
|
updatePkg: "pip install --upgrade",
|
|
@@ -76,6 +106,7 @@ export const PM_CONFIG = {
|
|
|
76
106
|
exec: "poetry run",
|
|
77
107
|
install: "poetry install",
|
|
78
108
|
installSilent: "poetry install -q",
|
|
109
|
+
ciInstall: "poetry install -q",
|
|
79
110
|
addPkg: "poetry add",
|
|
80
111
|
removePkg: "poetry remove",
|
|
81
112
|
updatePkg: "poetry update",
|
|
@@ -85,6 +116,7 @@ export const PM_CONFIG = {
|
|
|
85
116
|
exec: "uvx",
|
|
86
117
|
install: "uv pip install",
|
|
87
118
|
installSilent: "uv pip install -q",
|
|
119
|
+
ciInstall: "uv pip install -q",
|
|
88
120
|
addPkg: "uv pip install",
|
|
89
121
|
removePkg: "uv pip uninstall",
|
|
90
122
|
updatePkg: "uv pip install --upgrade",
|
|
@@ -143,21 +175,228 @@ export async function detectPackageManager() {
|
|
|
143
175
|
/**
|
|
144
176
|
* Synchronous version of detectPackageManager for use in startup code.
|
|
145
177
|
* Only checks JS lockfiles (not Python) since sequant is a Node.js tool.
|
|
178
|
+
*
|
|
179
|
+
* @param root Directory to look for lockfiles in. Defaults to the process cwd,
|
|
180
|
+
* which is what startup code wants; callers that operate on a specific
|
|
181
|
+
* repository (e.g. merge-check) must pass that repo's root instead.
|
|
146
182
|
*/
|
|
147
|
-
export function detectPackageManagerSync() {
|
|
183
|
+
export function detectPackageManagerSync(root = process.cwd()) {
|
|
148
184
|
for (const { file, pm } of LOCKFILE_PRIORITY) {
|
|
149
|
-
if (existsSync(file)) {
|
|
185
|
+
if (existsSync(join(root, file))) {
|
|
150
186
|
return pm;
|
|
151
187
|
}
|
|
152
188
|
}
|
|
153
189
|
return "npm";
|
|
154
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the package manager for a directory, preferring a declared value and
|
|
193
|
+
* falling back to live lockfile detection.
|
|
194
|
+
*
|
|
195
|
+
* The declared value is a *snapshot* — the worktree manifest records whatever
|
|
196
|
+
* `sequant init` saw, so it is absent on manifest-less or pre-init trees. The
|
|
197
|
+
* old spelling at those call sites was `(declared as keyof typeof PM_CONFIG)
|
|
198
|
+
* || "npm"`, which assumed npm whenever the snapshot was missing while the
|
|
199
|
+
* shell provisioning path (`templates/scripts/new-feature.sh`, PM-aware since
|
|
200
|
+
* #847) detected live from the lockfile. On a pnpm/yarn/bun project without a
|
|
201
|
+
* manifest the two paths therefore disagreed about the project's package
|
|
202
|
+
* manager — the dual-producer drift class of #833. Detecting here makes the
|
|
203
|
+
* TypeScript path agree with the shell path by construction (#870).
|
|
204
|
+
*
|
|
205
|
+
* The own-property membership test also retires that unchecked cast: a
|
|
206
|
+
* declared value outside `PackageManager` (a hand-edited manifest, a future
|
|
207
|
+
* `"npm@10"` spelling) used to index `PM_CONFIG` to `undefined` and throw on
|
|
208
|
+
* the next property access. Such a value now routes to detection instead.
|
|
209
|
+
* Python managers (`pip`/`poetry`/`uv`) are `PM_CONFIG` keys, so a declared
|
|
210
|
+
* Python manager still wins over the JS-only detector.
|
|
211
|
+
*
|
|
212
|
+
* The test is `hasOwnProperty`, deliberately not `in`: `in` walks the
|
|
213
|
+
* prototype chain, so `"toString"`, `"constructor"`, and `"__proto__"` would
|
|
214
|
+
* all pass it and be returned as if they named a package manager. Indexing
|
|
215
|
+
* `PM_CONFIG` with one yields an inherited function (or `Object.prototype`)
|
|
216
|
+
* whose `ciInstall` is `undefined`, crashing the caller on `.split(" ")` —
|
|
217
|
+
* reintroducing the exact failure this guard exists to prevent.
|
|
218
|
+
*
|
|
219
|
+
* @param declared Package manager recorded in the manifest, if any
|
|
220
|
+
* @param root Directory whose lockfiles decide the fallback
|
|
221
|
+
*/
|
|
222
|
+
export function resolvePackageManager(declared, root) {
|
|
223
|
+
if (declared && Object.prototype.hasOwnProperty.call(PM_CONFIG, declared)) {
|
|
224
|
+
return declared;
|
|
225
|
+
}
|
|
226
|
+
return detectPackageManagerSync(root);
|
|
227
|
+
}
|
|
155
228
|
/**
|
|
156
229
|
* Get package manager command configuration
|
|
157
230
|
*/
|
|
158
231
|
export function getPackageManagerCommands(pm) {
|
|
159
232
|
return PM_CONFIG[pm];
|
|
160
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Frozen install understood by Yarn 1 (classic).
|
|
236
|
+
*
|
|
237
|
+
* Yarn 2+ (berry) renamed this flag to `--immutable`, which is what
|
|
238
|
+
* `PM_CONFIG.yarn.ciInstall` carries. Both yarn spellings therefore exist, and
|
|
239
|
+
* both must appear in `new-feature.sh`'s `pm_ci_install()` table — the drift
|
|
240
|
+
* guard in `__tests__/new-feature-frozen-install.integration.test.ts` asserts
|
|
241
|
+
* exactly that, which is why this is a named export rather than a literal
|
|
242
|
+
* buried in `resolvePackageManagerConfig` (#871).
|
|
243
|
+
*/
|
|
244
|
+
export const YARN_CLASSIC_CI_INSTALL = "yarn install --frozen-lockfile";
|
|
245
|
+
/**
|
|
246
|
+
* Yarn 1 (classic) spellings for every `PM_CONFIG.yarn` field whose berry value
|
|
247
|
+
* is wrong on classic. Applied by {@link resolvePackageManagerConfig}.
|
|
248
|
+
*
|
|
249
|
+
* `exec` is `npx` rather than a `yarn` subcommand because Yarn 1 has no
|
|
250
|
+
* `dlx` equivalent at all — npm ships with node, so `npx` is the run-a-binary
|
|
251
|
+
* -without-installing story a Yarn 1 project actually has.
|
|
252
|
+
*
|
|
253
|
+
* `installSilent` is deliberately absent — see the note on `PM_CONFIG.yarn`.
|
|
254
|
+
*/
|
|
255
|
+
const YARN_CLASSIC_OVERRIDES = {
|
|
256
|
+
ciInstall: YARN_CLASSIC_CI_INSTALL,
|
|
257
|
+
exec: "npx",
|
|
258
|
+
updatePkg: "yarn upgrade",
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Bytes of `yarn.lock` scanned for its version header.
|
|
262
|
+
*
|
|
263
|
+
* Matches the `head -c` byte count in `new-feature.sh`'s `detect_yarn_major`.
|
|
264
|
+
* Keeping the scanned region identical is what makes the two implementations
|
|
265
|
+
* agree by construction rather than by inspection: a whole-file `grep` in the
|
|
266
|
+
* shell against a prefix-only read in TypeScript would silently diverge on a
|
|
267
|
+
* lockfile that happens to mention the header string further down.
|
|
268
|
+
*/
|
|
269
|
+
const YARN_LOCK_HEADER_BYTES = 1024;
|
|
270
|
+
/**
|
|
271
|
+
* Read the `packageManager` pin's yarn major from `package.json`, if declared.
|
|
272
|
+
*
|
|
273
|
+
* Deliberately a regex rather than `JSON.parse`: the shell path cannot parse
|
|
274
|
+
* JSON without adding a `jq` dependency, so both paths match on text. A
|
|
275
|
+
* non-numeric pin (`yarn@stable`, `yarn@berry`) does not match and falls
|
|
276
|
+
* through to the next signal.
|
|
277
|
+
*
|
|
278
|
+
* Matching on text does NOT make the two paths agree for free — `\s*` here
|
|
279
|
+
* spans newlines and `.match` returns the first hit, neither of which a
|
|
280
|
+
* line-oriented `sed`/`grep` gives you. `detect_yarn_major` in
|
|
281
|
+
* `new-feature.sh` collapses newlines and takes the first match specifically
|
|
282
|
+
* to line up with this; see its comment for the two inputs that diverged
|
|
283
|
+
* before it did.
|
|
284
|
+
*
|
|
285
|
+
* @returns the declared major, or null when nothing is declared
|
|
286
|
+
*/
|
|
287
|
+
function readDeclaredYarnMajor(root) {
|
|
288
|
+
let raw;
|
|
289
|
+
try {
|
|
290
|
+
raw = readFileSync(join(root, "package.json"), "utf8");
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const match = raw.match(/"packageManager"\s*:\s*"yarn@v?(\d+)/);
|
|
296
|
+
if (!match) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
// Every major above 1 is berry and shares berry's CLI surface, so the return
|
|
300
|
+
// type is the *behavioral* split (classic vs. berry), not the literal major.
|
|
301
|
+
return match[1] === "1" ? 1 : 2;
|
|
302
|
+
}
|
|
303
|
+
/** Read the first {@link YARN_LOCK_HEADER_BYTES} of `yarn.lock` ("" if absent). */
|
|
304
|
+
function readYarnLockHeader(root) {
|
|
305
|
+
let fd;
|
|
306
|
+
try {
|
|
307
|
+
fd = openSync(join(root, "yarn.lock"), "r");
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return "";
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const buffer = Buffer.alloc(YARN_LOCK_HEADER_BYTES);
|
|
314
|
+
const bytesRead = readSync(fd, buffer, 0, YARN_LOCK_HEADER_BYTES, 0);
|
|
315
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
closeSync(fd);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Detect whether a directory is a Yarn 1 (classic) or Yarn 2+ (berry) project.
|
|
323
|
+
*
|
|
324
|
+
* Yarn 1 and berry both use `yarn.lock`, so `detectPackageManagerSync` cannot
|
|
325
|
+
* tell them apart — it answers "yarn", and this answers "which yarn". They are
|
|
326
|
+
* separate axes on purpose: adding a `yarn1` member to `PackageManager` would
|
|
327
|
+
* leak a synthetic name into settings (`pmRun`), doctor output, and templates
|
|
328
|
+
* for what is, today, a one-flag difference.
|
|
329
|
+
*
|
|
330
|
+
* Signals, in order of authority. The ordering follows a single rule: **flag
|
|
331
|
+
* acceptance is decided by the yarn binary that runs, not by the lockfile it
|
|
332
|
+
* reads.** So a Corepack pin outranks the lockfile header — a yarn-1 lockfile
|
|
333
|
+
* under `packageManager: "yarn@4"` still gets berry's `--immutable`, because
|
|
334
|
+
* yarn 4 is what will execute and yarn 4 rejects `--frozen-lockfile`.
|
|
335
|
+
*
|
|
336
|
+
* 1. `packageManager: "yarn@<major>"` in `package.json` — Corepack pins the
|
|
337
|
+
* binary.
|
|
338
|
+
* 2. `.yarnrc.yml` — a berry-only config file; yarn 1 reads `.yarnrc`.
|
|
339
|
+
* 3. `yarn.lock` header — yarn 1 writes `# yarn lockfile v1`; berry writes
|
|
340
|
+
* `__metadata:` instead.
|
|
341
|
+
* 4. Nothing recognizable → berry, the pre-#871 assumption. Staying with it
|
|
342
|
+
* keeps a contentless or unreadable `yarn.lock` behaving exactly as it did
|
|
343
|
+
* before this function existed.
|
|
344
|
+
*
|
|
345
|
+
* Mirrored by `detect_yarn_major` in `templates/scripts/new-feature.sh` (#871).
|
|
346
|
+
*
|
|
347
|
+
* @internal Exported for testing — production code should go through
|
|
348
|
+
* {@link resolvePackageManagerConfig}, whose whole job is to apply this answer.
|
|
349
|
+
* The export exists so the cross-path agreement test can run this and the shell
|
|
350
|
+
* mirror against the same fixtures, which is the gate keeping the two in step.
|
|
351
|
+
*
|
|
352
|
+
* @param root Directory to inspect. Defaults to the process cwd.
|
|
353
|
+
* @returns 1 for Yarn 1 (classic), 2 for Yarn 2+ (berry)
|
|
354
|
+
*/
|
|
355
|
+
export function detectYarnMajor(root = process.cwd()) {
|
|
356
|
+
const declared = readDeclaredYarnMajor(root);
|
|
357
|
+
if (declared !== null) {
|
|
358
|
+
return declared;
|
|
359
|
+
}
|
|
360
|
+
if (existsSync(join(root, ".yarnrc.yml"))) {
|
|
361
|
+
return 2;
|
|
362
|
+
}
|
|
363
|
+
if (readYarnLockHeader(root).includes("yarn lockfile v1")) {
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
return 2;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Package manager commands for `pm`, with directory-dependent commands resolved
|
|
370
|
+
* against `root`.
|
|
371
|
+
*
|
|
372
|
+
* A drop-in replacement for `PM_CONFIG[pm]` at every call site that knows which
|
|
373
|
+
* project directory it is acting on. `PM_CONFIG.yarn` is the berry baseline, so
|
|
374
|
+
* for a Yarn 1 project this substitutes every field whose berry spelling is
|
|
375
|
+
* wrong on classic — `ciInstall`, `exec`, and `updatePkg` (see
|
|
376
|
+
* {@link YARN_CLASSIC_OVERRIDES}). Every other package manager, and every
|
|
377
|
+
* major-agnostic yarn field, is returned untouched.
|
|
378
|
+
*
|
|
379
|
+
* Returning the whole config rather than one resolved string is what makes that
|
|
380
|
+
* cheap: call sites that thread a `PackageManagerConfig` onward — merge-check's
|
|
381
|
+
* combined-branch test reads `ciInstall` for three commands and two user-facing
|
|
382
|
+
* messages — stay correct with a single-line change, and each newly discovered
|
|
383
|
+
* classic/berry split is one entry in the overrides table rather than a new
|
|
384
|
+
* call-site edit.
|
|
385
|
+
*
|
|
386
|
+
* Resolve against the tree you are about to act on, not against wherever the
|
|
387
|
+
* process started: `root` decides the answer, and in merge-check the combined
|
|
388
|
+
* (post-merge) state can disagree with the pre-merge one.
|
|
389
|
+
*
|
|
390
|
+
* @param pm Package manager identity, e.g. from `detectPackageManagerSync`
|
|
391
|
+
* @param root Directory whose files decide the directory-dependent commands
|
|
392
|
+
*/
|
|
393
|
+
export function resolvePackageManagerConfig(pm, root) {
|
|
394
|
+
const config = PM_CONFIG[pm];
|
|
395
|
+
if (pm !== "yarn" || detectYarnMajor(root) !== 1) {
|
|
396
|
+
return config;
|
|
397
|
+
}
|
|
398
|
+
return { ...config, ...YARN_CLASSIC_OVERRIDES };
|
|
399
|
+
}
|
|
161
400
|
export const STACKS = {
|
|
162
401
|
nextjs: {
|
|
163
402
|
name: "nextjs",
|
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Template management - copy and process templates
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* How well a candidate path matches "the templates root of *this* package".
|
|
6
|
+
*
|
|
7
|
+
* `package-root` outranks `exists` because the two offsets are not mutually
|
|
8
|
+
* exclusive: from a source tree at `<repo>/src/lib`, the *compiled* offset
|
|
9
|
+
* resolves to `<repo>/../templates` — a directory one level **above** the repo.
|
|
10
|
+
* A bare existence probe would happily bind to an unrelated `templates/` that
|
|
11
|
+
* happens to sit beside the checkout. Requiring the candidate's parent to be
|
|
12
|
+
* the sequant package root removes that ambiguity without walking the tree.
|
|
13
|
+
*/
|
|
14
|
+
export type TemplatesCandidateRank = "package-root" | "exists" | "missing";
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the templates root relative to a module directory, given a ranking
|
|
17
|
+
* function.
|
|
18
|
+
*
|
|
19
|
+
* Split out from `getTemplatesDir` purely so both layouts are testable: a test
|
|
20
|
+
* cannot relocate `import.meta.url`, so without injection the "works from both
|
|
21
|
+
* layouts" assertion could only ever exercise whichever layout the test runner
|
|
22
|
+
* happens to use.
|
|
23
|
+
*
|
|
24
|
+
* Resolution order: the first `package-root` candidate, else the first that
|
|
25
|
+
* merely `exists` (keeps any layout that works today working, even one whose
|
|
26
|
+
* package root is not where we expect), else the **compiled** offset — so the
|
|
27
|
+
* caller's error message names the canonical expected location rather than a
|
|
28
|
+
* source-tree guess.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveTemplatesDirFrom(baseDir: string, rank: (candidate: string) => TemplatesCandidateRank): string;
|
|
4
31
|
export declare function getTemplatesDir(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the templates root and fail loudly when it does not exist.
|
|
34
|
+
*
|
|
35
|
+
* A missing templates *root* means the install is broken: every `copyDir` call
|
|
36
|
+
* below would hit `copyDir`'s per-directory ENOENT skip, no-op, and let the
|
|
37
|
+
* caller print a success message over an empty tree (#822). That skip is
|
|
38
|
+
* deliberate for individual subdirectories — a stack may legitimately ship
|
|
39
|
+
* without `memory/` — but it must not absorb the whole source tree.
|
|
40
|
+
*
|
|
41
|
+
* Throws rather than printing so the lib layer stays free of presentation;
|
|
42
|
+
* commands catch and render.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertTemplatesDirExists(): Promise<string>;
|
|
5
45
|
/**
|
|
6
46
|
* Process template variables in content
|
|
7
47
|
*/
|
|
@@ -52,6 +92,25 @@ export interface TemplateChange {
|
|
|
52
92
|
/** Unified-ish diff (installed → rendered), only set for `modified` */
|
|
53
93
|
diff?: string;
|
|
54
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Map a bundled template path to the location `copyTemplates` installs it to,
|
|
97
|
+
* or `null` for templates the copy/diff cycle does not manage.
|
|
98
|
+
*
|
|
99
|
+
* This mirrors the routing in `copyTemplates` (write time) so
|
|
100
|
+
* `computeTemplateChanges` (diff time) can never report drift on a file init
|
|
101
|
+
* deliberately places elsewhere — `.claude/mcp.json`, `.claude/relay/*`, and
|
|
102
|
+
* `.claude/scripts/*` read as phantom "new" files on every fresh install when
|
|
103
|
+
* the diff assumed a flat `.claude/<relpath>` layout. #708 gives the same
|
|
104
|
+
* guarantee for the template *variables*; this covers the *destinations*.
|
|
105
|
+
*
|
|
106
|
+
* - `templates/mcp.json` → `null`: the project `.mcp.json` is generated and
|
|
107
|
+
* version-pinned by `mcp-config.ts` (`syncSequantMcpPin`), never copied.
|
|
108
|
+
* - `templates/relay/` → `null`: not installed by `copyTemplates`.
|
|
109
|
+
* - `templates/scripts/` → `scripts/dev/`: symlinked on POSIX, copied on
|
|
110
|
+
* Windows / `--no-symlinks`.
|
|
111
|
+
* - everything else → `.claude/<relpath>`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function templateDestination(templatePath: string): string | null;
|
|
55
114
|
/**
|
|
56
115
|
* Compare bundled template content against what's installed under `.claude/`.
|
|
57
116
|
*
|
|
@@ -83,6 +142,15 @@ export interface CopyTemplatesOptions {
|
|
|
83
142
|
noSymlinks?: boolean;
|
|
84
143
|
/** Force replacement of existing files/symlinks */
|
|
85
144
|
force?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Opt in to overwriting in-place customizations (files in `CUSTOMIZABLE_FILES`,
|
|
147
|
+
* e.g. the constitution) that already exist and differ from the rendered
|
|
148
|
+
* template. Deliberately separate from `force`: `force` refreshes the managed
|
|
149
|
+
* skills/agents/hooks trees, but that always-on tree overwrite must NOT imply
|
|
150
|
+
* consent to clobber user-owned files. Only an explicit user `--force` sets
|
|
151
|
+
* this. A missing or identical customizable file is written regardless (#814).
|
|
152
|
+
*/
|
|
153
|
+
overwriteCustomizable?: boolean;
|
|
86
154
|
/** Additional stacks to include in constitution notes (for multi-stack projects) */
|
|
87
155
|
additionalStacks?: string[];
|
|
88
156
|
}
|
|
@@ -102,4 +170,10 @@ export declare function symlinkDir(srcDir: string, destDir: string, options?: {
|
|
|
102
170
|
export declare function copyTemplates(stack: string, tokens?: Record<string, string>, options?: CopyTemplatesOptions): Promise<{
|
|
103
171
|
scriptsSymlinked: boolean;
|
|
104
172
|
symlinkResults?: SymlinkResult[];
|
|
173
|
+
/**
|
|
174
|
+
* Customizable files that already existed, differed from the rendered
|
|
175
|
+
* template, and were left untouched because `overwriteCustomizable` was not
|
|
176
|
+
* set. Normalized to forward slashes so callers can report them verbatim.
|
|
177
|
+
*/
|
|
178
|
+
preservedCustomizable: string[];
|
|
105
179
|
}>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Template management - copy and process templates
|
|
3
3
|
*/
|
|
4
|
-
import { readdir, chmod } from "fs/promises";
|
|
4
|
+
import { readdir, chmod, stat } from "fs/promises";
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
6
|
import { join, dirname, relative, isAbsolute } from "path";
|
|
6
7
|
import { fileURLToPath } from "url";
|
|
7
8
|
import { diffLines } from "diff";
|
|
@@ -11,18 +12,122 @@ const SKILLS_VERSION_PATH = ".claude/skills/.sequant-version";
|
|
|
11
12
|
import { getStackConfig, getStackNotes, getMultiStackNotes } from "./stacks.js";
|
|
12
13
|
import { isNativeWindows } from "./system.js";
|
|
13
14
|
import { getProjectName } from "./project-name.js";
|
|
15
|
+
/**
|
|
16
|
+
* Offsets from this module's directory to the bundled `templates/` root, in
|
|
17
|
+
* probe order. This module lives at `src/lib/templates.ts`, so it sits three
|
|
18
|
+
* levels below the package root once compiled (`dist/src/lib/templates.js`) but
|
|
19
|
+
* only two when executed straight from source under `tsx` (`src/lib/`).
|
|
20
|
+
*
|
|
21
|
+
* The compiled offset is listed first so consumers — who always run the compiled
|
|
22
|
+
* binary — keep byte-identical behavior. See #822.
|
|
23
|
+
*/
|
|
24
|
+
const TEMPLATES_DIR_OFFSETS = [
|
|
25
|
+
["..", "..", "..", "templates"], // compiled: dist/src/lib → <pkg>/templates
|
|
26
|
+
["..", "..", "templates"], // source (tsx): src/lib → <repo>/templates
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the templates root relative to a module directory, given a ranking
|
|
30
|
+
* function.
|
|
31
|
+
*
|
|
32
|
+
* Split out from `getTemplatesDir` purely so both layouts are testable: a test
|
|
33
|
+
* cannot relocate `import.meta.url`, so without injection the "works from both
|
|
34
|
+
* layouts" assertion could only ever exercise whichever layout the test runner
|
|
35
|
+
* happens to use.
|
|
36
|
+
*
|
|
37
|
+
* Resolution order: the first `package-root` candidate, else the first that
|
|
38
|
+
* merely `exists` (keeps any layout that works today working, even one whose
|
|
39
|
+
* package root is not where we expect), else the **compiled** offset — so the
|
|
40
|
+
* caller's error message names the canonical expected location rather than a
|
|
41
|
+
* source-tree guess.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveTemplatesDirFrom(baseDir, rank) {
|
|
44
|
+
const candidates = TEMPLATES_DIR_OFFSETS.map((offset) => join(baseDir, ...offset));
|
|
45
|
+
const ranked = candidates.map((candidate) => ({
|
|
46
|
+
candidate,
|
|
47
|
+
rank: rank(candidate),
|
|
48
|
+
}));
|
|
49
|
+
return (ranked.find((c) => c.rank === "package-root")?.candidate ??
|
|
50
|
+
ranked.find((c) => c.rank === "exists")?.candidate ??
|
|
51
|
+
candidates[0]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Rank a candidate by checking whether it exists and whether its parent is the
|
|
55
|
+
* sequant package root. Anchoring on `package.json` mirrors how every other
|
|
56
|
+
* root resolver in this codebase locates the package (`version.ts`,
|
|
57
|
+
* `manifest.ts`, `bin/preflight.ts`).
|
|
58
|
+
*/
|
|
59
|
+
function rankTemplatesCandidate(candidate) {
|
|
60
|
+
if (!existsSync(candidate)) {
|
|
61
|
+
return "missing";
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(readFileSync(join(candidate, "..", "package.json"), "utf-8"));
|
|
65
|
+
if (pkg.name === "sequant") {
|
|
66
|
+
return "package-root";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// No readable/parseable package.json beside it — still a usable directory,
|
|
71
|
+
// just not a positively identified package root.
|
|
72
|
+
}
|
|
73
|
+
return "exists";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Memoized bundled-templates path. Resolution is now stat-backed rather than a
|
|
77
|
+
* pure string join, and `getTemplateContent` calls it once per template file
|
|
78
|
+
* during a drift scan — ~2ms per scan uncached, on a pre-flight path #708
|
|
79
|
+
* deliberately keeps in the 2-5ms range. The install layout cannot change
|
|
80
|
+
* within a process, so caching it is safe. Only the *bundled* resolution is
|
|
81
|
+
* cached; the env override is re-read every call, so tests that set and unset
|
|
82
|
+
* `SEQUANT_TEMPLATES_DIR` are unaffected.
|
|
83
|
+
*/
|
|
84
|
+
let cachedBundledTemplatesDir;
|
|
14
85
|
// Get the package templates directory
|
|
15
86
|
export function getTemplatesDir() {
|
|
16
87
|
// Allow overriding the templates source (used by tests; also lets the dir be
|
|
17
|
-
// relocated without relying on the compiled-output layout below).
|
|
88
|
+
// relocated without relying on the compiled-output layout below). Returned
|
|
89
|
+
// verbatim — the override is authoritative and is never probed, so a bad
|
|
90
|
+
// value surfaces at `assertTemplatesDirExists` rather than silently falling
|
|
91
|
+
// back to the bundled tree.
|
|
18
92
|
if (process.env.SEQUANT_TEMPLATES_DIR) {
|
|
19
93
|
return process.env.SEQUANT_TEMPLATES_DIR;
|
|
20
94
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
95
|
+
if (cachedBundledTemplatesDir === undefined) {
|
|
96
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
cachedBundledTemplatesDir = resolveTemplatesDirFrom(__dirname, rankTemplatesCandidate);
|
|
98
|
+
}
|
|
99
|
+
return cachedBundledTemplatesDir;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the templates root and fail loudly when it does not exist.
|
|
103
|
+
*
|
|
104
|
+
* A missing templates *root* means the install is broken: every `copyDir` call
|
|
105
|
+
* below would hit `copyDir`'s per-directory ENOENT skip, no-op, and let the
|
|
106
|
+
* caller print a success message over an empty tree (#822). That skip is
|
|
107
|
+
* deliberate for individual subdirectories — a stack may legitimately ship
|
|
108
|
+
* without `memory/` — but it must not absorb the whole source tree.
|
|
109
|
+
*
|
|
110
|
+
* Throws rather than printing so the lib layer stays free of presentation;
|
|
111
|
+
* commands catch and render.
|
|
112
|
+
*/
|
|
113
|
+
export async function assertTemplatesDirExists() {
|
|
114
|
+
const templatesDir = getTemplatesDir();
|
|
115
|
+
// A *directory* check, not a bare existence check: a stray file at that path
|
|
116
|
+
// would pass `access()` and then fail deeper in `readdir` with the same silent
|
|
117
|
+
// ENOTDIR-shaped confusion this guard exists to prevent.
|
|
118
|
+
let isDirectory;
|
|
119
|
+
try {
|
|
120
|
+
isDirectory = (await stat(templatesDir)).isDirectory();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
isDirectory = false;
|
|
124
|
+
}
|
|
125
|
+
if (isDirectory) {
|
|
126
|
+
return templatesDir;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Bundled templates directory not found: ${templatesDir}\n` +
|
|
129
|
+
"This usually means the Sequant install is incomplete or was run from an unexpected layout.\n" +
|
|
130
|
+
"Reinstall sequant, or set SEQUANT_TEMPLATES_DIR to the templates/ directory.");
|
|
26
131
|
}
|
|
27
132
|
/**
|
|
28
133
|
* Process template variables in content
|
|
@@ -112,6 +217,35 @@ export async function buildTemplateVariables(stack, tokens, options = {}) {
|
|
|
112
217
|
STACK_NOTES: stackNotes,
|
|
113
218
|
};
|
|
114
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Map a bundled template path to the location `copyTemplates` installs it to,
|
|
222
|
+
* or `null` for templates the copy/diff cycle does not manage.
|
|
223
|
+
*
|
|
224
|
+
* This mirrors the routing in `copyTemplates` (write time) so
|
|
225
|
+
* `computeTemplateChanges` (diff time) can never report drift on a file init
|
|
226
|
+
* deliberately places elsewhere — `.claude/mcp.json`, `.claude/relay/*`, and
|
|
227
|
+
* `.claude/scripts/*` read as phantom "new" files on every fresh install when
|
|
228
|
+
* the diff assumed a flat `.claude/<relpath>` layout. #708 gives the same
|
|
229
|
+
* guarantee for the template *variables*; this covers the *destinations*.
|
|
230
|
+
*
|
|
231
|
+
* - `templates/mcp.json` → `null`: the project `.mcp.json` is generated and
|
|
232
|
+
* version-pinned by `mcp-config.ts` (`syncSequantMcpPin`), never copied.
|
|
233
|
+
* - `templates/relay/` → `null`: not installed by `copyTemplates`.
|
|
234
|
+
* - `templates/scripts/` → `scripts/dev/`: symlinked on POSIX, copied on
|
|
235
|
+
* Windows / `--no-symlinks`.
|
|
236
|
+
* - everything else → `.claude/<relpath>`.
|
|
237
|
+
*/
|
|
238
|
+
export function templateDestination(templatePath) {
|
|
239
|
+
const normalized = templatePath.replace(/\\/g, "/");
|
|
240
|
+
if (normalized === "templates/mcp.json")
|
|
241
|
+
return null;
|
|
242
|
+
if (normalized.startsWith("templates/relay/"))
|
|
243
|
+
return null;
|
|
244
|
+
if (normalized.startsWith("templates/scripts/")) {
|
|
245
|
+
return normalized.replace("templates/scripts/", "scripts/dev/");
|
|
246
|
+
}
|
|
247
|
+
return normalized.replace("templates/", ".claude/");
|
|
248
|
+
}
|
|
115
249
|
/**
|
|
116
250
|
* Compare bundled template content against what's installed under `.claude/`.
|
|
117
251
|
*
|
|
@@ -126,12 +260,13 @@ export async function computeTemplateChanges(stack, tokens, options = {}) {
|
|
|
126
260
|
const templateFiles = await listTemplateFiles();
|
|
127
261
|
const changes = [];
|
|
128
262
|
for (const templatePath of templateFiles) {
|
|
129
|
-
//
|
|
130
|
-
// separator
|
|
131
|
-
//
|
|
132
|
-
const localPath = templatePath
|
|
133
|
-
|
|
134
|
-
|
|
263
|
+
// templateDestination normalizes separators (listTemplateFiles builds
|
|
264
|
+
// paths with the OS separator, backslashes on Windows — #708) and applies
|
|
265
|
+
// the same routing copyTemplates uses at write time.
|
|
266
|
+
const localPath = templateDestination(templatePath);
|
|
267
|
+
if (localPath === null) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
135
270
|
// Skip .local files (user customizations are never overwritten)
|
|
136
271
|
if (localPath.includes(".local/")) {
|
|
137
272
|
continue;
|
|
@@ -165,8 +300,11 @@ export async function computeTemplateChanges(stack, tokens, options = {}) {
|
|
|
165
300
|
// auto-skipped above because it lives under `.local/`. The directive sits at
|
|
166
301
|
// the top, not end-of-file, so it fires reliably even in 3000-line skills.
|
|
167
302
|
// See docs/guides/customization.md.
|
|
168
|
-
|
|
169
|
-
|
|
303
|
+
// Only `.claude/` files have `.local/` twins; for `scripts/dev/` paths the
|
|
304
|
+
// replace would be a no-op and the file would shadow itself as its own
|
|
305
|
+
// "override".
|
|
306
|
+
const hasLocalOverride = localPath.startsWith(".claude/") &&
|
|
307
|
+
(await fileExists(localPath.replace(".claude/", ".claude/.local/")));
|
|
170
308
|
if (hasLocalOverride || isCustomizableFile(localPath)) {
|
|
171
309
|
changes.push({
|
|
172
310
|
path: localPath,
|
|
@@ -293,6 +431,9 @@ export async function copyTemplates(stack, tokens, options = {}) {
|
|
|
293
431
|
const templatesDir = getTemplatesDir();
|
|
294
432
|
// Single source of truth for template variables (shared with the diff path)
|
|
295
433
|
const variables = await buildTemplateVariables(stack, tokens, options);
|
|
434
|
+
// Customizable files skipped on the write path (see copyDir), surfaced to the
|
|
435
|
+
// caller so it can report them without a second diff pass (#814).
|
|
436
|
+
const preservedCustomizable = [];
|
|
296
437
|
async function copyDir(srcDir, destDir) {
|
|
297
438
|
try {
|
|
298
439
|
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
@@ -307,6 +448,20 @@ export async function copyTemplates(stack, tokens, options = {}) {
|
|
|
307
448
|
// Read, process, and write
|
|
308
449
|
let content = await readFile(srcPath);
|
|
309
450
|
content = processTemplate(content, variables);
|
|
451
|
+
// Protect in-place customizations on the write path. A file in
|
|
452
|
+
// CUSTOMIZABLE_FILES that already exists and differs from the
|
|
453
|
+
// rendered template is preserved unless the caller explicitly opted
|
|
454
|
+
// in via `overwriteCustomizable`. A missing file (fresh install) or
|
|
455
|
+
// an identical one falls through and is written as usual (#814).
|
|
456
|
+
if (!options.overwriteCustomizable &&
|
|
457
|
+
isCustomizableFile(destPath) &&
|
|
458
|
+
(await fileExists(destPath))) {
|
|
459
|
+
const existing = await readFile(destPath);
|
|
460
|
+
if (existing !== content) {
|
|
461
|
+
preservedCustomizable.push(destPath.replace(/\\/g, "/"));
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
310
465
|
await writeFile(destPath, content);
|
|
311
466
|
// Make shell scripts executable
|
|
312
467
|
if (entry.name.endsWith(".sh")) {
|
|
@@ -352,5 +507,5 @@ export async function copyTemplates(stack, tokens, options = {}) {
|
|
|
352
507
|
}
|
|
353
508
|
// Write skills version marker for sync detection
|
|
354
509
|
await writeFile(SKILLS_VERSION_PATH, getPackageVersion());
|
|
355
|
-
return { scriptsSymlinked, symlinkResults };
|
|
510
|
+
return { scriptsSymlinked, symlinkResults, preservedCustomizable };
|
|
356
511
|
}
|