sequant 2.9.0 → 2.11.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 +20 -5
- package/dist/bin/cli.js +143 -18
- 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.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- 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 +31 -3
- package/dist/src/commands/ready.js +53 -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 +38 -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/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- 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 +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -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 +87 -0
- package/dist/src/lib/settings.js +106 -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 +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -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 +552 -60
- 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 +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -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 +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- 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 +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -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 +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -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/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -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 +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- 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 +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- 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 +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- 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 +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- 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/templates/agents/sequant-explorer.md +0 -24
|
@@ -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
|
}
|
|
@@ -100,9 +100,17 @@ export declare function extractTestBlocks(content: string): Array<{
|
|
|
100
100
|
style: "it" | "test";
|
|
101
101
|
}>;
|
|
102
102
|
/**
|
|
103
|
-
* Check if a test block
|
|
103
|
+
* Check if a test block calls production code. A block counts as non-tautological
|
|
104
|
+
* when it references an imported production function, directly spawns the
|
|
105
|
+
* project's build output, or calls a helper that (transitively) does so.
|
|
106
|
+
*
|
|
107
|
+
* @param productionHandles Names of describe/module-scope helpers that reach
|
|
108
|
+
* production — by spawning the project's executable code or by calling an
|
|
109
|
+
* imported production function (see {@link collectProductionHandles}).
|
|
110
|
+
* @param buildOutputVars Variable names bound to a build-output path (see
|
|
111
|
+
* {@link collectBuildOutputVars}).
|
|
104
112
|
*/
|
|
105
|
-
export declare function testBlockCallsProductionCode(body: string, importedFunctions: ImportedFunction[]): boolean;
|
|
113
|
+
export declare function testBlockCallsProductionCode(body: string, importedFunctions: ImportedFunction[], productionHandles?: string[], buildOutputVars?: string[]): boolean;
|
|
106
114
|
/**
|
|
107
115
|
* Check if a file opts out of tautology detection via pragma comment.
|
|
108
116
|
*
|
|
@@ -341,20 +341,219 @@ function escapeRegex(str) {
|
|
|
341
341
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
342
342
|
}
|
|
343
343
|
/**
|
|
344
|
-
*
|
|
344
|
+
* Build a whole-identifier reference matcher for `name`, bounded by
|
|
345
|
+
* non-identifier chars ([\w$]). Catches direct calls, method calls, callback
|
|
346
|
+
* references, and assignments while rejecting substring matches.
|
|
345
347
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
function referenceMatcher(name) {
|
|
349
|
+
return new RegExp(`(?<![\\w$])${escapeRegex(name)}(?![\\w$])`);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Child-process spawn functions. A test that spawns the project's *build
|
|
353
|
+
* output* is exercising production code across a process boundary that static
|
|
354
|
+
* import analysis cannot see through — issue #885.
|
|
355
|
+
*
|
|
356
|
+
* Two alternates by name ambiguity: the long names are unambiguous
|
|
357
|
+
* child-process API and match anywhere, including method-style calls from a
|
|
358
|
+
* namespace import (`cp.execSync(...)`). The short names (`exec`, `spawn`,
|
|
359
|
+
* `fork`) collide with unrelated methods — `RegExp.prototype.exec` most of
|
|
360
|
+
* all — so they must not be preceded by `.` (or an identifier char). The
|
|
361
|
+
* cost is that method-style callback `cp.exec(...)` no longer counts; tests
|
|
362
|
+
* that spawn build output overwhelmingly use the sync variants, and a false
|
|
363
|
+
* tautology report is loud where the `.exec()` collision was silent.
|
|
364
|
+
*/
|
|
365
|
+
const SPAWN_PATTERN = /(?:\b(?:execFileSync|spawnSync|execSync|execFile)\s*\(|(?<![\w$.])(?:exec|fork|spawn)\s*\()/;
|
|
366
|
+
/**
|
|
367
|
+
* Marker for the project's build-output directory. A spawn whose arguments
|
|
368
|
+
* reach this path is running compiled production code.
|
|
369
|
+
*/
|
|
370
|
+
const BUILD_OUTPUT_PATTERN = /\bdist\//;
|
|
371
|
+
/**
|
|
372
|
+
* Non-compiled production code this project ships and executes: the hook
|
|
373
|
+
* scripts and the `scripts/` + `templates/scripts/` trees (#906).
|
|
374
|
+
*
|
|
375
|
+
* `dist/` alone was too narrow. `checkout-lock.integration.test.ts` drives the
|
|
376
|
+
* real `.claude/hooks/pre-tool.sh` as a subprocess — that hook IS the
|
|
377
|
+
* enforcement half of the feature under test — yet every block in the file
|
|
378
|
+
* read as tautological because the path is not under `dist/`.
|
|
379
|
+
*/
|
|
380
|
+
const PROJECT_SCRIPT_PATTERN = /\b(?:hooks\/[\w.-]+\.sh|scripts\/[\w./-]+)/;
|
|
381
|
+
/**
|
|
382
|
+
* Collect names of variables bound to a path into the project's own executable
|
|
383
|
+
* code, e.g.
|
|
384
|
+
* const cliPath = resolve(projectRoot, "dist/bin/cli.js");
|
|
385
|
+
* const HOOK = join(REPO_ROOT, ".claude/hooks/pre-tool.sh");
|
|
386
|
+
* captures `cliPath` / `HOOK`. Tests almost always spawn via such a handle
|
|
387
|
+
* rather than an inline string, so these names stand in for the literal path.
|
|
388
|
+
*
|
|
389
|
+
* The right-hand side is statement-bounded (`[^;]`) so a match cannot bleed
|
|
390
|
+
* across declarations, and must reach one of the two markers. Spawning a
|
|
391
|
+
* *system* binary (`git`, `bash` with a temp fixture) matches neither, which
|
|
392
|
+
* is the intended exclusion — those are not this project's code.
|
|
393
|
+
*/
|
|
394
|
+
function collectBuildOutputVars(content) {
|
|
395
|
+
const names = new Set();
|
|
396
|
+
const patterns = [
|
|
397
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*[^;]*?\bdist\//g,
|
|
398
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*[^;]*?\b(?:hooks\/[\w.-]+\.sh|scripts\/[\w./-]+)/g,
|
|
399
|
+
];
|
|
400
|
+
for (const pattern of patterns) {
|
|
401
|
+
let match;
|
|
402
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
403
|
+
names.add(match[1]);
|
|
404
|
+
}
|
|
349
405
|
}
|
|
406
|
+
return [...names];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Whether a code body references a build-output token: either the literal
|
|
410
|
+
* `dist/` marker or one of the collected build-path variable names.
|
|
411
|
+
*/
|
|
412
|
+
function referencesBuildOutput(body, buildOutputVars) {
|
|
413
|
+
if (BUILD_OUTPUT_PATTERN.test(body) || PROJECT_SCRIPT_PATTERN.test(body)) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return buildOutputVars.some((name) => referenceMatcher(name).test(body));
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Whether a code body itself spawns the build output: it must contain BOTH a
|
|
420
|
+
* child-process spawn call AND a build-output token. Requiring co-occurrence
|
|
421
|
+
* keeps a helper that merely mentions `dist/` in a string (but never spawns)
|
|
422
|
+
* from counting as production (#885 AC-5).
|
|
423
|
+
*/
|
|
424
|
+
function spawnsBuildOutput(body, buildOutputVars) {
|
|
425
|
+
return (SPAWN_PATTERN.test(body) && referencesBuildOutput(body, buildOutputVars));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Extract module/describe-scope helper definitions as { name, body } pairs.
|
|
429
|
+
*
|
|
430
|
+
* Two shapes, because both are idiomatic and a detector that saw only one
|
|
431
|
+
* produced large-scale false positives (#906): a test calling a helper the
|
|
432
|
+
* detector cannot see reads as import-less, hence tautological. Measured on
|
|
433
|
+
* `checkout-lock.integration.test.ts` (helpers written as `function`
|
|
434
|
+
* declarations): 17 of 19 blocks flagged, every one of them real.
|
|
435
|
+
*
|
|
436
|
+
* Params are matched with `[^()]*` (no nested parens) to keep the scan from
|
|
437
|
+
* running away across the file. Expression-bodied arrows are skipped — they
|
|
438
|
+
* have no `{` body to extract.
|
|
439
|
+
*/
|
|
440
|
+
function extractHelperDefinitions(content) {
|
|
441
|
+
const helpers = [];
|
|
442
|
+
// Arrow consts anchor on `=> {`, so the body brace is unambiguous.
|
|
443
|
+
const arrowPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^()]*\)\s*(?::[^=]*?)?=>\s*\{/g;
|
|
444
|
+
let match;
|
|
445
|
+
while ((match = arrowPattern.exec(content)) !== null) {
|
|
446
|
+
if (isInsideString(content, match.index))
|
|
447
|
+
continue;
|
|
448
|
+
const braceIndex = match.index + match[0].length - 1;
|
|
449
|
+
helpers.push({
|
|
450
|
+
name: match[1],
|
|
451
|
+
body: extractBlockBody(content.substring(braceIndex)),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
// Declarations have no `=>` marker, and the return-type annotation may open
|
|
455
|
+
// a brace group of its own:
|
|
456
|
+
// function runHook(o): { status: number; stderr: string } { ... }
|
|
457
|
+
// so the body is NOT simply the first `{` after the parameters. Do not try
|
|
458
|
+
// to express that in the regex — a greedy annotation subpattern silently ran
|
|
459
|
+
// past the body and anchored on the NEXT declaration's brace, yielding a
|
|
460
|
+
// "helper" whose body was somebody else's code (caught by the object
|
|
461
|
+
// return-type test below). Match only to the closing paren, then walk: take
|
|
462
|
+
// the first brace group; if another `{` follows it, that group was the
|
|
463
|
+
// return type and the body is the next one.
|
|
464
|
+
const declPattern = /(?:async\s+)?function\s*\*?\s*(\w+)\s*\([^()]*\)/g;
|
|
465
|
+
while ((match = declPattern.exec(content)) !== null) {
|
|
466
|
+
if (isInsideString(content, match.index))
|
|
467
|
+
continue;
|
|
468
|
+
const rest = content.substring(match.index + match[0].length);
|
|
469
|
+
const firstBrace = rest.indexOf("{");
|
|
470
|
+
if (firstBrace === -1)
|
|
471
|
+
continue;
|
|
472
|
+
// Anything between `)` and the first `{` must be an annotation, not code.
|
|
473
|
+
if (/[;)=]/.test(rest.substring(0, firstBrace)))
|
|
474
|
+
continue;
|
|
475
|
+
let body = extractBlockBody(rest.substring(firstBrace));
|
|
476
|
+
const after = rest.substring(firstBrace + body.length);
|
|
477
|
+
if (/^\s*\{/.test(after)) {
|
|
478
|
+
body = extractBlockBody(after);
|
|
479
|
+
}
|
|
480
|
+
helpers.push({ name: match[1], body });
|
|
481
|
+
}
|
|
482
|
+
return helpers;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Collect the names of helper functions that reach production code, resolving
|
|
486
|
+
* indirection transitively: a helper that calls an already-known handle is
|
|
487
|
+
* itself a handle. This lets a test that only calls `expectFlagAccepted(...)`
|
|
488
|
+
* (which calls `runInUninitializedDir`, which spawns the CLI) count as
|
|
489
|
+
* exercising production code.
|
|
490
|
+
*
|
|
491
|
+
* A helper qualifies two ways:
|
|
492
|
+
* - it spawns the project's own executable code (subprocess integration
|
|
493
|
+
* tests), or
|
|
494
|
+
* - it references an imported production function (#906). `makeLock()`
|
|
495
|
+
* returning `new CheckoutLock({...})` is production code by any reading,
|
|
496
|
+
* but seeding on spawns alone missed it, so every test that built its
|
|
497
|
+
* subject through a factory read as tautological.
|
|
498
|
+
*/
|
|
499
|
+
function collectProductionHandles(content, buildOutputVars, importedFunctions = []) {
|
|
500
|
+
const helpers = extractHelperDefinitions(content);
|
|
501
|
+
const handles = new Set();
|
|
502
|
+
// Seed: helpers that directly reach production.
|
|
503
|
+
for (const helper of helpers) {
|
|
504
|
+
if (spawnsBuildOutput(helper.body, buildOutputVars) ||
|
|
505
|
+
importedFunctions.some((fn) => referenceMatcher(fn.name).test(helper.body))) {
|
|
506
|
+
handles.add(helper.name);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Transitive closure: a helper referencing a known spawn helper is one too.
|
|
510
|
+
let changed = true;
|
|
511
|
+
while (changed) {
|
|
512
|
+
changed = false;
|
|
513
|
+
for (const helper of helpers) {
|
|
514
|
+
if (handles.has(helper.name)) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
for (const known of handles) {
|
|
518
|
+
if (referenceMatcher(known).test(helper.body)) {
|
|
519
|
+
handles.add(helper.name);
|
|
520
|
+
changed = true;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return [...handles];
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if a test block calls production code. A block counts as non-tautological
|
|
530
|
+
* when it references an imported production function, directly spawns the
|
|
531
|
+
* project's build output, or calls a helper that (transitively) does so.
|
|
532
|
+
*
|
|
533
|
+
* @param productionHandles Names of describe/module-scope helpers that reach
|
|
534
|
+
* production — by spawning the project's executable code or by calling an
|
|
535
|
+
* imported production function (see {@link collectProductionHandles}).
|
|
536
|
+
* @param buildOutputVars Variable names bound to a build-output path (see
|
|
537
|
+
* {@link collectBuildOutputVars}).
|
|
538
|
+
*/
|
|
539
|
+
export function testBlockCallsProductionCode(body, importedFunctions, productionHandles = [], buildOutputVars = []) {
|
|
540
|
+
// 1. References an imported production function.
|
|
350
541
|
for (const fn of importedFunctions) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
542
|
+
if (referenceMatcher(fn.name).test(body)) {
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// 2. Directly spawns the project's build output (#885). Static import
|
|
547
|
+
// analysis can't see through a subprocess boundary, so a test that runs
|
|
548
|
+
// `dist/bin/cli.js` looks import-less but exercises production code.
|
|
549
|
+
if (spawnsBuildOutput(body, buildOutputVars)) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
// 3. Calls a describe/module-scope helper that (transitively) reaches
|
|
553
|
+
// production — spawns the project's executable code, or calls an imported
|
|
554
|
+
// production function (#906). Covers arrow-const and `function` helpers.
|
|
555
|
+
for (const handle of productionHandles) {
|
|
556
|
+
if (referenceMatcher(handle).test(body)) {
|
|
358
557
|
return true;
|
|
359
558
|
}
|
|
360
559
|
}
|
|
@@ -389,12 +588,14 @@ export function analyzeTestFile(content, filePath) {
|
|
|
389
588
|
}
|
|
390
589
|
try {
|
|
391
590
|
const importedFunctions = extractImports(content);
|
|
591
|
+
const buildOutputVars = collectBuildOutputVars(content);
|
|
592
|
+
const productionHandles = collectProductionHandles(content, buildOutputVars, importedFunctions);
|
|
392
593
|
const testBlocks = extractTestBlocks(content);
|
|
393
594
|
const analyzedBlocks = testBlocks.map((block) => ({
|
|
394
595
|
description: block.description,
|
|
395
596
|
lineNumber: block.lineNumber,
|
|
396
597
|
style: block.style,
|
|
397
|
-
isTautological: !testBlockCallsProductionCode(block.body, importedFunctions),
|
|
598
|
+
isTautological: !testBlockCallsProductionCode(block.body, importedFunctions, productionHandles, buildOutputVars),
|
|
398
599
|
}));
|
|
399
600
|
const tautologicalCount = analyzedBlocks.filter((b) => b.isTautological).length;
|
|
400
601
|
const totalTests = analyzedBlocks.length;
|
|
@@ -53,6 +53,37 @@ export declare function isGlobalInstall(installPath?: string): boolean;
|
|
|
53
53
|
* neither receives the project-local "use npm update sequant" warning.
|
|
54
54
|
*/
|
|
55
55
|
export declare function isLocalNodeModulesInstall(installPath?: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Check if running from the npx cache (~/.npm/_npx/<hash>/node_modules/sequant).
|
|
58
|
+
*
|
|
59
|
+
* Separated from `isLocalNodeModulesInstall`, which deliberately excludes the
|
|
60
|
+
* cache: that predicate answers "is this a project dependency the user can
|
|
61
|
+
* `npm update`", while this one answers "did the user reach us through npx".
|
|
62
|
+
* Backslashes are normalized first, so the single forward-slash form also
|
|
63
|
+
* matches a Windows `…\.npm\_npx\…` path.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isNpxCacheInstall(installPath?: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the invocation to name in user-facing "run this next" messages.
|
|
68
|
+
*
|
|
69
|
+
* A hardcoded `npx sequant` is right for the documented install path (README:
|
|
70
|
+
* `npm install sequant` then `npx sequant init`) — npx resolves the project's
|
|
71
|
+
* `node_modules/.bin/sequant`, and a bare `sequant` would not be on PATH. It is
|
|
72
|
+
* wrong for a global or `npm link`ed install with no local dependency: there,
|
|
73
|
+
* `npx sequant` cannot resolve locally and falls through to the npx cache,
|
|
74
|
+
* which can hold an older *published* copy than the one currently running. The
|
|
75
|
+
* observed symptom is a remediation that fights itself — the recommended
|
|
76
|
+
* command reinstalls the very content whose staleness triggered the message.
|
|
77
|
+
*
|
|
78
|
+
* So recommend the form that actually reaches *this* build:
|
|
79
|
+
* - project-local dependency or npx cache → `npx sequant`
|
|
80
|
+
* - global, `npm link`ed, or a dev checkout → `sequant`
|
|
81
|
+
*
|
|
82
|
+
* A linked checkout resolves through its realpath (Node resolves symlinks
|
|
83
|
+
* unless `--preserve-symlinks` is set), so it looks like neither a global nor a
|
|
84
|
+
* local install and correctly lands on the bare form.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveCliInvocation(installPath?: string): "sequant" | "npx sequant";
|
|
56
87
|
/**
|
|
57
88
|
* Walk up from the given directory to find the directory containing
|
|
58
89
|
* sequant's package.json. Returns null if not found.
|
|
@@ -8,7 +8,7 @@ import fs from "fs";
|
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
-
import { detectPackageManagerSync,
|
|
11
|
+
import { detectPackageManagerSync, resolvePackageManagerConfig, } from "./stacks.js";
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
const PACKAGE_NAME = "sequant";
|
|
@@ -92,6 +92,44 @@ export function isLocalNodeModulesInstall(installPath = __dirname) {
|
|
|
92
92
|
normalizedPath.includes("\\.npm\\_npx\\");
|
|
93
93
|
return inNodeModules && !inNpxCache && !isGlobalInstall(normalizedPath);
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Check if running from the npx cache (~/.npm/_npx/<hash>/node_modules/sequant).
|
|
97
|
+
*
|
|
98
|
+
* Separated from `isLocalNodeModulesInstall`, which deliberately excludes the
|
|
99
|
+
* cache: that predicate answers "is this a project dependency the user can
|
|
100
|
+
* `npm update`", while this one answers "did the user reach us through npx".
|
|
101
|
+
* Backslashes are normalized first, so the single forward-slash form also
|
|
102
|
+
* matches a Windows `…\.npm\_npx\…` path.
|
|
103
|
+
*/
|
|
104
|
+
export function isNpxCacheInstall(installPath = __dirname) {
|
|
105
|
+
return installPath.replace(/\\/g, "/").includes("/.npm/_npx/");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the invocation to name in user-facing "run this next" messages.
|
|
109
|
+
*
|
|
110
|
+
* A hardcoded `npx sequant` is right for the documented install path (README:
|
|
111
|
+
* `npm install sequant` then `npx sequant init`) — npx resolves the project's
|
|
112
|
+
* `node_modules/.bin/sequant`, and a bare `sequant` would not be on PATH. It is
|
|
113
|
+
* wrong for a global or `npm link`ed install with no local dependency: there,
|
|
114
|
+
* `npx sequant` cannot resolve locally and falls through to the npx cache,
|
|
115
|
+
* which can hold an older *published* copy than the one currently running. The
|
|
116
|
+
* observed symptom is a remediation that fights itself — the recommended
|
|
117
|
+
* command reinstalls the very content whose staleness triggered the message.
|
|
118
|
+
*
|
|
119
|
+
* So recommend the form that actually reaches *this* build:
|
|
120
|
+
* - project-local dependency or npx cache → `npx sequant`
|
|
121
|
+
* - global, `npm link`ed, or a dev checkout → `sequant`
|
|
122
|
+
*
|
|
123
|
+
* A linked checkout resolves through its realpath (Node resolves symlinks
|
|
124
|
+
* unless `--preserve-symlinks` is set), so it looks like neither a global nor a
|
|
125
|
+
* local install and correctly lands on the bare form.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveCliInvocation(installPath = __dirname) {
|
|
128
|
+
return isLocalNodeModulesInstall(installPath) ||
|
|
129
|
+
isNpxCacheInstall(installPath)
|
|
130
|
+
? "npx sequant"
|
|
131
|
+
: "sequant";
|
|
132
|
+
}
|
|
95
133
|
/**
|
|
96
134
|
* Walk up from the given directory to find the directory containing
|
|
97
135
|
* sequant's package.json. Returns null if not found.
|
|
@@ -314,7 +352,12 @@ export function getVersionWarning(currentVersion, latestVersion, isLocal) {
|
|
|
314
352
|
const isLocalInstall = isLocal ?? isLocalNodeModulesInstall();
|
|
315
353
|
if (isLocalInstall) {
|
|
316
354
|
const pm = detectPackageManagerSync();
|
|
317
|
-
|
|
355
|
+
// Resolved, not read off PM_CONFIG: `updatePkg` is one of the fields whose
|
|
356
|
+
// yarn spelling depends on the major (berry `yarn up` vs classic
|
|
357
|
+
// `yarn upgrade`), and this string is a command we are telling the user to
|
|
358
|
+
// run. Both `detectPackageManagerSync` and the resolver read the cwd, which
|
|
359
|
+
// is the project this local install belongs to (#871).
|
|
360
|
+
const pmConfig = resolvePackageManagerConfig(pm, process.cwd());
|
|
318
361
|
return `sequant ${latestVersion} is available (you have ${currentVersion})
|
|
319
362
|
Run: ${pmConfig.updatePkg} sequant
|
|
320
363
|
Note: You have sequant as a local dependency. npx uses your node_modules version.`;
|