@intentius/chant 0.124.30 → 0.124.32
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/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +74 -12
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +65 -0
- package/dist/cli/registry.d.ts +29 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/local-executor.d.ts +12 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -3
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/redact.d.ts +34 -0
- package/dist/op/redact.d.ts.map +1 -0
- package/dist/op/redact.js +94 -0
- package/dist/op/step-console.d.ts +2 -0
- package/dist/op/step-console.d.ts.map +1 -1
- package/dist/op/step-console.js +5 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +27 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +51 -6
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +10 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +52 -6
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +108 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +164 -0
- package/dist/workspace/ci-run-index.schema.json +52 -0
- package/dist/workspace/ci-run.d.ts +159 -9
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +419 -50
- package/dist/workspace/ci-runner-key.d.ts +133 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +237 -0
- package/dist/workspace/ci-secrets.d.ts +65 -0
- package/dist/workspace/ci-secrets.d.ts.map +1 -0
- package/dist/workspace/ci-secrets.js +211 -0
- package/dist/workspace/ci-serve.d.ts +67 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +240 -35
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +7 -3
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +9 -4
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/trust/trust-write.d.ts +109 -0
- package/dist/workspace/trust/trust-write.d.ts.map +1 -0
- package/dist/workspace/trust/trust-write.js +320 -0
- package/dist/workspace/trust-write.schema.json +265 -0
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +19 -0
- package/src/cli/main.ts +73 -12
- package/src/cli/parse-args.ts +48 -0
- package/src/cli/registry.ts +35 -4
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/local-executor.ts +31 -3
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/redact.ts +93 -0
- package/src/op/step-console.ts +7 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +71 -0
- package/src/pr-forge.ts +70 -6
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +58 -6
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +52 -0
- package/src/workspace/ci-run-index.test.ts +392 -0
- package/src/workspace/ci-run-index.ts +206 -0
- package/src/workspace/ci-run.test.ts +520 -42
- package/src/workspace/ci-run.ts +483 -51
- package/src/workspace/ci-runner-key.test.ts +204 -0
- package/src/workspace/ci-runner-key.ts +275 -0
- package/src/workspace/ci-secrets.test.ts +106 -0
- package/src/workspace/ci-secrets.ts +213 -0
- package/src/workspace/ci-serve.test.ts +282 -39
- package/src/workspace/ci-serve.ts +231 -34
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +9 -4
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/trust/trust-write.test.ts +179 -0
- package/src/workspace/trust/trust-write.ts +377 -0
- package/src/workspace/trust-write.schema.json +265 -0
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an Op that changes the checkout makes its work worktrees (#2893).
|
|
3
|
+
*
|
|
4
|
+
* A work item's worktree is `<checkout>/.chant/work/[<member>/]<item>`. It
|
|
5
|
+
* sits inside the checkout, so a coding agent whose project root is the
|
|
6
|
+
* checkout may write there, and `node_modules` above it still resolves. It
|
|
7
|
+
* used to be `<git-common-dir>/chant-work/...`, where Claude Code refuses
|
|
8
|
+
* every write. `/.chant/work/` goes in `<git-common-dir>/info/exclude`, so the
|
|
9
|
+
* checkout's `git status` never lists it, and the rest of `.chant/` (the
|
|
10
|
+
* workspace lock, the factory's check logs) stays tracked.
|
|
11
|
+
*
|
|
12
|
+
* This module imports nothing from the workspace or the Op runner, so the
|
|
13
|
+
* hand-written walkers can ask it which directory to skip.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync } from "node:fs";
|
|
17
|
+
import { dirname, join, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** The work directory, relative to a checkout's top, `/`-separated. */
|
|
20
|
+
export const WORK_DIR = ".chant/work";
|
|
21
|
+
|
|
22
|
+
/** The line `<git-common-dir>/info/exclude` gets, anchored at each checkout's top. */
|
|
23
|
+
export const WORK_DIR_EXCLUDE = `/${WORK_DIR}/`;
|
|
24
|
+
|
|
25
|
+
/** The directory where earlier versions made work worktrees, under the git common directory. */
|
|
26
|
+
export const LEGACY_WORK_DIR = "chant-work";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a directory, given as a `/`-separated path relative to the root a
|
|
30
|
+
* walker started from, is a work directory: `.chant/work` at that root or at
|
|
31
|
+
* any depth below it. A walker skips it, since each worktree in it is a whole
|
|
32
|
+
* copy of the checkout.
|
|
33
|
+
*/
|
|
34
|
+
export function isWorkDir(rel: string): boolean {
|
|
35
|
+
return rel === WORK_DIR || rel.endsWith(`/${WORK_DIR}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The worktree for `id` under the checkout whose top is `top`. */
|
|
39
|
+
export function workWorktreePath(top: string, members: readonly string[], id: string): string {
|
|
40
|
+
return join(top, ...WORK_DIR.split("/"), ...members, id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The worktree an earlier version made for `id`, under the git common directory. */
|
|
44
|
+
export function legacyWorkWorktreePath(commonDir: string, members: readonly string[], id: string): string {
|
|
45
|
+
return join(commonDir, LEGACY_WORK_DIR, ...members, id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Add {@link WORK_DIR_EXCLUDE} to `<commonDir>/info/exclude` unless the file
|
|
50
|
+
* already has it, or `ignored` says git already ignores the work directory
|
|
51
|
+
* (a `.gitignore` of the project's, or the global excludes file). Returns
|
|
52
|
+
* whether it wrote the line.
|
|
53
|
+
*/
|
|
54
|
+
export function excludeWorkDir(commonDir: string, ignored: boolean): boolean {
|
|
55
|
+
const file = join(commonDir, "info", "exclude");
|
|
56
|
+
const text = existsSync(file) ? readFileSync(file, "utf-8") : "";
|
|
57
|
+
if (text.split(/\r?\n/).some((l) => l.trim() === WORK_DIR_EXCLUDE)) return false;
|
|
58
|
+
if (ignored) return false;
|
|
59
|
+
mkdirSync(join(commonDir, "info"), { recursive: true });
|
|
60
|
+
appendFileSync(file, `${text === "" || text.endsWith("\n") ? "" : "\n"}${WORK_DIR_EXCLUDE}\n`);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* After a work worktree is removed, remove the directories above it that are
|
|
66
|
+
* left empty, up to and including the checkout's `.chant`, so a run leaves
|
|
67
|
+
* nothing behind in a checkout that had no `.chant` before it.
|
|
68
|
+
*/
|
|
69
|
+
export function removeEmptyWorkDirs(worktree: string): void {
|
|
70
|
+
const marker = `${sep}${WORK_DIR.split("/").join(sep)}${sep}`;
|
|
71
|
+
const at = worktree.lastIndexOf(marker);
|
|
72
|
+
if (at < 0) return;
|
|
73
|
+
const stop = worktree.slice(0, at);
|
|
74
|
+
for (let dir = dirname(worktree); dir.length > stop.length; dir = dirname(dir)) {
|
|
75
|
+
try {
|
|
76
|
+
if (readdirSync(dir).length > 0) return;
|
|
77
|
+
rmdirSync(dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import { discoverLintRules, discoverPostSynthChecks, listRuleFiles } from "./discover";
|
|
5
7
|
|
|
6
8
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
|
|
@@ -67,3 +69,45 @@ describe("discoverPostSynthChecks", () => {
|
|
|
67
69
|
expect(checks.length).toBe(0);
|
|
68
70
|
});
|
|
69
71
|
});
|
|
72
|
+
|
|
73
|
+
describe("rule modules compiled to JS (#3981)", () => {
|
|
74
|
+
let dir: string;
|
|
75
|
+
|
|
76
|
+
beforeAll(() => {
|
|
77
|
+
// A compiled rules directory: ES modules in a "type": "module" package, each with its declarations beside it.
|
|
78
|
+
dir = mkdtempSync(join(tmpdir(), "chant-discover-js-"));
|
|
79
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ type: "module" }));
|
|
80
|
+
writeFileSync(join(dir, "alpha.js"), `export const alpha = { id: "JS002", severity: "warning", category: "style", check: () => [] };\n`);
|
|
81
|
+
writeFileSync(join(dir, "alpha.d.ts"), `export declare const alpha: unknown;\n`);
|
|
82
|
+
writeFileSync(join(dir, "beta.js"), `export const beta = { id: "JS001", severity: "error", category: "security", check: () => [] };\n`);
|
|
83
|
+
writeFileSync(join(dir, "beta.d.ts"), `export declare const beta: unknown;\n`);
|
|
84
|
+
writeFileSync(join(dir, "gamma.js"), `export const gamma = { id: "JSP001", description: "a post-synth check", check: () => [] };\n`);
|
|
85
|
+
writeFileSync(join(dir, "index.js"), `export * from "./alpha.js";\n`);
|
|
86
|
+
writeFileSync(join(dir, "rule-helpers.js"), `export const helper = 1;\n`);
|
|
87
|
+
writeFileSync(join(dir, "alpha.test.js"), `throw new Error("a test is not a rule");\n`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
91
|
+
|
|
92
|
+
test("listRuleFiles lists .js modules and leaves out declarations, tests, helpers and the index", () => {
|
|
93
|
+
expect(listRuleFiles(dir).sort()).toEqual(["alpha.js", "beta.js", "gamma.js"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a source module and its compiled twin list once, as the source", () => {
|
|
97
|
+
const both = mkdtempSync(join(tmpdir(), "chant-discover-both-"));
|
|
98
|
+
try {
|
|
99
|
+
for (const f of ["one.ts", "one.js", "one.d.ts", "two.js"]) writeFileSync(join(both, f), "");
|
|
100
|
+
expect(listRuleFiles(both).sort()).toEqual(["one.ts", "two.js"]);
|
|
101
|
+
} finally {
|
|
102
|
+
rmSync(both, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("discoverLintRules loads compiled rules", () => {
|
|
107
|
+
expect(discoverLintRules(dir, import.meta.url).map((r) => r.id)).toEqual(["JS001", "JS002"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("discoverPostSynthChecks loads compiled checks", () => {
|
|
111
|
+
expect(discoverPostSynthChecks(dir, import.meta.url).map((c) => c.id)).toEqual(["JSP001"]);
|
|
112
|
+
});
|
|
113
|
+
});
|
package/src/lint/discover.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-discovery for lint rules and post-synth checks.
|
|
3
3
|
*
|
|
4
|
-
* Scans directories for
|
|
5
|
-
* eliminating the need to manually list every rule in plugin.ts.
|
|
4
|
+
* Scans directories for rule modules and loads exported rules/checks,
|
|
5
|
+
* eliminating the need to manually list every rule in plugin.ts. A lexicon
|
|
6
|
+
* run from source has `.ts` rule modules; one run compiled has `.js` modules
|
|
7
|
+
* beside `.d.ts` declarations (#3981). Both are found, and tsx is registered
|
|
8
|
+
* only when a module found is TypeScript.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { readdirSync } from "fs";
|
|
@@ -11,6 +14,7 @@ import { createRequire } from "module";
|
|
|
11
14
|
import type { LintRule } from "./rule";
|
|
12
15
|
import type { PostSynthCheck } from "./post-synth";
|
|
13
16
|
import { ensureTypeScriptLoader } from "../ts-loader";
|
|
17
|
+
import { isTypeScriptModule } from "../module-source";
|
|
14
18
|
|
|
15
19
|
let cjsTypeScript = false;
|
|
16
20
|
|
|
@@ -22,7 +26,8 @@ let cjsTypeScript = false;
|
|
|
22
26
|
* From source this runs when the module loads, as it always has: vitest and
|
|
23
27
|
* the source CLI rely on tsx's CommonJS hook being in place from then on. The
|
|
24
28
|
* compiled CLI imports this module on every start and most commands read no
|
|
25
|
-
* rule directory, so there it waits for the first
|
|
29
|
+
* rule directory, so there it waits for the first directory that holds a
|
|
30
|
+
* TypeScript rule module (#3878, #3981).
|
|
26
31
|
*/
|
|
27
32
|
function registerTypeScriptRequire(): void {
|
|
28
33
|
if (cjsTypeScript) return;
|
|
@@ -40,82 +45,83 @@ function registerTypeScriptRequire(): void {
|
|
|
40
45
|
if (!import.meta.url.endsWith(".js")) registerTypeScriptRequire();
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* Scans the directory for .ts files (excluding tests, helpers, and index files),
|
|
46
|
-
* loads each module, and collects any exported objects that look like a LintRule
|
|
47
|
-
* (has `id`, `severity`, `category`, and `check` function).
|
|
48
|
-
*
|
|
49
|
-
* @param dir - Absolute path to the rules directory
|
|
50
|
-
* @param importMetaUrl - The caller's import.meta.url (for createRequire)
|
|
48
|
+
* Load every rule module in `dir` and collect the exports `accept` takes.
|
|
49
|
+
* Registers tsx first only when one of the modules is TypeScript.
|
|
51
50
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
function discoverExports<T>(dir: string, importMetaUrl: string, accept: (value: unknown) => value is T): T[] {
|
|
52
|
+
const files = listRuleFiles(dir);
|
|
53
|
+
if (files.some(isTypeScriptModule)) registerTypeScriptRequire();
|
|
54
54
|
const require = createRequire(importMetaUrl);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
for (const file of listRuleFiles(dir)) {
|
|
55
|
+
const found: T[] = [];
|
|
56
|
+
for (const file of files) {
|
|
58
57
|
try {
|
|
59
|
-
|
|
60
|
-
const modPath = join(dir, file.replace(/\.ts$/, ""));
|
|
61
|
-
const mod = require(modPath);
|
|
58
|
+
const mod = require(join(dir, file)) as Record<string, unknown>;
|
|
62
59
|
for (const exp of Object.values(mod)) {
|
|
63
|
-
if (
|
|
60
|
+
if (accept(exp)) found.push(exp);
|
|
64
61
|
}
|
|
65
62
|
} catch {
|
|
66
63
|
// Skip files that fail to load
|
|
67
64
|
}
|
|
68
65
|
}
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Discover lint rules from a directory.
|
|
71
|
+
*
|
|
72
|
+
* Scans the directory for rule modules (see {@link listRuleFiles}), loads
|
|
73
|
+
* each one, and collects any exported objects that look like a LintRule (has
|
|
74
|
+
* `id`, `severity`, `category`, and `check` function).
|
|
75
|
+
*
|
|
76
|
+
* @param dir - Absolute path to the rules directory
|
|
77
|
+
* @param importMetaUrl - The caller's import.meta.url (for createRequire)
|
|
78
|
+
*/
|
|
79
|
+
export function discoverLintRules(dir: string, importMetaUrl: string): LintRule[] {
|
|
80
|
+
return discoverExports(dir, importMetaUrl, isLintRule).sort((a, b) => a.id.localeCompare(b.id));
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
/**
|
|
74
84
|
* Discover post-synth checks from a directory.
|
|
75
85
|
*
|
|
76
|
-
* Scans the directory for
|
|
77
|
-
*
|
|
78
|
-
* (has `id`, `description`, and `check` function).
|
|
86
|
+
* Scans the directory for rule modules (see {@link listRuleFiles}), loads
|
|
87
|
+
* each one, and collects any exported objects that look like a
|
|
88
|
+
* PostSynthCheck (has `id`, `description`, and `check` function).
|
|
79
89
|
*
|
|
80
90
|
* @param dir - Absolute path to the post-synth rules directory
|
|
81
91
|
* @param importMetaUrl - The caller's import.meta.url (for createRequire)
|
|
82
92
|
*/
|
|
83
93
|
export function discoverPostSynthChecks(dir: string, importMetaUrl: string): PostSynthCheck[] {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const checks: PostSynthCheck[] = [];
|
|
87
|
-
|
|
88
|
-
for (const file of listRuleFiles(dir)) {
|
|
89
|
-
try {
|
|
90
|
-
// Strip .ts extension — require() resolves without it
|
|
91
|
-
const modPath = join(dir, file.replace(/\.ts$/, ""));
|
|
92
|
-
const mod = require(modPath);
|
|
93
|
-
for (const exp of Object.values(mod)) {
|
|
94
|
-
if (isPostSynthCheck(exp)) checks.push(exp);
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Skip files that fail to load
|
|
98
|
-
}
|
|
99
|
-
}
|
|
94
|
+
return discoverExports(dir, importMetaUrl, isPostSynthCheck).sort((a, b) => a.id.localeCompare(b.id));
|
|
95
|
+
}
|
|
100
96
|
|
|
101
|
-
|
|
97
|
+
/** A rule module's name without its extension: `foo` for `foo.ts` and `foo.js`. */
|
|
98
|
+
export function ruleModuleBase(file: string): string {
|
|
99
|
+
return file.replace(/\.(?:ts|js)$/, "");
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
/**
|
|
105
|
-
* List rule
|
|
103
|
+
* List rule modules in a directory: `.ts` source or compiled `.js`, leaving
|
|
104
|
+
* out declarations (`.d.ts`), tests, helpers and the index. A directory
|
|
105
|
+
* holding both `foo.ts` and `foo.js` lists `foo.ts` alone, so a rule is
|
|
106
|
+
* never loaded twice.
|
|
106
107
|
*/
|
|
107
108
|
export function listRuleFiles(dir: string): string[] {
|
|
109
|
+
let entries: string[];
|
|
108
110
|
try {
|
|
109
|
-
|
|
110
|
-
if (!f.endsWith(".ts")) return false;
|
|
111
|
-
if (f.endsWith(".test.ts")) return false;
|
|
112
|
-
if (f === "index.ts") return false;
|
|
113
|
-
if (f.includes("helper")) return false;
|
|
114
|
-
return true;
|
|
115
|
-
});
|
|
111
|
+
entries = readdirSync(dir);
|
|
116
112
|
} catch {
|
|
117
113
|
return [];
|
|
118
114
|
}
|
|
115
|
+
const modules = entries.filter((f) => {
|
|
116
|
+
if (!f.endsWith(".ts") && !f.endsWith(".js")) return false;
|
|
117
|
+
if (f.endsWith(".d.ts")) return false;
|
|
118
|
+
if (/\.test\.[tj]s$/.test(f)) return false;
|
|
119
|
+
if (f === "index.ts" || f === "index.js") return false;
|
|
120
|
+
if (f.includes("helper")) return false;
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
const sources = new Set(modules.filter((f) => f.endsWith(".ts")).map(ruleModuleBase));
|
|
124
|
+
return modules.filter((f) => f.endsWith(".ts") || !sources.has(ruleModuleBase(f)));
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
function isLintRule(obj: unknown): obj is LintRule {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { isJavaScriptModule, isTypeScriptModule, moduleSourceDir, moduleSourceUrl, SOURCE_EXPORT_CONDITIONS } from "./module-source";
|
|
6
|
+
|
|
7
|
+
describe("module-source (#3981)", () => {
|
|
8
|
+
test("from source, chant's own resolution adds the development condition", () => {
|
|
9
|
+
expect(SOURCE_EXPORT_CONDITIONS).toEqual(["development"]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const pkg = join(sep, "x", "node_modules", "@intentius", "chant-lexicon-aws");
|
|
13
|
+
|
|
14
|
+
test("moduleSourceDir maps a compiled module's directory to the source directory", () => {
|
|
15
|
+
expect(moduleSourceDir(pathToFileURL(join(pkg, "dist", "lint", "post-synth", "ext001.js")).href)).toBe(join(pkg, "src", "lint", "post-synth"));
|
|
16
|
+
expect(moduleSourceDir(pathToFileURL(join(pkg, "dist", "taggable.js")).href)).toBe(join(pkg, "src"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("moduleSourceDir leaves a source module's directory alone, even under a dist/ directory", () => {
|
|
20
|
+
expect(moduleSourceDir(pathToFileURL(join(pkg, "src", "taggable.ts")).href)).toBe(join(pkg, "src"));
|
|
21
|
+
const checkout = join(sep, "home", "dist", "chant", "lexicons", "aws", "src", "taggable.ts");
|
|
22
|
+
expect(moduleSourceDir(pathToFileURL(checkout).href)).toBe(join(sep, "home", "dist", "chant", "lexicons", "aws", "src"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("moduleSourceUrl is a URL in the source directory, for createRequire", () => {
|
|
26
|
+
const url = moduleSourceUrl(pathToFileURL(join(pkg, "dist", "lsp", "hover.js")).href);
|
|
27
|
+
expect(url).toBe(pathToFileURL(join(pkg, "src", "lsp", "hover.js")).href);
|
|
28
|
+
expect(new URL("../generated/lexicon-aws.json", url).href).toBe(pathToFileURL(join(pkg, "src", "generated", "lexicon-aws.json")).href);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("isTypeScriptModule and isJavaScriptModule", () => {
|
|
32
|
+
expect(isTypeScriptModule("/p/src/index.ts")).toBe(true);
|
|
33
|
+
expect(isTypeScriptModule("file:///p/src/index.mts?x=1")).toBe(true);
|
|
34
|
+
expect(isTypeScriptModule("/p/dist/index.d.ts")).toBe(false);
|
|
35
|
+
expect(isTypeScriptModule("/p/dist/index.js")).toBe(false);
|
|
36
|
+
expect(isJavaScriptModule("file:///p/dist/index.js")).toBe(true);
|
|
37
|
+
expect(isJavaScriptModule("/p/dist/index.mjs")).toBe(true);
|
|
38
|
+
expect(isJavaScriptModule("/p/src/index.ts")).toBe(false);
|
|
39
|
+
// No extension: only tsx's resolver completes it, so it is not plain JavaScript.
|
|
40
|
+
expect(isJavaScriptModule("/p/lexicon/index")).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("package sources run under plain node ESM (#3981)", () => {
|
|
45
|
+
const repo = resolve(import.meta.dirname, "..", "..", "..");
|
|
46
|
+
const SKIP_DIRS = new Set(["node_modules", "generated", "testdata", "fixtures", "__fixtures__", "test-fixtures"]);
|
|
47
|
+
|
|
48
|
+
function sources(dir: string, out: string[] = []): string[] {
|
|
49
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
if (!SKIP_DIRS.has(entry.name)) sources(join(dir, entry.name), out);
|
|
52
|
+
} else if (/\.ts$/.test(entry.name) && !/\.(test|d)\.ts$/.test(entry.name)) {
|
|
53
|
+
out.push(join(dir, entry.name));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("no lexicon or package source uses the CommonJS __dirname or __filename", () => {
|
|
60
|
+
// Both are undefined in an ES module under plain node, and under tsx too: a
|
|
61
|
+
// read built on one throws, and the catch around it hides the data.
|
|
62
|
+
// moduleSourceDir(import.meta.url) names the source directory from src/ and dist/ alike.
|
|
63
|
+
const roots = ["lexicons", "packages"].flatMap((top) =>
|
|
64
|
+
readdirSync(join(repo, top), { withFileTypes: true })
|
|
65
|
+
.filter((e) => e.isDirectory())
|
|
66
|
+
.map((e) => join(repo, top, e.name, "src")),
|
|
67
|
+
);
|
|
68
|
+
const offenders: string[] = [];
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
let files: string[];
|
|
71
|
+
try {
|
|
72
|
+
files = sources(root);
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const code = readFileSync(file, "utf-8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
78
|
+
code.split("\n").forEach((line, i) => {
|
|
79
|
+
if (/\b__(?:dirname|filename)\b/.test(line)) offenders.push(`${relative(repo, file)}:${i + 1}: ${line.trim()}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
expect(offenders).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3981 — paths for a module that may run from `src/*.ts` or from compiled
|
|
3
|
+
* `dist/*.js`.
|
|
4
|
+
*
|
|
5
|
+
* A package compiles `src/` to `dist/` with the same layout (`rootDir: src`,
|
|
6
|
+
* `outDir: dist`), so a compiled module sits at the same relative path under
|
|
7
|
+
* `dist/` as its source does under `src/`. tsc compiles only the modules: a
|
|
8
|
+
* file a module reads beside itself (generated JSON, skills markdown,
|
|
9
|
+
* snapshots) stays under `src/`, which every package publishes. A module that
|
|
10
|
+
* reads such a file names it from {@link moduleSourceDir}, not from its own
|
|
11
|
+
* directory, and finds it from either layout.
|
|
12
|
+
*
|
|
13
|
+
* String arithmetic only, no filesystem access, so it is safe in a sandboxed
|
|
14
|
+
* child and costs nothing at module scope.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { dirname, sep } from "node:path";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
const DIST = `${sep}dist${sep}`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The path a module's source has: `path` itself for a source module, and the
|
|
24
|
+
* matching path under `<package>/src` for a module under `<package>/dist`.
|
|
25
|
+
*/
|
|
26
|
+
function sourcePathOf(path: string): string {
|
|
27
|
+
if (!/\.[cm]?js$/.test(path)) return path;
|
|
28
|
+
const at = path.lastIndexOf(DIST);
|
|
29
|
+
if (at < 0) return path;
|
|
30
|
+
return path.slice(0, at) + `${sep}src${sep}` + path.slice(at + DIST.length);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* #3981 — the source file a compiled module was built from:
|
|
35
|
+
* `<package>/dist/x/y.js` is `<package>/src/x/y.ts` (`.mjs` from `.mts`, `.cjs`
|
|
36
|
+
* from `.cts`). `undefined` for a path that is not a compiled module under a
|
|
37
|
+
* `dist/`. The file is not checked for.
|
|
38
|
+
*/
|
|
39
|
+
export function compiledModuleSource(path: string): string | undefined {
|
|
40
|
+
const ext = /\.([cm]?)js$/.exec(path);
|
|
41
|
+
if (!ext || path.lastIndexOf(DIST) < 0) return undefined;
|
|
42
|
+
return sourcePathOf(path).slice(0, -ext[0].length) + `.${ext[1]}ts`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The directory a module's source sits in: `dirname` of `moduleUrl` when it
|
|
47
|
+
* runs from source, and the matching directory under `<package>/src` when it
|
|
48
|
+
* runs compiled from `<package>/dist`.
|
|
49
|
+
*/
|
|
50
|
+
export function moduleSourceDir(moduleUrl: string): string {
|
|
51
|
+
return dirname(sourcePathOf(fileURLToPath(moduleUrl)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A URL in the module's source directory, for `createRequire` to read a file
|
|
56
|
+
* that sits beside the source (`require("./generated/lexicon-aws.json")`). The
|
|
57
|
+
* file name keeps the compiled extension; only the directory matters to
|
|
58
|
+
* `createRequire`.
|
|
59
|
+
*/
|
|
60
|
+
export function moduleSourceUrl(moduleUrl: string): string {
|
|
61
|
+
return pathToFileURL(sourcePathOf(fileURLToPath(moduleUrl))).href;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether `pathOrUrl` names a TypeScript module, which plain node cannot
|
|
66
|
+
* import without a loader: `.ts`, `.mts`, `.cts` or `.tsx`, and not a
|
|
67
|
+
* declaration file. A query or hash is ignored.
|
|
68
|
+
*/
|
|
69
|
+
export function isTypeScriptModule(pathOrUrl: string): boolean {
|
|
70
|
+
const path = pathOrUrl.replace(/[?#].*$/, "");
|
|
71
|
+
return /\.(?:[cm]?ts|tsx)$/.test(path) && !/\.d\.[cm]?ts$/.test(path);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether plain node imports `pathOrUrl` with no loader: `.js`, `.mjs`,
|
|
76
|
+
* `.cjs` or `.json`. Anything else, a module with no extension included, is
|
|
77
|
+
* left to tsx, whose resolver completes an extensionless path.
|
|
78
|
+
*/
|
|
79
|
+
export function isJavaScriptModule(pathOrUrl: string): boolean {
|
|
80
|
+
return /\.(?:[cm]?js|json)$/.test(pathOrUrl.replace(/[?#].*$/, ""));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* #3981 — whether this process resolves chant's packages through their
|
|
85
|
+
* `development` export condition: always when chant runs from source (the
|
|
86
|
+
* CLI's `main()` installs a resolve hook that does so, and vitest's resolver
|
|
87
|
+
* adds the condition outside production mode), and otherwise when node runs
|
|
88
|
+
* with `--conditions=development` (or `-C development`, on the command line
|
|
89
|
+
* or in `NODE_OPTIONS`). Not from the environment: a compiled CLI that a test
|
|
90
|
+
* spawns inherits vitest's variables.
|
|
91
|
+
*/
|
|
92
|
+
function resolvesDevelopment(): boolean {
|
|
93
|
+
if (isTypeScriptModule(import.meta.url)) return true;
|
|
94
|
+
if (typeof process === "undefined") return false;
|
|
95
|
+
const flags = [...process.execArgv, ...(process.env.NODE_OPTIONS ?? "").split(/\s+/)];
|
|
96
|
+
return flags.some(
|
|
97
|
+
(flag, i) =>
|
|
98
|
+
flag === "--conditions=development" ||
|
|
99
|
+
flag === "-C=development" ||
|
|
100
|
+
((flag === "--conditions" || flag === "-C") && flags[i + 1] === "development"),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* #3981 — the package export conditions chant adds to `import`, `node` and
|
|
106
|
+
* `default` when it resolves a package itself (fold's import resolution, the
|
|
107
|
+
* sandbox bundle), so it lands on the module this process's `import()` would.
|
|
108
|
+
* Each chant package maps `development` to `src/*.ts` and `default` to the
|
|
109
|
+
* compiled `dist/*.js` it publishes. `["development"]` when the process
|
|
110
|
+
* resolves with it ({@link resolvesDevelopment}), else empty.
|
|
111
|
+
*/
|
|
112
|
+
export const SOURCE_EXPORT_CONDITIONS: readonly string[] = resolvesDevelopment() ? ["development"] : [];
|
|
@@ -357,7 +357,7 @@ function inWorktree(kindFile: string, checkout: string, worktree: string): strin
|
|
|
357
357
|
}
|
|
358
358
|
};
|
|
359
359
|
const [file, top, tree] = [real(kindFile), real(checkout), real(worktree)];
|
|
360
|
-
// Read from the worktree already (it sits
|
|
360
|
+
// Read from the worktree already (it sits in the checkout's .chant/work, #2893), or outside the checkout: as it is.
|
|
361
361
|
if (file.startsWith(`${tree}/`) || !file.startsWith(`${top}/`)) return file;
|
|
362
362
|
return join(tree, file.slice(top.length + 1));
|
|
363
363
|
}
|
|
@@ -15,13 +15,15 @@ import { discoverOps } from "./discover";
|
|
|
15
15
|
* with whatever the current test set (`realpath`ed, since macOS's tmpdir is a
|
|
16
16
|
* symlink and the walk compares resolved paths).
|
|
17
17
|
*/
|
|
18
|
-
let fakeGitRoot = "";
|
|
18
|
+
let fakeGitRoot: string | undefined = "";
|
|
19
19
|
vi.mock("../runtime-adapter", async (importOriginal) => ({
|
|
20
20
|
...(await importOriginal<typeof import("../runtime-adapter")>()),
|
|
21
21
|
getRuntime: () => ({
|
|
22
22
|
spawn: async (cmd: string[]) =>
|
|
23
23
|
cmd[0] === "git" && cmd[1] === "rev-parse"
|
|
24
|
-
?
|
|
24
|
+
? fakeGitRoot === undefined
|
|
25
|
+
? { stdout: "", stderr: "fatal: not a git repository", exitCode: 128 }
|
|
26
|
+
: { stdout: fakeGitRoot, stderr: "", exitCode: 0 }
|
|
25
27
|
: { stdout: "", stderr: "", exitCode: 0 },
|
|
26
28
|
}),
|
|
27
29
|
}));
|
|
@@ -68,6 +70,33 @@ describe("discoverOps — the scan root is the project, not the checkout (#2058)
|
|
|
68
70
|
});
|
|
69
71
|
});
|
|
70
72
|
|
|
73
|
+
describe("discoverOps outside a git repository (#2504)", () => {
|
|
74
|
+
let dir: string;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-nogit-")));
|
|
77
|
+
fakeGitRoot = undefined;
|
|
78
|
+
mkdirSync(join(dir, "demo", "ops"), { recursive: true });
|
|
79
|
+
writeFileSync(join(dir, "demo", "ops", "apply.op.ts"), OP_FILE("k3d-apply"));
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
fakeGitRoot = "";
|
|
83
|
+
rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a chant.config names the project root, so no git root is needed", async () => {
|
|
87
|
+
writeFileSync(join(dir, "demo", "chant.config.json"), JSON.stringify({ lexicons: ["k8s"] }));
|
|
88
|
+
const { ops, errors } = await discoverOps({ cwd: join(dir, "demo", "ops") });
|
|
89
|
+
expect(errors).toEqual([]);
|
|
90
|
+
expect([...ops.keys()]).toEqual(["k3d-apply"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("with neither a config nor a git root, discovery says both are missing", async () => {
|
|
94
|
+
await expect(discoverOps({ cwd: join(dir, "demo") })).rejects.toThrow(
|
|
95
|
+
/Not in a git repository, and no chant\.config/,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
71
100
|
/**
|
|
72
101
|
* #2171. An Op may be exported by name, not only as the file's default.
|
|
73
102
|
*
|
package/src/op/discover.ts
CHANGED
|
@@ -25,10 +25,11 @@ export interface OpDiscoveryResult {
|
|
|
25
25
|
errors: string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/** The git root `cwd` is inside, or undefined when it is in no repository. */
|
|
29
|
+
async function findGitRoot(cwd?: string): Promise<string | undefined> {
|
|
29
30
|
const rt = getRuntime();
|
|
30
31
|
const result = await rt.spawn(["git", "rev-parse", "--show-toplevel"], { cwd });
|
|
31
|
-
if (result.exitCode !== 0)
|
|
32
|
+
if (result.exitCode !== 0) return undefined;
|
|
32
33
|
return result.stdout.trim();
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -43,17 +44,26 @@ async function findGitRoot(cwd?: string): Promise<string> {
|
|
|
43
44
|
* file is the project boundary, the same one entity discovery respects.
|
|
44
45
|
* With no config anywhere up to the git root, the git root stands —
|
|
45
46
|
* #1675's original scope, kept for configless layouts.
|
|
47
|
+
*
|
|
48
|
+
* Outside any git repository the walk goes to the filesystem root, and the
|
|
49
|
+
* config is all discovery needs (#2504): a demo copy, an unpacked tarball or
|
|
50
|
+
* a fresh `chant init` before `git init`. Only a directory with neither a
|
|
51
|
+
* config above it nor a git root has no Op root, and that is the error.
|
|
46
52
|
*/
|
|
47
53
|
async function findDiscoveryRoot(cwd?: string): Promise<string> {
|
|
48
|
-
const
|
|
54
|
+
const found = await findGitRoot(cwd);
|
|
55
|
+
const gitRoot = found === undefined ? undefined : resolve(found);
|
|
49
56
|
let dir = resolve(cwd ?? process.cwd());
|
|
50
57
|
for (;;) {
|
|
51
58
|
if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) return dir;
|
|
52
59
|
if (dir === gitRoot) break;
|
|
53
60
|
const parent = dirname(dir);
|
|
54
|
-
if (parent === dir) break; // filesystem root — cwd was outside the git root
|
|
61
|
+
if (parent === dir) break; // filesystem root — cwd was outside the git root, or there is none
|
|
55
62
|
dir = parent;
|
|
56
63
|
}
|
|
64
|
+
if (gitRoot === undefined) {
|
|
65
|
+
throw new Error("Not in a git repository, and no chant.config.ts or chant.config.json above the current directory");
|
|
66
|
+
}
|
|
57
67
|
return gitRoot;
|
|
58
68
|
}
|
|
59
69
|
|
package/src/op/gate.test.ts
CHANGED
|
@@ -613,6 +613,36 @@ describe("op/gate — a rejected ledger push is reported, not swallowed (#2310)"
|
|
|
613
613
|
});
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
+
describe("op/gate — an applied record is written on the remote's ledger (#3985)", () => {
|
|
617
|
+
test("appendApplied fetches first, so a remote another writer moved still takes the push", async () => {
|
|
618
|
+
const { remote, author } = await clonePair();
|
|
619
|
+
const other = tmp("other");
|
|
620
|
+
git(["clone", "-q", remote, other], tmpdir());
|
|
621
|
+
git(["config", "user.email", "other@chant.dev"], other);
|
|
622
|
+
git(["config", "user.name", "Other"], other);
|
|
623
|
+
|
|
624
|
+
const port = gitGateLedgerPort({ cwd: author });
|
|
625
|
+
const pending = { op: "migrations", gate: "migrations-wave-1", timestamp: "2026-10-10T00:00:00.000Z", expiresAt: "2026-10-11T00:00:00.000Z" };
|
|
626
|
+
expect((await port.appendPending(pending)).pushed).toBe(true);
|
|
627
|
+
|
|
628
|
+
// Another writer moves the remote past what `author` holds.
|
|
629
|
+
git(["fetch", "-q", "origin", "chant/lifecycle:chant/lifecycle"], other);
|
|
630
|
+
expect((await gitGateLedgerPort({ cwd: other }).appendPending({ ...pending, gate: "migrations-wave-2" })).pushed).toBe(true);
|
|
631
|
+
|
|
632
|
+
const line = {
|
|
633
|
+
version: 1 as const, kind: "applied" as const, op: "migrations", gate: "migrations-wave-1", planDigest: "sha256:aa",
|
|
634
|
+
approvedAt: "2026-10-10T01:00:00.000Z", approvedBy: "alice", timestamp: "2026-10-10T02:00:00.000Z",
|
|
635
|
+
};
|
|
636
|
+
expect(await port.appendApplied!(line)).toEqual({ pushed: true });
|
|
637
|
+
|
|
638
|
+
// The remote holds the applied line and the other writer's pending fact.
|
|
639
|
+
const reader = ciCheckout(remote);
|
|
640
|
+
const readerPort = gitGateLedgerPort({ cwd: reader });
|
|
641
|
+
expect(await readerPort.readApplied!("migrations")).toEqual([line]);
|
|
642
|
+
expect((await readerPort.read("migrations")).pending.map((p) => p.gate)).toEqual(["migrations-wave-1", "migrations-wave-2"]);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
616
646
|
describe("op/gate — the approve hint on a sealed gate (#3521)", () => {
|
|
617
647
|
test("approveCommand adds --sign last, only when sealed", () => {
|
|
618
648
|
expect(approveCommand("app", "ship")).toBe("chant approve app ship");
|
package/src/op/gate.ts
CHANGED
|
@@ -176,6 +176,9 @@ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
|
|
|
176
176
|
async appendApplied(record) {
|
|
177
177
|
const { dir, file } = appliedGatePath(record.op);
|
|
178
178
|
const line = JSON.stringify(record, sortedJsonReplacer);
|
|
179
|
+
// #3985: write on top of the remote's ledger, not a local copy another
|
|
180
|
+
// writer has since moved past, so the push that follows can land.
|
|
181
|
+
await requireLifecycleLedger(opts);
|
|
179
182
|
let lastErr: unknown;
|
|
180
183
|
// The same append-and-retry as a ledger line: another writer to the branch is the ordinary case.
|
|
181
184
|
for (let attempt = 1; attempt <= 5; attempt++) {
|