@intentius/chant 0.54.2 → 0.54.3
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/lifecycle/status.d.ts +15 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/op/discover.d.ts +4 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/terraform/manifest.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/carve-apply.test.ts +31 -1
- package/src/lifecycle/status.test.ts +38 -0
- package/src/lifecycle/status.ts +17 -2
- package/src/op/discover-root.test.ts +68 -0
- package/src/op/discover.ts +33 -4
- package/src/terraform/manifest.ts +12 -4
|
@@ -275,8 +275,22 @@ export interface CrossEnvComparison {
|
|
|
275
275
|
envB: string;
|
|
276
276
|
digestA?: string;
|
|
277
277
|
digestB?: string;
|
|
278
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* True only when both envs have a recorded digest and their *comparable*
|
|
280
|
+
* identities match. For a record carrying `inputDigest` (a pinned helm
|
|
281
|
+
* deploy, chant#1242/#1243), that field is the comparable identity —
|
|
282
|
+
* `ReleaseRecord.inputDigest`'s own contract: profiles are per cluster, two
|
|
283
|
+
* environments legitimately render to different bytes, so cross-environment
|
|
284
|
+
* "is prod running what staging tested" joins on the input side while
|
|
285
|
+
* `digest` proves the exact bytes each cluster got. Records without it
|
|
286
|
+
* compare on `digest` exactly as before.
|
|
287
|
+
*/
|
|
279
288
|
same: boolean;
|
|
289
|
+
/** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
|
|
290
|
+
comparedOn?: {
|
|
291
|
+
a: string;
|
|
292
|
+
b: string;
|
|
293
|
+
};
|
|
280
294
|
}
|
|
281
295
|
/** Compare the latest recorded digest for `component` between two environments' release records. */
|
|
282
296
|
export declare function compareAcrossEnvironments(component: string, envA: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
|
package/dist/op/discover.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export interface OpDiscoveryResult {
|
|
|
8
8
|
errors: string[];
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Discover all Op definitions from
|
|
11
|
+
* Discover all Op definitions from `*.op.ts` files under the nearest chant
|
|
12
|
+
* project root (the directory holding `chant.config.ts`/`.json`, walking up
|
|
13
|
+
* from `cwd`), or under the git root when no config exists — see
|
|
14
|
+
* {@link findDiscoveryRoot} (#2058).
|
|
12
15
|
*/
|
|
13
16
|
export declare function discoverOps(opts?: {
|
|
14
17
|
cwd?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/op/discover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/op/discover.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AA0DD;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuCrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/terraform/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;IACf,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,EAAE,WAAW,CAAC;IACtB,gCAAgC;IAChC,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;QAC3B;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CACb,MAAM,EACN;YACE,8EAA8E;YAC9E,MAAM,EAAE,MAAM,CAAC;YACf,mEAAmE;YACnE,QAAQ,EAAE,MAAM,CAAC;YACjB,kCAAkC;YAClC,KAAK,EAAE,MAAM,EAAE,CAAC;YAChB,2CAA2C;YAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;SACrC,CACF,CAAC;QACF,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;IACF,kCAAkC;IAClC,MAAM,CAAC,EAAE;QACP,6FAA6F;QAC7F,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,cAAc,EAAE,OAAO,CAAC;QACxB,2IAA2I;QAC3I,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,yEAAyE;QACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;IACF,iCAAiC;IACjC,KAAK,CAAC,EAAE;QACN,MAAM,EAAE,eAAe,CAAC;QACxB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,4IAA4I;QAC5I,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;CACH;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAExE;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/terraform/manifest.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { carveApply, formatCarveApply } from "./carve-apply";
|
|
@@ -108,6 +108,36 @@ describe("carveApply", () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test("a RELATIVE --output still records stampedFiles relative to the manifest's directory, not the cwd (#2059)", async () => {
|
|
112
|
+
if (!parserAvailable) return;
|
|
113
|
+
await withEstate(async (dir) => {
|
|
114
|
+
// The behold carve-demo shape: run FROM the project dir with
|
|
115
|
+
// `--output carveout`. The bug: resolveManifestFilePath joined against
|
|
116
|
+
// the relative outDir, producing a cwd-relative path ("carveout/…")
|
|
117
|
+
// that normalize-on-write passed through — two spellings in one manifest.
|
|
118
|
+
const out = join(dir, "carveout");
|
|
119
|
+
const manifest = manifestFor("aws_s3_bucket.assets", dir);
|
|
120
|
+
manifest.emit = { source: "tfstate", files: [join("src", "assets.ts")], at: "t" };
|
|
121
|
+
writeCarveManifest(out, manifest);
|
|
122
|
+
mkdirSync(join(out, "src"), { recursive: true });
|
|
123
|
+
writeFileSync(join(out, "src", "assets.ts"), 'export const assets = new Bucket({\n BucketName: "b",\n});\n');
|
|
124
|
+
|
|
125
|
+
const prevCwd = process.cwd();
|
|
126
|
+
process.chdir(dir);
|
|
127
|
+
try {
|
|
128
|
+
const res = await carveApply({ from: ".", output: "carveout", env: "prod", stack: "assets", writeSource: true });
|
|
129
|
+
expect(res.ok).toBe(true);
|
|
130
|
+
const m = readCarveManifest(res.manifestPath!)!;
|
|
131
|
+
// One spelling: manifest-dir-relative, same as emit.files — never
|
|
132
|
+
// "carveout/src/assets.ts" (relative to wherever the command ran).
|
|
133
|
+
expect(m.apply!.stampedFiles).toEqual([join("src", "assets.ts")]);
|
|
134
|
+
expect(m.emit!.files).toEqual([join("src", "assets.ts")]);
|
|
135
|
+
} finally {
|
|
136
|
+
process.chdir(prevCwd);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
111
141
|
test("--write-source without an emit record in the manifest is a clear error", async () => {
|
|
112
142
|
if (!parserAvailable) return;
|
|
113
143
|
await withEstate(async (dir) => {
|
|
@@ -413,6 +413,44 @@ describe("status", () => {
|
|
|
413
413
|
expect(result.digestB).toBe("sha256:abc");
|
|
414
414
|
expect(result.same).toBe(false);
|
|
415
415
|
});
|
|
416
|
+
|
|
417
|
+
test("pinned helm records compare on inputDigest — per-cluster bytes legitimately differ (ReleaseRecord.inputDigest's own contract)", () => {
|
|
418
|
+
// Same chart+values rendered against two clusters' capability profiles:
|
|
419
|
+
// contentDigests (recorded as `digest`) differ, inputDigests match —
|
|
420
|
+
// prod IS running what staging tested.
|
|
421
|
+
const result = compareAcrossEnvironments(
|
|
422
|
+
"web",
|
|
423
|
+
{ name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:staging-bytes", inputDigest: "sha256:inputs" })] },
|
|
424
|
+
{ name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:prod-bytes", inputDigest: "sha256:inputs" })] },
|
|
425
|
+
);
|
|
426
|
+
expect(result.same).toBe(true);
|
|
427
|
+
expect(result.comparedOn).toEqual({ a: "sha256:inputs", b: "sha256:inputs" });
|
|
428
|
+
// The exact bytes each cluster got stay visible.
|
|
429
|
+
expect(result.digestA).toBe("sha256:staging-bytes");
|
|
430
|
+
expect(result.digestB).toBe("sha256:prod-bytes");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("differing inputDigests are a real difference, whatever the bytes say", () => {
|
|
434
|
+
const result = compareAcrossEnvironments(
|
|
435
|
+
"web",
|
|
436
|
+
{ name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:same-bytes", inputDigest: "sha256:inputs-v2" })] },
|
|
437
|
+
{ name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:same-bytes", inputDigest: "sha256:inputs-v1" })] },
|
|
438
|
+
);
|
|
439
|
+
expect(result.same).toBe(false);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("one side input-identified, the other content-identified: compared honestly on what each carries", () => {
|
|
443
|
+
// Mixed deploys (a pinned helm release in prod, an unpinned one in
|
|
444
|
+
// staging where the record's digest IS the input digest): staging's
|
|
445
|
+
// digest against prod's inputDigest is the only meaningful join.
|
|
446
|
+
const result = compareAcrossEnvironments(
|
|
447
|
+
"web",
|
|
448
|
+
{ name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:inputs" })] },
|
|
449
|
+
{ name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:prod-bytes", inputDigest: "sha256:inputs" })] },
|
|
450
|
+
);
|
|
451
|
+
expect(result.same).toBe(true);
|
|
452
|
+
expect(result.comparedOn).toEqual({ a: "sha256:inputs", b: "sha256:inputs" });
|
|
453
|
+
});
|
|
416
454
|
});
|
|
417
455
|
|
|
418
456
|
describe("mergeLiveEvidence (#57 — per-component stack presence overlay)", () => {
|
package/src/lifecycle/status.ts
CHANGED
|
@@ -478,8 +478,19 @@ export interface CrossEnvComparison {
|
|
|
478
478
|
envB: string;
|
|
479
479
|
digestA?: string;
|
|
480
480
|
digestB?: string;
|
|
481
|
-
/**
|
|
481
|
+
/**
|
|
482
|
+
* True only when both envs have a recorded digest and their *comparable*
|
|
483
|
+
* identities match. For a record carrying `inputDigest` (a pinned helm
|
|
484
|
+
* deploy, chant#1242/#1243), that field is the comparable identity —
|
|
485
|
+
* `ReleaseRecord.inputDigest`'s own contract: profiles are per cluster, two
|
|
486
|
+
* environments legitimately render to different bytes, so cross-environment
|
|
487
|
+
* "is prod running what staging tested" joins on the input side while
|
|
488
|
+
* `digest` proves the exact bytes each cluster got. Records without it
|
|
489
|
+
* compare on `digest` exactly as before.
|
|
490
|
+
*/
|
|
482
491
|
same: boolean;
|
|
492
|
+
/** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
|
|
493
|
+
comparedOn?: { a: string; b: string };
|
|
483
494
|
}
|
|
484
495
|
|
|
485
496
|
/** Compare the latest recorded digest for `component` between two environments' release records. */
|
|
@@ -490,12 +501,16 @@ export function compareAcrossEnvironments(
|
|
|
490
501
|
): CrossEnvComparison {
|
|
491
502
|
const latestA = latestPerComponent(envA.records).get(component);
|
|
492
503
|
const latestB = latestPerComponent(envB.records).get(component);
|
|
504
|
+
const idA = latestA ? latestA.inputDigest ?? latestA.digest : undefined;
|
|
505
|
+
const idB = latestB ? latestB.inputDigest ?? latestB.digest : undefined;
|
|
506
|
+
const comparedOnInputs = (latestA?.inputDigest ?? latestB?.inputDigest) !== undefined;
|
|
493
507
|
return {
|
|
494
508
|
component,
|
|
495
509
|
envA: envA.name,
|
|
496
510
|
envB: envB.name,
|
|
497
511
|
digestA: latestA?.digest,
|
|
498
512
|
digestB: latestB?.digest,
|
|
499
|
-
same: !!
|
|
513
|
+
same: !!idA && !!idB && idA === idB,
|
|
514
|
+
...(comparedOnInputs && idA && idB ? { comparedOn: { a: idA, b: idB } } : {}),
|
|
500
515
|
};
|
|
501
516
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { discoverOps } from "./discover";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* #2058 — Op discovery scoped to the project root, the way entity discovery
|
|
9
|
+
* is. The repro: a chant project nested in a larger checkout (behold's
|
|
10
|
+
* committed `example-carve/app` beside `example-writes`, `example-k8s`)
|
|
11
|
+
* discovered every sibling project's `*.op.ts` as its own, because the scan
|
|
12
|
+
* root was the git root unconditionally.
|
|
13
|
+
*
|
|
14
|
+
* The fake git root is a temp tree; the runtime mock answers `git rev-parse`
|
|
15
|
+
* with whatever the current test set (`realpath`ed, since macOS's tmpdir is a
|
|
16
|
+
* symlink and the walk compares resolved paths).
|
|
17
|
+
*/
|
|
18
|
+
let fakeGitRoot = "";
|
|
19
|
+
vi.mock("../runtime-adapter", () => ({
|
|
20
|
+
getRuntime: () => ({
|
|
21
|
+
spawn: async (cmd: string[]) =>
|
|
22
|
+
cmd[0] === "git" && cmd[1] === "rev-parse"
|
|
23
|
+
? { stdout: fakeGitRoot, stderr: "", exitCode: 0 }
|
|
24
|
+
: { stdout: "", stderr: "", exitCode: 0 },
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/** A minimal default export shaped the way discovery validates (entity.props = OpConfig). */
|
|
29
|
+
const OP_FILE = (name: string): string =>
|
|
30
|
+
`export default { props: { name: ${JSON.stringify(name)}, overview: "t", phases: [{ name: "Run", steps: [] }] } };\n`;
|
|
31
|
+
|
|
32
|
+
describe("discoverOps — the scan root is the project, not the checkout (#2058)", () => {
|
|
33
|
+
let checkout: string;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
checkout = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-root-")));
|
|
36
|
+
fakeGitRoot = checkout;
|
|
37
|
+
// The monorepo shape from the issue: two sibling chant projects, each
|
|
38
|
+
// with its own config and ops, nested in one git checkout.
|
|
39
|
+
mkdirSync(join(checkout, "app", "ops"), { recursive: true });
|
|
40
|
+
writeFileSync(join(checkout, "app", "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
41
|
+
writeFileSync(join(checkout, "app", "ops", "mine.op.ts"), OP_FILE("mine"));
|
|
42
|
+
mkdirSync(join(checkout, "sibling", "ops"), { recursive: true });
|
|
43
|
+
writeFileSync(join(checkout, "sibling", "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
44
|
+
writeFileSync(join(checkout, "sibling", "ops", "theirs.op.ts"), OP_FILE("theirs"));
|
|
45
|
+
// A configless dir at the checkout root, with an op of its own.
|
|
46
|
+
mkdirSync(join(checkout, "ops"), { recursive: true });
|
|
47
|
+
writeFileSync(join(checkout, "ops", "root.op.ts"), OP_FILE("root"));
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => rmSync(checkout, { recursive: true, force: true }));
|
|
50
|
+
|
|
51
|
+
test("from inside a project, only that project's Ops are discovered — never a sibling's", async () => {
|
|
52
|
+
const { ops, errors } = await discoverOps({ cwd: join(checkout, "app") });
|
|
53
|
+
expect(errors).toEqual([]);
|
|
54
|
+
expect([...ops.keys()]).toEqual(["mine"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the walk finds the config from a subdirectory of the project too (ops/ beside src/, #1675)", async () => {
|
|
58
|
+
const { ops } = await discoverOps({ cwd: join(checkout, "app", "ops") });
|
|
59
|
+
expect([...ops.keys()]).toEqual(["mine"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("with no chant config anywhere up to the git root, the git root stands — #1675's original scope", async () => {
|
|
63
|
+
rmSync(join(checkout, "app", "chant.config.json"));
|
|
64
|
+
rmSync(join(checkout, "sibling", "chant.config.json"));
|
|
65
|
+
const { ops } = await discoverOps({ cwd: join(checkout, "app") });
|
|
66
|
+
expect([...ops.keys()].sort()).toEqual(["mine", "root", "theirs"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/op/discover.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRuntime } from "../runtime-adapter";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import type { OpConfig } from "./types";
|
|
5
6
|
|
|
6
7
|
export interface DiscoveredOp {
|
|
@@ -20,6 +21,31 @@ async function findGitRoot(cwd?: string): Promise<string> {
|
|
|
20
21
|
return result.stdout.trim();
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The root the `*.op.ts` scan starts from (#2058): the nearest chant project
|
|
26
|
+
* root — the directory holding `chant.config.ts`/`chant.config.json`, walking
|
|
27
|
+
* up from `cwd` and never past the git root. #1675's convention keeps Op
|
|
28
|
+
* files OUTSIDE `sourceDir` (`ops/` beside `src/`), which is why discovery
|
|
29
|
+
* walks up at all — but "up" used to mean the git root unconditionally, so a
|
|
30
|
+
* chant project nested in a larger checkout (a monorepo, behold's committed
|
|
31
|
+
* examples) discovered every SIBLING project's Ops as its own. The config
|
|
32
|
+
* file is the project boundary, the same one entity discovery respects.
|
|
33
|
+
* With no config anywhere up to the git root, the git root stands —
|
|
34
|
+
* #1675's original scope, kept for configless layouts.
|
|
35
|
+
*/
|
|
36
|
+
async function findDiscoveryRoot(cwd?: string): Promise<string> {
|
|
37
|
+
const gitRoot = resolve(await findGitRoot(cwd));
|
|
38
|
+
let dir = resolve(cwd ?? process.cwd());
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) return dir;
|
|
41
|
+
if (dir === gitRoot) break;
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) break; // filesystem root — cwd was outside the git root
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
return gitRoot;
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
async function collectOpFiles(dir: string): Promise<string[]> {
|
|
24
50
|
const files: string[] = [];
|
|
25
51
|
let entries;
|
|
@@ -45,14 +71,17 @@ async function collectOpFiles(dir: string): Promise<string[]> {
|
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
/**
|
|
48
|
-
* Discover all Op definitions from
|
|
74
|
+
* Discover all Op definitions from `*.op.ts` files under the nearest chant
|
|
75
|
+
* project root (the directory holding `chant.config.ts`/`.json`, walking up
|
|
76
|
+
* from `cwd`), or under the git root when no config exists — see
|
|
77
|
+
* {@link findDiscoveryRoot} (#2058).
|
|
49
78
|
*/
|
|
50
79
|
export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryResult> {
|
|
51
80
|
const errors: string[] = [];
|
|
52
81
|
const ops = new Map<string, DiscoveredOp>();
|
|
53
82
|
|
|
54
|
-
const
|
|
55
|
-
const files = await collectOpFiles(
|
|
83
|
+
const root = await findDiscoveryRoot(opts?.cwd);
|
|
84
|
+
const files = await collectOpFiles(root);
|
|
56
85
|
|
|
57
86
|
const nameToFile = new Map<string, string>();
|
|
58
87
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
12
|
-
import { isAbsolute, join, relative } from "path";
|
|
12
|
+
import { isAbsolute, join, relative, resolve } from "path";
|
|
13
13
|
import type { CarveReport } from "./carve";
|
|
14
14
|
import type { OwnershipMarker } from "../ownership";
|
|
15
15
|
|
|
@@ -100,10 +100,14 @@ export function carveManifestPath(outDir: string, target: string): string {
|
|
|
100
100
|
/**
|
|
101
101
|
* Relative-to-`outDir` spelling of a recorded file path. Absolute entries
|
|
102
102
|
* (the run-time spelling every step naturally produces, and every version-1
|
|
103
|
-
* manifest on disk) are rewritten
|
|
103
|
+
* manifest on disk) are rewritten against the RESOLVED outDir — a caller's
|
|
104
|
+
* relative `--output` must not change what "relative to the manifest's
|
|
105
|
+
* directory" means (#2059). Already-relative entries pass through: they can
|
|
106
|
+
* only have come from a reader honouring the contract, and re-basing them
|
|
107
|
+
* against an unknowable original cwd would corrupt, not normalize.
|
|
104
108
|
*/
|
|
105
109
|
function relativizePath(outDir: string, p: string): string {
|
|
106
|
-
return isAbsolute(p) ? relative(outDir, p) : p;
|
|
110
|
+
return isAbsolute(p) ? relative(resolve(outDir), p) : p;
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
/**
|
|
@@ -136,7 +140,11 @@ function normalizeManifest(outDir: string, manifest: CarveManifest): CarveManife
|
|
|
136
140
|
* on-disk spellings read the same.
|
|
137
141
|
*/
|
|
138
142
|
export function resolveManifestFilePath(manifestDir: string, p: string): string {
|
|
139
|
-
|
|
143
|
+
// Always ABSOLUTE (#2059): `join` with a relative manifestDir produced a
|
|
144
|
+
// cwd-relative path, which `relativizePath` then passed through untouched —
|
|
145
|
+
// so `carve apply --output carveout` recorded `stampedFiles` relative to
|
|
146
|
+
// wherever the command happened to run, two spellings in one manifest.
|
|
147
|
+
return isAbsolute(p) ? p : resolve(manifestDir, p);
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
/** Write (or overwrite) a manifest into the output dir. Returns the path. */
|