@intentius/chant 0.23.0 → 0.24.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/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +8 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +8 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lint/config.d.ts +1 -12
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/project-root.d.ts +51 -0
- package/dist/project-root.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/build.ts +29 -6
- package/src/cli/handlers/build.test.ts +11 -9
- package/src/cli/handlers/build.ts +7 -2
- package/src/cli/handlers/run.test.ts +31 -0
- package/src/cli/handlers/run.ts +15 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +17 -3
- package/src/cli/plugins.ts +10 -2
- package/src/config.ts +23 -0
- package/src/lint/config.test.ts +9 -4
- package/src/lint/config.ts +7 -23
- package/src/lint/policy.ts +5 -5
- package/src/project-root.test.ts +105 -0
- package/src/project-root.ts +78 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AASnD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2JAA2J;IAC3J,WAAW,CAAC,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,EAAE,CAAC;CACjE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AASnD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2JAA2J;IAC3J,WAAW,CAAC,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,EAAE,CAAC;CACjE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAuW9E;AAiDD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAIlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAItD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACxC,MAAM,IAAI,CAmCZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/build.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/build.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgFlD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6EnE"}
|
|
@@ -32,6 +32,14 @@ export declare function runOpCancel(ctx: CommandContext): Promise<number>;
|
|
|
32
32
|
* (`../../components/driver.ts`) rather than a `*.op.ts` Op. Checked first,
|
|
33
33
|
* mirroring `runGraph`'s `if (ctx.args.components) return
|
|
34
34
|
* runComponentGraph(ctx)` branch (../handlers/graph.ts).
|
|
35
|
+
*
|
|
36
|
+
* chant #1116 — `--report` with `--components` is checked and hard-errored
|
|
37
|
+
* before that dispatch. There is no preview/dry-run mode for the component
|
|
38
|
+
* driver: unlike the Op path (where `--report` reads a past Temporal run),
|
|
39
|
+
* `runOpComponents` has never read `ctx.args.report` at all, so the flag was
|
|
40
|
+
* silently ignored and the command fell through to a real dispatch — observed
|
|
41
|
+
* live reaching an actual cloud shell-out. Erroring here is the safe minimum
|
|
42
|
+
* called out on the issue; a real preview is future work.
|
|
35
43
|
*/
|
|
36
44
|
export declare function runOp(ctx: CommandContext): Promise<number>;
|
|
37
45
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAO7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA4ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCtE;AA0CD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAO7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA4ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCtE;AA0CD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBhE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsF1E;AA+OD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiFrE;AAuLD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
|
package/dist/cli/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAsB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAsB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA+NpD;AA0ZD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
|
package/dist/cli/plugins.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ export declare function loadPlugins(lexiconNames: string[]): Promise<LexiconPlug
|
|
|
32
32
|
* that never needed live module exports to begin with. This is strictly
|
|
33
33
|
* better than routing the detection through the sandbox: it removes the
|
|
34
34
|
* execution entirely rather than containing it, at no bundling/spawn cost.
|
|
35
|
+
*
|
|
36
|
+
* chant #1117 — walks up from `projectPath` to the project root
|
|
37
|
+
* (`loadChantConfigUpward`) rather than reading `projectPath` alone: `chant
|
|
38
|
+
* build src/<stack>` calls this with the stack's own subdirectory, and a
|
|
39
|
+
* project that declares `lexicons` only in its root `chant.config.ts` (never
|
|
40
|
+
* detectable by `detectLexicons()`'s import scan alone, e.g. a lexicon that's
|
|
41
|
+
* loaded but never imported by name in that particular stack's files) would
|
|
42
|
+
* otherwise silently miss it.
|
|
35
43
|
*/
|
|
36
44
|
export declare function resolveProjectLexicons(projectPath: string): Promise<string[]>;
|
|
37
45
|
//# sourceMappingURL=plugins.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/cli/plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAKjE;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B5E;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA6BlF;AAED
|
|
1
|
+
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/cli/plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAKjE;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B5E;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA6BlF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnF"}
|
package/dist/config.d.ts
CHANGED
|
@@ -280,6 +280,24 @@ export declare const DEFAULT_CHANT_CONFIG: ChantConfig;
|
|
|
280
280
|
* Returns default config if neither exists.
|
|
281
281
|
*/
|
|
282
282
|
export declare function loadChantConfig(dir: string): Promise<ResolvedConfig>;
|
|
283
|
+
/**
|
|
284
|
+
* Load project configuration by walking up from `startDir` to the project
|
|
285
|
+
* root (chant #1117), instead of trying only `startDir` itself.
|
|
286
|
+
*
|
|
287
|
+
* `chant build src/<stack>` (and anything else invoked with a subdirectory —
|
|
288
|
+
* `lint.policies`' `evaluateProjectPolicies`, `--components --generate`)
|
|
289
|
+
* builds scoped to that subdirectory, but `chant.config.ts` almost always
|
|
290
|
+
* lives at the project root, one or more levels up. Before this, callers
|
|
291
|
+
* either read `startDir` alone or bolted on a single `dirname()` fallback —
|
|
292
|
+
* fine for a one-level-deep stack, silently blind to anything deeper
|
|
293
|
+
* (loomster's `src/<stack>` layout is exactly one level too deep: `buildParams`'
|
|
294
|
+
* declared `env:` mappings never resolved, so `LOOM_TIER`/`LOOM_ENV` were inert
|
|
295
|
+
* under every `npm run synth:*` for two releases — loomster#162). Uses the
|
|
296
|
+
* same walk `chant lint`/`chant graph` already used ({@link findProjectConfig},
|
|
297
|
+
* shared with `./lint/config.ts`'s `findProjectRoot`) — one config-discovery
|
|
298
|
+
* contract for the whole CLI.
|
|
299
|
+
*/
|
|
300
|
+
export declare function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig>;
|
|
283
301
|
/**
|
|
284
302
|
* Resolve the ownership marker to stamp from project config, or undefined when
|
|
285
303
|
* ownership marking is off (no `stack`, or `enabled: false`).
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lint/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { Severity, RuleConfig } from "./rule.js";
|
|
3
|
+
export { findProjectRoot } from "../project-root.js";
|
|
3
4
|
export declare const LintConfigSchema: z.ZodObject<{
|
|
4
5
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
5
6
|
error: "error";
|
|
@@ -75,18 +76,6 @@ export declare function parseRuleConfig(value: RuleConfig): ParsedRuleConfig;
|
|
|
75
76
|
* Default configuration with all rules enabled at strict preset severities
|
|
76
77
|
*/
|
|
77
78
|
export declare const DEFAULT_CONFIG: LintConfig;
|
|
78
|
-
/**
|
|
79
|
-
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
80
|
-
* config (`chant.config.ts` or `chant.config.json`). Returns that directory, or
|
|
81
|
-
* `startDir` unchanged when none is found before the filesystem root.
|
|
82
|
-
*
|
|
83
|
-
* Linting a subpath (`chant graph src --format ir`, `chant lint src/lib`) must
|
|
84
|
-
* still see the project-root config: its `lint.overrides` globs are written
|
|
85
|
-
* project-root-relative (`src/lib/**`), and a rule set scoped only to the lint
|
|
86
|
-
* arg would silently drop them. Config discovery therefore anchors on the
|
|
87
|
-
* project root, not the path being linted.
|
|
88
|
-
*/
|
|
89
|
-
export declare function findProjectRoot(startDir: string): string;
|
|
90
79
|
/**
|
|
91
80
|
* Load lint configuration from a directory.
|
|
92
81
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lint/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lint/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AASnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAiBlD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAS3B,CAAC;AAkGH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAsBnE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,UAG5B,CAAC;AA0HF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAoClD;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAkBpG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared upward config-discovery walk (chant #1117).
|
|
3
|
+
*
|
|
4
|
+
* Before this, `chant build <subdir>` and `lint.policies`
|
|
5
|
+
* (`./lint/policy.ts`'s `evaluateProjectPolicies`) each searched only the
|
|
6
|
+
* build directory and its immediate parent for `chant.config.ts`/`.json`,
|
|
7
|
+
* while `chant lint`/`chant graph` (`./lint/config.ts`'s old, file-local
|
|
8
|
+
* `findProjectRoot`) already walked all the way up. A project with a deeper
|
|
9
|
+
* `src/<stack>` layout — `chant build src/<stack>` two or more levels below
|
|
10
|
+
* the project root — silently never found the root config: `buildParams`'
|
|
11
|
+
* declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
|
|
12
|
+
* fell back to defaults, and nothing warned (loomster#162: `LOOM_TIER`/
|
|
13
|
+
* `LOOM_ENV` inert under every `npm run synth:*` for two releases).
|
|
14
|
+
*
|
|
15
|
+
* `findProjectConfig` is the one walk every config-discovery call site now
|
|
16
|
+
* shares. It stops at the first of:
|
|
17
|
+
*
|
|
18
|
+
* 1. A directory holding `chant.config.ts` or `chant.config.json` — found.
|
|
19
|
+
* 2. A directory holding `.git` or `package.json` with no chant config of its
|
|
20
|
+
* own — the project boundary. Discovery must never wander past the actual
|
|
21
|
+
* project into an unrelated ancestor directory just because this project
|
|
22
|
+
* happens not to declare a config (a stray `chant.config.ts` two levels
|
|
23
|
+
* above an unrelated git repo must never be picked up).
|
|
24
|
+
* 3. `startDir` itself, unchanged, if the walk reaches the filesystem root
|
|
25
|
+
* without ever finding a config OR a boundary marker. This is not just a
|
|
26
|
+
* "give up gracefully" nicety — several callers (`resolveProjectLexicons`
|
|
27
|
+
* -> `findInfraFiles`) scope a real directory walk off this function's
|
|
28
|
+
* result; if a rootless/marker-less start dir (a bare tmpdir, as chant's
|
|
29
|
+
* own test suites use) resolved all the way to `/`, that downstream walk
|
|
30
|
+
* would scan the entire filesystem instead of failing fast. Falling back
|
|
31
|
+
* to `startDir` keeps every caller's blast radius local no matter how far
|
|
32
|
+
* up the walk had to look.
|
|
33
|
+
*/
|
|
34
|
+
export interface ProjectConfigSearch {
|
|
35
|
+
/** The resolved project root: the config's directory, the boundary directory, or the (resolved) `startDir` when neither was found. */
|
|
36
|
+
dir: string;
|
|
37
|
+
/** Absolute path to the discovered `chant.config.ts`/`.json`, if any. */
|
|
38
|
+
configPath?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Walk up from `startDir` (inclusive) to the nearest chant config or project boundary. See {@link ProjectConfigSearch}. */
|
|
41
|
+
export declare function findProjectConfig(startDir: string): ProjectConfigSearch;
|
|
42
|
+
/**
|
|
43
|
+
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
44
|
+
* config (`chant.config.ts` or `chant.config.json`), the `.git`/`package.json`
|
|
45
|
+
* project boundary, or `startDir` itself when neither is found. Thin wrapper
|
|
46
|
+
* over {@link findProjectConfig} for callers that only need the directory
|
|
47
|
+
* (e.g. `chant lint`'s rule/plugin resolution, which just needs *a* stable
|
|
48
|
+
* root to resolve relative paths against).
|
|
49
|
+
*/
|
|
50
|
+
export declare function findProjectRoot(startDir: string): string;
|
|
51
|
+
//# sourceMappingURL=project-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-root.d.ts","sourceRoot":"","sources":["../src/project-root.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,mBAAmB;IAClC,sIAAsI;IACtI,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4HAA4H;AAC5H,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAqBvE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { build } from "../../build";
|
|
2
|
-
import {
|
|
2
|
+
import { loadChantConfigUpward, resolveOwnershipMarker, resolveFoldEnabled, resolveSandboxEnabled } from "../../config";
|
|
3
3
|
import { resolveCliBuildParams } from "../build-params-cli";
|
|
4
4
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
5
5
|
import type { LexiconPlugin } from "../../lexicon";
|
|
@@ -128,11 +128,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
128
128
|
// Resolve the path
|
|
129
129
|
const infraPath = resolve(options.path);
|
|
130
130
|
|
|
131
|
-
// Resolve opt-in ownership marking from project config
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// Resolve opt-in ownership marking from project config. chant #1117 — walks
|
|
132
|
+
// up from the infra dir to the project root (`loadChantConfigUpward`), not
|
|
133
|
+
// just the infra dir's immediate parent: a project whose stacks live two or
|
|
134
|
+
// more levels below `chant.config.ts` (loomster's `src/<stack>` layout)
|
|
135
|
+
// otherwise never finds the root config at all, and every declared
|
|
136
|
+
// `buildParams`/`ownership`/`lint.policies` setting silently falls back to
|
|
137
|
+
// its default.
|
|
138
|
+
const loaded = await loadChantConfigUpward(infraPath);
|
|
136
139
|
const config = loaded.config;
|
|
137
140
|
const ownership = resolveOwnershipMarker(config);
|
|
138
141
|
|
|
@@ -174,6 +177,26 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
174
177
|
return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
// chant #1117 — a project that declares buildParams but resolves NONE of
|
|
181
|
+
// them for this build is the exact shape that let loomster#162 live for two
|
|
182
|
+
// releases: the discovered config wasn't the one the project author
|
|
183
|
+
// expected (a stale --path, a workspace boundary that stopped the walk
|
|
184
|
+
// short), or every declared parameter's `env:` var went unset, and either
|
|
185
|
+
// way every `params.<name>` read silently falls back to `undefined` — with
|
|
186
|
+
// no error (an all-`required: false` declaration resolves successfully to
|
|
187
|
+
// an empty set). Warn, naming the config path this build actually
|
|
188
|
+
// discovered, so a mismatch is visible instead of silent.
|
|
189
|
+
if (
|
|
190
|
+
Object.keys(config.buildParams ?? {}).length > 0 &&
|
|
191
|
+
paramsResolution.provenance.length === 0
|
|
192
|
+
) {
|
|
193
|
+
warnings.push(
|
|
194
|
+
formatWarning({
|
|
195
|
+
message: `chant.config.ts declares buildParams${loaded.configPath ? ` (${loaded.configPath})` : ""}, but none resolved for this build — every params.<name> read will be undefined. Pass --param/--params-file, or check that this is the config you expect.`,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
177
200
|
// #1039 — thread each loaded plugin's registered intrinsics (e.g. AWS's
|
|
178
201
|
// `Sub`) through to the fold path, so a file using a registered intrinsic
|
|
179
202
|
// tagged template folds instead of unconditionally falling back to run.
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* run --components` (see ../handlers/run.test.ts's "build-time parameters"
|
|
9
9
|
* describe block for the equivalent local/`--temporal` coverage).
|
|
10
10
|
*
|
|
11
|
-
* Mocks `generateComponentsPipeline` and `
|
|
12
|
-
*
|
|
11
|
+
* Mocks `generateComponentsPipeline` and `loadChantConfigUpward` (chant
|
|
12
|
+
* #1117 — `runGenerateComponents` walks up to the project root now, same as
|
|
13
|
+
* `chant build` proper, instead of reading `args.path` alone) and exercises
|
|
14
|
+
* the public `runBuild` dispatcher (`runGenerateComponents` itself isn't
|
|
13
15
|
* exported), mirroring `run.test.ts`'s style of driving the handler through
|
|
14
16
|
* its `CommandContext` entrypoint rather than reaching into private helpers.
|
|
15
17
|
*/
|
|
@@ -17,14 +19,14 @@ import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
|
17
19
|
import type { ParsedArgs } from "../registry";
|
|
18
20
|
|
|
19
21
|
const generateComponentsPipelineMock = vi.fn();
|
|
20
|
-
const
|
|
22
|
+
const loadChantConfigUpwardMock = vi.fn();
|
|
21
23
|
|
|
22
24
|
vi.mock("../../components/cli-support", () => ({
|
|
23
25
|
generateComponentsPipeline: (...args: unknown[]) => generateComponentsPipelineMock(...args),
|
|
24
26
|
}));
|
|
25
27
|
vi.mock("../../config", async () => {
|
|
26
28
|
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
27
|
-
return { ...actual,
|
|
29
|
+
return { ...actual, loadChantConfigUpward: (...args: unknown[]) => loadChantConfigUpwardMock(...args) };
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
const { runBuild } = await import("./build");
|
|
@@ -54,7 +56,7 @@ function makeStderrSpy() {
|
|
|
54
56
|
describe("runBuild --components --generate (chant #1108 build-time parameters)", () => {
|
|
55
57
|
beforeEach(() => {
|
|
56
58
|
generateComponentsPipelineMock.mockReset();
|
|
57
|
-
|
|
59
|
+
loadChantConfigUpwardMock.mockReset().mockResolvedValue({ config: {} });
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
test("no declared buildParams → generateComponentsPipeline is called with an empty provenance array", async () => {
|
|
@@ -69,7 +71,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
|
|
72
|
-
|
|
74
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
73
75
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
74
76
|
});
|
|
75
77
|
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
@@ -91,7 +93,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
test("--param overrides a declared default", async () => {
|
|
94
|
-
|
|
96
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
95
97
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
96
98
|
});
|
|
97
99
|
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
@@ -115,7 +117,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
test("an unresolved required build-time parameter → exit 1, never reaches generateComponentsPipeline", async () => {
|
|
118
|
-
|
|
120
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
119
121
|
config: { buildParams: { tier: { type: "string" } } },
|
|
120
122
|
});
|
|
121
123
|
const stderr = makeStderrSpy();
|
|
@@ -128,7 +130,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
test("an enum violation on --param → exit 1, never reaches generateComponentsPipeline", async () => {
|
|
131
|
-
|
|
133
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
132
134
|
config: { buildParams: { tier: { type: "string", enum: ["light", "production"] } } },
|
|
133
135
|
});
|
|
134
136
|
const stderr = makeStderrSpy();
|
|
@@ -4,7 +4,7 @@ import { buildCommand, buildCommandWatch, printErrors, printWarnings, resolveBui
|
|
|
4
4
|
import { formatError, formatInfo, formatSuccess, formatBold } from "../format";
|
|
5
5
|
import type { CommandContext } from "../registry";
|
|
6
6
|
import { generateComponentsPipeline } from "../../components/cli-support";
|
|
7
|
-
import {
|
|
7
|
+
import { loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
8
8
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -23,12 +23,17 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
|
23
23
|
* Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
|
|
24
24
|
* this command too — generate mode shares `discoverComponents` with `chant
|
|
25
25
|
* run --components`, so it had the identical gap.
|
|
26
|
+
*
|
|
27
|
+
* chant #1117 — loads config by walking up from `args.path` to the project
|
|
28
|
+
* root (`loadChantConfigUpward`), same as `chant build` proper, instead of
|
|
29
|
+
* `args.path` alone: a components-only project built from a subdirectory
|
|
30
|
+
* otherwise never sees the root `chant.config.ts`'s `buildParams` either.
|
|
26
31
|
*/
|
|
27
32
|
async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
28
33
|
const { args } = ctx;
|
|
29
34
|
const lexicon = args.generate as string;
|
|
30
35
|
|
|
31
|
-
const { config } = await
|
|
36
|
+
const { config } = await loadChantConfigUpward(resolve(args.path)).catch(() => ({ config: {} as ChantConfig }));
|
|
32
37
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
33
38
|
cli: parseParamFlags(args.param),
|
|
34
39
|
paramsFile: args.paramsFile,
|
|
@@ -882,6 +882,37 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
|
|
|
882
882
|
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
883
883
|
vi.restoreAllMocks();
|
|
884
884
|
});
|
|
885
|
+
|
|
886
|
+
// chant #1116 — --report is Op/Temporal-only (reads a past workflow run);
|
|
887
|
+
// the component driver never checked it, so it was silently ignored and the
|
|
888
|
+
// command fell through to a real dispatch. Hard-error instead, before
|
|
889
|
+
// runComponents is ever reached.
|
|
890
|
+
test("--report combined with --components → exit 1 before any dispatch, no fall-through (#1116)", async () => {
|
|
891
|
+
discoverOpsMock.mockReset();
|
|
892
|
+
const stderr = makeStderrSpy();
|
|
893
|
+
|
|
894
|
+
const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true, temporal: false }), plugins: [], serializers: [] });
|
|
895
|
+
|
|
896
|
+
expect(exit).toBe(1);
|
|
897
|
+
expect(stderr.join("\n")).toContain("not supported with --components");
|
|
898
|
+
expect(stderr.join("\n")).toContain("#1116");
|
|
899
|
+
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
900
|
+
expect(runComponentsMock).not.toHaveBeenCalled();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Plain --components (no --report) must be unaffected: it still reaches a
|
|
904
|
+
// real dispatch through runComponents — mocked here, never a real cloud call.
|
|
905
|
+
test("plain --components (no --report) still dispatches to runComponents (#1116 regression guard)", async () => {
|
|
906
|
+
discoverOpsMock.mockReset();
|
|
907
|
+
runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
|
|
908
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
909
|
+
|
|
910
|
+
const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false, temporal: false }), plugins: [], serializers: [] });
|
|
911
|
+
|
|
912
|
+
expect(exit).toBe(0);
|
|
913
|
+
expect(runComponentsMock).toHaveBeenCalled();
|
|
914
|
+
vi.restoreAllMocks();
|
|
915
|
+
});
|
|
885
916
|
});
|
|
886
917
|
|
|
887
918
|
describe("runOpComponents", () => {
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -548,8 +548,23 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
|
|
|
548
548
|
* (`../../components/driver.ts`) rather than a `*.op.ts` Op. Checked first,
|
|
549
549
|
* mirroring `runGraph`'s `if (ctx.args.components) return
|
|
550
550
|
* runComponentGraph(ctx)` branch (../handlers/graph.ts).
|
|
551
|
+
*
|
|
552
|
+
* chant #1116 — `--report` with `--components` is checked and hard-errored
|
|
553
|
+
* before that dispatch. There is no preview/dry-run mode for the component
|
|
554
|
+
* driver: unlike the Op path (where `--report` reads a past Temporal run),
|
|
555
|
+
* `runOpComponents` has never read `ctx.args.report` at all, so the flag was
|
|
556
|
+
* silently ignored and the command fell through to a real dispatch — observed
|
|
557
|
+
* live reaching an actual cloud shell-out. Erroring here is the safe minimum
|
|
558
|
+
* called out on the issue; a real preview is future work.
|
|
551
559
|
*/
|
|
552
560
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
561
|
+
if (ctx.args.components && ctx.args.report) {
|
|
562
|
+
console.error(formatError({
|
|
563
|
+
message: "--report is not supported with --components",
|
|
564
|
+
hint: "No preview/dry-run mode exists yet for the component driver (see chant#1116). Omit --report.",
|
|
565
|
+
}));
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
553
568
|
if (ctx.args.components) return runOpComponents(ctx);
|
|
554
569
|
if (ctx.args.local && ctx.args.temporal) {
|
|
555
570
|
console.error(formatError({
|
package/src/cli/main.test.ts
CHANGED
|
@@ -270,6 +270,29 @@ describe("parseArgs", () => {
|
|
|
270
270
|
const result = parseArgs(["build", "src", "--params-file", "./params.json"]);
|
|
271
271
|
expect(result.paramsFile).toBe("./params.json");
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
// ── --param=name=value hard error (chant #1118) ──────────────────────────
|
|
275
|
+
// The joined `--flag=value` form is not supported anywhere in this parser
|
|
276
|
+
// (see "ignores unknown flags" above) — a dropped --param can silently
|
|
277
|
+
// change what a build measures/deploys, so this form is rejected loudly
|
|
278
|
+
// instead of silently accepted as a no-op.
|
|
279
|
+
|
|
280
|
+
test("--param=name=value throws instead of silently dropping", () => {
|
|
281
|
+
expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param=tier=production/);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("--param=name=value error names the working form", () => {
|
|
285
|
+
expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param name=value/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("--param= (empty value) also throws", () => {
|
|
289
|
+
expect(() => parseArgs(["build", "src", "--param="])).toThrow(/--param=/);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("plain --param name=value is unaffected", () => {
|
|
293
|
+
const result = parseArgs(["build", "src", "--param", "tier=production"]);
|
|
294
|
+
expect(result.param).toEqual(["tier=production"]);
|
|
295
|
+
});
|
|
273
296
|
});
|
|
274
297
|
|
|
275
298
|
// ── resolveCommand tests ──────────────────────────────────────────
|
package/src/cli/main.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isEntryPoint } from "./is-entry-point";
|
|
|
5
5
|
import { formatSuccess, formatError } from "./format";
|
|
6
6
|
import { loadPlugins, resolveProjectLexicons } from "./plugins";
|
|
7
7
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
8
|
-
import {
|
|
8
|
+
import { loadChantConfigUpward } from "../config";
|
|
9
9
|
import { ENV_VAR, unknownEnvError } from "../env";
|
|
10
10
|
import { initRuntime } from "../runtime-adapter";
|
|
11
11
|
import { runBuild } from "./handlers/build";
|
|
@@ -224,6 +224,16 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
224
224
|
result.sandbox = true;
|
|
225
225
|
} else if (arg === "--param") {
|
|
226
226
|
(result.param ??= []).push(args[++i]);
|
|
227
|
+
} else if (arg.startsWith("--param=")) {
|
|
228
|
+
// chant #1118 — this parser never supports an `--flag=value` joined
|
|
229
|
+
// form for any value-taking flag (every branch above is an exact `===`
|
|
230
|
+
// match, so a joined token falls through unrecognized and is silently
|
|
231
|
+
// dropped — see the "ignores unknown flags" case below). `--param
|
|
232
|
+
// name=value` (space-separated) is the only accepted form. Rather than
|
|
233
|
+
// teach the parser joined forms generally, `--param=name=value` is
|
|
234
|
+
// called out as a hard error instead of a silent no-op: a dropped
|
|
235
|
+
// `--param` can silently change what a build measures/deploys.
|
|
236
|
+
throw new Error(`${arg} is not supported. Use --param name=value (space-separated) instead.`);
|
|
227
237
|
} else if (arg === "--params-file") {
|
|
228
238
|
result.paramsFile = args[++i];
|
|
229
239
|
} else if (!arg.startsWith("-")) {
|
|
@@ -588,11 +598,15 @@ async function main(): Promise<void> {
|
|
|
588
598
|
// chant.config itself may branch on the env. (#505)
|
|
589
599
|
if (args.env) process.env[ENV_VAR] = args.env;
|
|
590
600
|
|
|
591
|
-
// Initialize runtime adapter early — before plugins or commands run
|
|
601
|
+
// Initialize runtime adapter early — before plugins or commands run.
|
|
602
|
+
// chant #1117 — walks up from `args.path` to the project root: for a
|
|
603
|
+
// subdirectory build/command (`chant build src/<stack> --env prod`) the
|
|
604
|
+
// declared `environments` almost always live in the root `chant.config.ts`,
|
|
605
|
+
// not `args.path` itself.
|
|
592
606
|
const projectPath0 = resolve(args.path === "." ? "." : args.path);
|
|
593
607
|
let loadedConfig;
|
|
594
608
|
try {
|
|
595
|
-
loadedConfig = await
|
|
609
|
+
loadedConfig = await loadChantConfigUpward(projectPath0);
|
|
596
610
|
initRuntime();
|
|
597
611
|
} catch {
|
|
598
612
|
// Config may not exist yet (e.g. `chant init`)
|
package/src/cli/plugins.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
|
|
2
|
-
import {
|
|
2
|
+
import { loadChantConfigUpward } from "../config";
|
|
3
3
|
import { findInfraFiles, detectLexicons } from "../index";
|
|
4
4
|
import { checkConflicts } from "./conflict-check";
|
|
5
5
|
|
|
@@ -96,9 +96,17 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
|
|
|
96
96
|
* that never needed live module exports to begin with. This is strictly
|
|
97
97
|
* better than routing the detection through the sandbox: it removes the
|
|
98
98
|
* execution entirely rather than containing it, at no bundling/spawn cost.
|
|
99
|
+
*
|
|
100
|
+
* chant #1117 — walks up from `projectPath` to the project root
|
|
101
|
+
* (`loadChantConfigUpward`) rather than reading `projectPath` alone: `chant
|
|
102
|
+
* build src/<stack>` calls this with the stack's own subdirectory, and a
|
|
103
|
+
* project that declares `lexicons` only in its root `chant.config.ts` (never
|
|
104
|
+
* detectable by `detectLexicons()`'s import scan alone, e.g. a lexicon that's
|
|
105
|
+
* loaded but never imported by name in that particular stack's files) would
|
|
106
|
+
* otherwise silently miss it.
|
|
99
107
|
*/
|
|
100
108
|
export async function resolveProjectLexicons(projectPath: string): Promise<string[]> {
|
|
101
|
-
const { config } = await
|
|
109
|
+
const { config } = await loadChantConfigUpward(projectPath);
|
|
102
110
|
|
|
103
111
|
if (config.lexicons && config.lexicons.length > 0) {
|
|
104
112
|
return config.lexicons;
|
package/src/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { DEFAULT_SBOM_FORMAT, type SbomFormat } from "./components/verbs/sbom-ge
|
|
|
7
7
|
import type { Severity } from "./components/verbs/vuln-scan";
|
|
8
8
|
import type { VulnPolicy } from "./components/verbs/vuln-gate";
|
|
9
9
|
import type { BuildParamsConfig } from "./build-params";
|
|
10
|
+
import { findProjectConfig } from "./project-root";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Zod schema for ChantConfig validation.
|
|
@@ -297,6 +298,28 @@ export async function loadChantConfig(dir: string): Promise<ResolvedConfig> {
|
|
|
297
298
|
return { config: DEFAULT_CHANT_CONFIG };
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Load project configuration by walking up from `startDir` to the project
|
|
303
|
+
* root (chant #1117), instead of trying only `startDir` itself.
|
|
304
|
+
*
|
|
305
|
+
* `chant build src/<stack>` (and anything else invoked with a subdirectory —
|
|
306
|
+
* `lint.policies`' `evaluateProjectPolicies`, `--components --generate`)
|
|
307
|
+
* builds scoped to that subdirectory, but `chant.config.ts` almost always
|
|
308
|
+
* lives at the project root, one or more levels up. Before this, callers
|
|
309
|
+
* either read `startDir` alone or bolted on a single `dirname()` fallback —
|
|
310
|
+
* fine for a one-level-deep stack, silently blind to anything deeper
|
|
311
|
+
* (loomster's `src/<stack>` layout is exactly one level too deep: `buildParams`'
|
|
312
|
+
* declared `env:` mappings never resolved, so `LOOM_TIER`/`LOOM_ENV` were inert
|
|
313
|
+
* under every `npm run synth:*` for two releases — loomster#162). Uses the
|
|
314
|
+
* same walk `chant lint`/`chant graph` already used ({@link findProjectConfig},
|
|
315
|
+
* shared with `./lint/config.ts`'s `findProjectRoot`) — one config-discovery
|
|
316
|
+
* contract for the whole CLI.
|
|
317
|
+
*/
|
|
318
|
+
export async function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig> {
|
|
319
|
+
const { dir } = findProjectConfig(startDir);
|
|
320
|
+
return loadChantConfig(dir);
|
|
321
|
+
}
|
|
322
|
+
|
|
300
323
|
/**
|
|
301
324
|
* Resolve the ownership marker to stamp from project config, or undefined when
|
|
302
325
|
* ownership marking is off (no `stack`, or `enabled: false`).
|
package/src/lint/config.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { loadConfig, DEFAULT_CONFIG, findProjectRoot } from "./config";
|
|
3
3
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
5
|
|
|
6
6
|
const TEST_DIR = join(import.meta.dirname, "__test_config__");
|
|
7
7
|
|
|
@@ -705,10 +705,15 @@ describe("findProjectRoot", () => {
|
|
|
705
705
|
expect(findProjectRoot(sub)).toBe(TEST_DIR);
|
|
706
706
|
});
|
|
707
707
|
|
|
708
|
-
test("
|
|
708
|
+
test("stops at the nearest .git/package.json boundary when no config is found (#1117)", () => {
|
|
709
709
|
const sub = join(TEST_DIR, "nowhere");
|
|
710
710
|
mkdirSync(sub, { recursive: true });
|
|
711
|
-
// No chant.config anywhere under TEST_DIR —
|
|
712
|
-
|
|
711
|
+
// No chant.config anywhere under TEST_DIR — walking up from this real
|
|
712
|
+
// repo location reaches `packages/core`'s own package.json before the
|
|
713
|
+
// filesystem root, so that's the returned boundary, not `sub` itself
|
|
714
|
+
// (chant #1117 — discovery must never wander past the project just
|
|
715
|
+
// because it declares no config).
|
|
716
|
+
const packageRoot = resolve(import.meta.dirname, "..", "..");
|
|
717
|
+
expect(findProjectRoot(sub)).toBe(packageRoot);
|
|
713
718
|
});
|
|
714
719
|
});
|
package/src/lint/config.ts
CHANGED
|
@@ -6,6 +6,13 @@ import type { Severity, RuleConfig } from "./rule";
|
|
|
6
6
|
import { moduleDir, getRuntime } from "../runtime-adapter";
|
|
7
7
|
import strictPreset from "./presets/strict.json";
|
|
8
8
|
|
|
9
|
+
// chant #1117 — the upward config-discovery walk moved to a shared module
|
|
10
|
+
// (`../project-root`) so `chant build`/`lint.policies` use the identical walk
|
|
11
|
+
// `chant lint`/`chant graph` already did. Re-exported here since this is
|
|
12
|
+
// still where every existing call site (`./config.test.ts`, `../cli/commands/lint.ts`)
|
|
13
|
+
// imports it from.
|
|
14
|
+
export { findProjectRoot } from "../project-root";
|
|
15
|
+
|
|
9
16
|
/** Mapping of built-in preset names to their file paths */
|
|
10
17
|
const BUILTIN_PRESETS: Record<string, string> = {
|
|
11
18
|
"@intentius/chant/lint/presets/strict": resolve(moduleDir(import.meta.url), "presets/strict.json"),
|
|
@@ -323,29 +330,6 @@ function loadConfigFile(configPath: string, visited: Set<string> = new Set()): L
|
|
|
323
330
|
return mergedConfig;
|
|
324
331
|
}
|
|
325
332
|
|
|
326
|
-
/**
|
|
327
|
-
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
328
|
-
* config (`chant.config.ts` or `chant.config.json`). Returns that directory, or
|
|
329
|
-
* `startDir` unchanged when none is found before the filesystem root.
|
|
330
|
-
*
|
|
331
|
-
* Linting a subpath (`chant graph src --format ir`, `chant lint src/lib`) must
|
|
332
|
-
* still see the project-root config: its `lint.overrides` globs are written
|
|
333
|
-
* project-root-relative (`src/lib/**`), and a rule set scoped only to the lint
|
|
334
|
-
* arg would silently drop them. Config discovery therefore anchors on the
|
|
335
|
-
* project root, not the path being linted.
|
|
336
|
-
*/
|
|
337
|
-
export function findProjectRoot(startDir: string): string {
|
|
338
|
-
let dir = resolve(startDir);
|
|
339
|
-
for (;;) {
|
|
340
|
-
if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) {
|
|
341
|
-
return dir;
|
|
342
|
-
}
|
|
343
|
-
const parent = dirname(dir);
|
|
344
|
-
if (parent === dir) return resolve(startDir);
|
|
345
|
-
dir = parent;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
333
|
/**
|
|
350
334
|
* Load lint configuration from a directory.
|
|
351
335
|
*
|
package/src/lint/policy.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `policyGate` Op step runs this to gate an apply on the same checks.
|
|
5
5
|
*/
|
|
6
6
|
import { resolve, dirname } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { loadChantConfigUpward } from "../config";
|
|
8
8
|
import { resolveProjectLexicons, loadPlugins } from "../cli/plugins";
|
|
9
9
|
import { build } from "../build";
|
|
10
10
|
import { runPostSynthChecks, isPostSynthCheck } from "./post-synth";
|
|
@@ -55,10 +55,10 @@ export async function evaluateProjectPolicies(opts: {
|
|
|
55
55
|
const plugins = await loadPlugins(lexiconNames);
|
|
56
56
|
const serializers = plugins.map((p) => p.serializer);
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
58
|
+
// chant #1117 — walks up from the build dir to the project root, same as
|
|
59
|
+
// `chant build` (`../cli/commands/build.ts`'s `loadChantConfigUpward`), not
|
|
60
|
+
// just the build dir's immediate parent.
|
|
61
|
+
const loaded = await loadChantConfigUpward(buildPath);
|
|
62
62
|
const config = loaded.config;
|
|
63
63
|
const configDir = loaded.configPath ? dirname(loaded.configPath) : buildPath;
|
|
64
64
|
const env = opts.env ?? config.ownership?.env;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { findProjectConfig, findProjectRoot } from "./project-root";
|
|
6
|
+
|
|
7
|
+
// Every test builds its own isolated directory tree under a fresh tmpdir —
|
|
8
|
+
// never reused across tests, and never anywhere near the real repo's own
|
|
9
|
+
// .git/package.json — so the boundary-stop behavior is exercised
|
|
10
|
+
// deterministically instead of depending on where this file happens to sit
|
|
11
|
+
// in the real chant checkout.
|
|
12
|
+
let root: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
root = mkdtempSync(join(tmpdir(), "chant-project-root-"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(root, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("findProjectConfig / findProjectRoot (chant #1117)", () => {
|
|
23
|
+
test("found at root — chant.config.ts in the start dir itself", () => {
|
|
24
|
+
writeFileSync(join(root, "chant.config.ts"), "export default {};");
|
|
25
|
+
|
|
26
|
+
const result = findProjectConfig(root);
|
|
27
|
+
|
|
28
|
+
expect(result.dir).toBe(root);
|
|
29
|
+
expect(result.configPath).toBe(join(root, "chant.config.ts"));
|
|
30
|
+
expect(findProjectRoot(root)).toBe(root);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("found at an intermediate ancestor — several levels below the config", () => {
|
|
34
|
+
writeFileSync(join(root, "chant.config.json"), "{}");
|
|
35
|
+
const stackDir = join(root, "src", "stacks", "shared-foundation");
|
|
36
|
+
mkdirSync(stackDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const result = findProjectConfig(stackDir);
|
|
39
|
+
|
|
40
|
+
expect(result.dir).toBe(root);
|
|
41
|
+
expect(result.configPath).toBe(join(root, "chant.config.json"));
|
|
42
|
+
expect(findProjectRoot(stackDir)).toBe(root);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("not found — no chant.config, no .git, no package.json anywhere above start dir: falls back to start dir, not the filesystem root", () => {
|
|
46
|
+
const deep = join(root, "a", "b", "c");
|
|
47
|
+
mkdirSync(deep, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const result = findProjectConfig(deep);
|
|
50
|
+
|
|
51
|
+
// Nothing between `deep` and the real filesystem root declares a chant
|
|
52
|
+
// config or a boundary marker (this mkdtemp tree carries none). Rather
|
|
53
|
+
// than adopting "/" as the project root — which would make a downstream
|
|
54
|
+
// caller that scopes a directory walk off this result (e.g.
|
|
55
|
+
// `resolveProjectLexicons` -> `findInfraFiles`) scan the entire disk —
|
|
56
|
+
// the walk gives up and returns `deep` itself unchanged.
|
|
57
|
+
expect(result.configPath).toBeUndefined();
|
|
58
|
+
expect(result.dir).toBe(deep);
|
|
59
|
+
expect(findProjectRoot(deep)).toBe(deep);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("boundary stop — a .git/package.json ancestor halts the walk before an outer chant.config.ts", () => {
|
|
63
|
+
// An unrelated chant.config.ts two levels above this project's own git
|
|
64
|
+
// root must never be picked up — the boundary marker (.git here) stops
|
|
65
|
+
// the walk at the project's own root first.
|
|
66
|
+
writeFileSync(join(root, "chant.config.ts"), "export default { unrelated: true };");
|
|
67
|
+
const projectRoot = join(root, "project");
|
|
68
|
+
mkdirSync(join(projectRoot, ".git"), { recursive: true });
|
|
69
|
+
const stackDir = join(projectRoot, "src", "stack");
|
|
70
|
+
mkdirSync(stackDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
const result = findProjectConfig(stackDir);
|
|
73
|
+
|
|
74
|
+
expect(result.dir).toBe(projectRoot);
|
|
75
|
+
expect(result.configPath).toBeUndefined();
|
|
76
|
+
expect(findProjectRoot(stackDir)).toBe(projectRoot);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("boundary stop — package.json (no .git) halts the walk the same way", () => {
|
|
80
|
+
writeFileSync(join(root, "chant.config.json"), "{}");
|
|
81
|
+
const projectRoot = join(root, "project");
|
|
82
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
83
|
+
writeFileSync(join(projectRoot, "package.json"), "{}");
|
|
84
|
+
const stackDir = join(projectRoot, "src", "stack");
|
|
85
|
+
mkdirSync(stackDir, { recursive: true });
|
|
86
|
+
|
|
87
|
+
const result = findProjectConfig(stackDir);
|
|
88
|
+
|
|
89
|
+
expect(result.dir).toBe(projectRoot);
|
|
90
|
+
expect(result.configPath).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a config living exactly at the boundary dir is still found — boundary check never wins over a real config", () => {
|
|
94
|
+
const projectRoot = join(root, "project");
|
|
95
|
+
mkdirSync(join(projectRoot, ".git"), { recursive: true });
|
|
96
|
+
writeFileSync(join(projectRoot, "chant.config.ts"), "export default {};");
|
|
97
|
+
const stackDir = join(projectRoot, "src", "stack");
|
|
98
|
+
mkdirSync(stackDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const result = findProjectConfig(stackDir);
|
|
101
|
+
|
|
102
|
+
expect(result.dir).toBe(projectRoot);
|
|
103
|
+
expect(result.configPath).toBe(join(projectRoot, "chant.config.ts"));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { dirname, join, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared upward config-discovery walk (chant #1117).
|
|
6
|
+
*
|
|
7
|
+
* Before this, `chant build <subdir>` and `lint.policies`
|
|
8
|
+
* (`./lint/policy.ts`'s `evaluateProjectPolicies`) each searched only the
|
|
9
|
+
* build directory and its immediate parent for `chant.config.ts`/`.json`,
|
|
10
|
+
* while `chant lint`/`chant graph` (`./lint/config.ts`'s old, file-local
|
|
11
|
+
* `findProjectRoot`) already walked all the way up. A project with a deeper
|
|
12
|
+
* `src/<stack>` layout — `chant build src/<stack>` two or more levels below
|
|
13
|
+
* the project root — silently never found the root config: `buildParams`'
|
|
14
|
+
* declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
|
|
15
|
+
* fell back to defaults, and nothing warned (loomster#162: `LOOM_TIER`/
|
|
16
|
+
* `LOOM_ENV` inert under every `npm run synth:*` for two releases).
|
|
17
|
+
*
|
|
18
|
+
* `findProjectConfig` is the one walk every config-discovery call site now
|
|
19
|
+
* shares. It stops at the first of:
|
|
20
|
+
*
|
|
21
|
+
* 1. A directory holding `chant.config.ts` or `chant.config.json` — found.
|
|
22
|
+
* 2. A directory holding `.git` or `package.json` with no chant config of its
|
|
23
|
+
* own — the project boundary. Discovery must never wander past the actual
|
|
24
|
+
* project into an unrelated ancestor directory just because this project
|
|
25
|
+
* happens not to declare a config (a stray `chant.config.ts` two levels
|
|
26
|
+
* above an unrelated git repo must never be picked up).
|
|
27
|
+
* 3. `startDir` itself, unchanged, if the walk reaches the filesystem root
|
|
28
|
+
* without ever finding a config OR a boundary marker. This is not just a
|
|
29
|
+
* "give up gracefully" nicety — several callers (`resolveProjectLexicons`
|
|
30
|
+
* -> `findInfraFiles`) scope a real directory walk off this function's
|
|
31
|
+
* result; if a rootless/marker-less start dir (a bare tmpdir, as chant's
|
|
32
|
+
* own test suites use) resolved all the way to `/`, that downstream walk
|
|
33
|
+
* would scan the entire filesystem instead of failing fast. Falling back
|
|
34
|
+
* to `startDir` keeps every caller's blast radius local no matter how far
|
|
35
|
+
* up the walk had to look.
|
|
36
|
+
*/
|
|
37
|
+
export interface ProjectConfigSearch {
|
|
38
|
+
/** The resolved project root: the config's directory, the boundary directory, or the (resolved) `startDir` when neither was found. */
|
|
39
|
+
dir: string;
|
|
40
|
+
/** Absolute path to the discovered `chant.config.ts`/`.json`, if any. */
|
|
41
|
+
configPath?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Walk up from `startDir` (inclusive) to the nearest chant config or project boundary. See {@link ProjectConfigSearch}. */
|
|
45
|
+
export function findProjectConfig(startDir: string): ProjectConfigSearch {
|
|
46
|
+
const resolvedStart = resolve(startDir);
|
|
47
|
+
let dir = resolvedStart;
|
|
48
|
+
for (;;) {
|
|
49
|
+
const tsPath = join(dir, "chant.config.ts");
|
|
50
|
+
if (existsSync(tsPath)) return { dir, configPath: tsPath };
|
|
51
|
+
const jsonPath = join(dir, "chant.config.json");
|
|
52
|
+
if (existsSync(jsonPath)) return { dir, configPath: jsonPath };
|
|
53
|
+
|
|
54
|
+
if (existsSync(join(dir, ".git")) || existsSync(join(dir, "package.json"))) {
|
|
55
|
+
return { dir };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parent = dirname(dir);
|
|
59
|
+
if (parent === dir) {
|
|
60
|
+
// Filesystem root, no config, no boundary ever seen — give up rather
|
|
61
|
+
// than adopting "/" as the project root (see the module doc above).
|
|
62
|
+
return { dir: resolvedStart };
|
|
63
|
+
}
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Walk up from `startDir` to the nearest ancestor holding a chant project
|
|
70
|
+
* config (`chant.config.ts` or `chant.config.json`), the `.git`/`package.json`
|
|
71
|
+
* project boundary, or `startDir` itself when neither is found. Thin wrapper
|
|
72
|
+
* over {@link findProjectConfig} for callers that only need the directory
|
|
73
|
+
* (e.g. `chant lint`'s rule/plugin resolution, which just needs *a* stable
|
|
74
|
+
* root to resolve relative paths against).
|
|
75
|
+
*/
|
|
76
|
+
export function findProjectRoot(startDir: string): string {
|
|
77
|
+
return findProjectConfig(startDir).dir;
|
|
78
|
+
}
|