@intentius/chant 0.18.27 → 0.18.29
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/handlers/components.d.ts.map +1 -1
- package/dist/cli/main.d.ts +10 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +25 -0
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.ts +13 -1
- package/src/cli/main.test.ts +49 -1
- package/src/cli/main.ts +44 -3
- package/src/lifecycle/status.test.ts +23 -0
- package/src/lifecycle/status.ts +38 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/components.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuErF;
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/components.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuErF;AAqHD,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuN9E;AAED,kEAAkE;AAClE,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM/E"}
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -4,4 +4,14 @@ import { type ParsedArgs } from "./registry.js";
|
|
|
4
4
|
* Parse command line arguments
|
|
5
5
|
*/
|
|
6
6
|
export declare function parseArgs(args: string[]): ParsedArgs;
|
|
7
|
+
/**
|
|
8
|
+
* Wait until a writable stream has flushed its buffer. `process.exit()` discards
|
|
9
|
+
* data still buffered for an async sink (a pipe or file), truncating large output
|
|
10
|
+
* at the ~64 KB pipe buffer — so `chant graph --format ir` piped into a consumer
|
|
11
|
+
* loses everything past 64 KB and its JSON won't parse. A TTY writes
|
|
12
|
+
* synchronously (`writableLength` stays 0), so this is a no-op there. Resolves on
|
|
13
|
+
* `error`/`close` too, so a reader that closes early (EPIPE) can't hang exit.
|
|
14
|
+
* Exported for testing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function waitForStreamDrain(stream: NodeJS.WriteStream): Promise<void>;
|
|
7
17
|
//# sourceMappingURL=main.d.ts.map
|
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;AAkB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA2LpD"}
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAkB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA2LpD;AAsVD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
|
|
@@ -58,6 +58,28 @@ export interface ComponentStatusRow {
|
|
|
58
58
|
reconciliation: "reconciled" | "unrecorded" | "stale" | "drifted" | "unknown";
|
|
59
59
|
/** Human-readable detail backing the verdict. */
|
|
60
60
|
detail: string;
|
|
61
|
+
/**
|
|
62
|
+
* Machine-readable "observed live", when live evidence was gathered (`--live`).
|
|
63
|
+
* A consumer joining this row onto a graph node should read this rather than
|
|
64
|
+
* string-matching `detail`. Absent when `--live` was not requested.
|
|
65
|
+
*/
|
|
66
|
+
live?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The owning deploy unit's raw status, when a lexicon reported it (AWS: the
|
|
69
|
+
* component's own CFN stack via `describeStackStatus`). Lets a renderer paint a
|
|
70
|
+
* richer palette than the reconciliation verdict — `healthy` green,
|
|
71
|
+
* present-but-not-healthy amber (mid-deploy) / red (rollback/failed).
|
|
72
|
+
*/
|
|
73
|
+
stack?: LiveStackInfo;
|
|
74
|
+
}
|
|
75
|
+
/** A component's owning deploy unit and its provider-native status. */
|
|
76
|
+
export interface LiveStackInfo {
|
|
77
|
+
/** The deploy-unit name (e.g. the CloudFormation stack name). */
|
|
78
|
+
name: string;
|
|
79
|
+
/** Provider-native status string, e.g. "CREATE_COMPLETE". */
|
|
80
|
+
status?: string;
|
|
81
|
+
/** True when `status` is a terminal success state. */
|
|
82
|
+
healthy?: boolean;
|
|
61
83
|
}
|
|
62
84
|
export interface ComponentStatusResult {
|
|
63
85
|
env: string;
|
|
@@ -80,6 +102,9 @@ export interface LiveComponentEvidence {
|
|
|
80
102
|
action?: ChangeAction;
|
|
81
103
|
/** Ownership verdict, when known. */
|
|
82
104
|
ownership?: "owned" | "foreign" | "unknown";
|
|
105
|
+
/** The owning deploy unit's raw status, when observed (AWS: the component's own
|
|
106
|
+
* CFN stack). Surfaced onto `ComponentStatusRow.stack` for a richer palette. */
|
|
107
|
+
stack?: LiveStackInfo;
|
|
83
108
|
}
|
|
84
109
|
/**
|
|
85
110
|
* Overlay per-component stack-presence evidence onto change-set evidence.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE5E,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,+IAA+I;IAC/I,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,gDAAgD;IAChD,cAAc,EAAE,YAAY,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9E,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -172,6 +172,8 @@ interface StatusJsonRow {
|
|
|
172
172
|
} | null;
|
|
173
173
|
reconciliation: string;
|
|
174
174
|
detail: string;
|
|
175
|
+
live?: boolean;
|
|
176
|
+
stack?: { name: string; status?: string; healthy?: boolean };
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
/**
|
|
@@ -237,7 +239,15 @@ async function observeComponentStacks(
|
|
|
237
239
|
const determinate = observed.filter((o): o is NonNullable<typeof o> => o !== null);
|
|
238
240
|
if (determinate.length === 0) continue;
|
|
239
241
|
const present = determinate.every((o) => o.present);
|
|
240
|
-
|
|
242
|
+
// Surface the (first present, else first) stack's raw status for a richer
|
|
243
|
+
// palette than the reconciliation verdict. One cfn-deploy stack per component
|
|
244
|
+
// is the norm; a multi-stack component reports its representative unit.
|
|
245
|
+
const repr = determinate.find((o) => o.present) ?? determinate[0];
|
|
246
|
+
evidence.set(name, {
|
|
247
|
+
live: present,
|
|
248
|
+
ownership: present ? "owned" : undefined,
|
|
249
|
+
stack: { name: repr.stack, status: repr.status, healthy: repr.healthy },
|
|
250
|
+
});
|
|
241
251
|
}
|
|
242
252
|
return evidence;
|
|
243
253
|
}
|
|
@@ -396,6 +406,8 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
396
406
|
: null,
|
|
397
407
|
reconciliation: row.reconciliation,
|
|
398
408
|
detail: row.detail,
|
|
409
|
+
...(row.live !== undefined ? { live: row.live } : {}),
|
|
410
|
+
...(row.stack ? { stack: row.stack } : {}),
|
|
399
411
|
});
|
|
400
412
|
}
|
|
401
413
|
}
|
package/src/cli/main.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { parseArgs, waitForStreamDrain } from "./main";
|
|
3
4
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
4
5
|
|
|
5
6
|
describe("parseArgs", () => {
|
|
@@ -416,3 +417,50 @@ describe("parseArgs — run flags", () => {
|
|
|
416
417
|
expect(result.extraPositional2).toBe("gate-dns");
|
|
417
418
|
});
|
|
418
419
|
});
|
|
420
|
+
|
|
421
|
+
describe("waitForStreamDrain", () => {
|
|
422
|
+
// Minimal writable stub — just the surface waitForStreamDrain reads.
|
|
423
|
+
function fakeStream(len: number): NodeJS.WriteStream & { writableLength: number } {
|
|
424
|
+
const s = new EventEmitter() as unknown as NodeJS.WriteStream & { writableLength: number };
|
|
425
|
+
s.writableLength = len;
|
|
426
|
+
(s as { writableEnded: boolean }).writableEnded = false;
|
|
427
|
+
(s as { destroyed: boolean }).destroyed = false;
|
|
428
|
+
return s;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
test("resolves immediately when nothing is buffered (e.g. a TTY)", async () => {
|
|
432
|
+
await expect(waitForStreamDrain(fakeStream(0))).resolves.toBeUndefined();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("waits through drains until the buffer is actually empty", async () => {
|
|
436
|
+
const s = fakeStream(1000);
|
|
437
|
+
let done = false;
|
|
438
|
+
const p = waitForStreamDrain(s).then(() => (done = true));
|
|
439
|
+
await Promise.resolve();
|
|
440
|
+
expect(done).toBe(false);
|
|
441
|
+
// A drain while still buffered must NOT resolve (large one-shot write, kernel
|
|
442
|
+
// took a slice, more remains) — it re-arms.
|
|
443
|
+
s.emit("drain");
|
|
444
|
+
await Promise.resolve();
|
|
445
|
+
expect(done).toBe(false);
|
|
446
|
+
// Fully flushed now.
|
|
447
|
+
s.writableLength = 0;
|
|
448
|
+
s.emit("drain");
|
|
449
|
+
await p;
|
|
450
|
+
expect(done).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("resolves on error so a reader that closed early (EPIPE) can't hang exit", async () => {
|
|
454
|
+
const s = fakeStream(500);
|
|
455
|
+
const p = waitForStreamDrain(s);
|
|
456
|
+
s.emit("error", new Error("EPIPE"));
|
|
457
|
+
await expect(p).resolves.toBeUndefined();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("resolves on close as well", async () => {
|
|
461
|
+
const s = fakeStream(500);
|
|
462
|
+
const p = waitForStreamDrain(s);
|
|
463
|
+
s.emit("close");
|
|
464
|
+
await expect(p).resolves.toBeUndefined();
|
|
465
|
+
});
|
|
466
|
+
});
|
package/src/cli/main.ts
CHANGED
|
@@ -551,7 +551,48 @@ async function main(): Promise<void> {
|
|
|
551
551
|
const serializers = plugins.map((p) => p.serializer);
|
|
552
552
|
const ctx = { args, plugins, serializers };
|
|
553
553
|
|
|
554
|
-
|
|
554
|
+
await flushAndExit(await match.def.handler(ctx));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Wait until a writable stream has flushed its buffer. `process.exit()` discards
|
|
559
|
+
* data still buffered for an async sink (a pipe or file), truncating large output
|
|
560
|
+
* at the ~64 KB pipe buffer — so `chant graph --format ir` piped into a consumer
|
|
561
|
+
* loses everything past 64 KB and its JSON won't parse. A TTY writes
|
|
562
|
+
* synchronously (`writableLength` stays 0), so this is a no-op there. Resolves on
|
|
563
|
+
* `error`/`close` too, so a reader that closes early (EPIPE) can't hang exit.
|
|
564
|
+
* Exported for testing.
|
|
565
|
+
*/
|
|
566
|
+
export function waitForStreamDrain(stream: NodeJS.WriteStream): Promise<void> {
|
|
567
|
+
return new Promise((resolve) => {
|
|
568
|
+
const tick = (): void => {
|
|
569
|
+
if (stream.writableLength === 0 || stream.writableEnded || stream.destroyed) {
|
|
570
|
+
cleanup();
|
|
571
|
+
resolve();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
stream.once("drain", tick);
|
|
575
|
+
};
|
|
576
|
+
const stop = (): void => {
|
|
577
|
+
cleanup();
|
|
578
|
+
resolve();
|
|
579
|
+
};
|
|
580
|
+
const cleanup = (): void => {
|
|
581
|
+
stream.off("drain", tick);
|
|
582
|
+
stream.off("error", stop);
|
|
583
|
+
stream.off("close", stop);
|
|
584
|
+
};
|
|
585
|
+
stream.once("error", stop);
|
|
586
|
+
stream.once("close", stop);
|
|
587
|
+
tick();
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Flush stdout+stderr, then exit — so a large piped payload isn't truncated. */
|
|
592
|
+
async function flushAndExit(code: number): Promise<never> {
|
|
593
|
+
await waitForStreamDrain(process.stdout);
|
|
594
|
+
await waitForStreamDrain(process.stderr);
|
|
595
|
+
process.exit(code);
|
|
555
596
|
}
|
|
556
597
|
|
|
557
598
|
// Only run main when executed directly, not when imported. Robust to symlinked
|
|
@@ -559,13 +600,13 @@ async function main(): Promise<void> {
|
|
|
559
600
|
// whole CLI through the npm .bin shim / a symlinked checkout).
|
|
560
601
|
const isMain = isEntryPoint(process.argv[1], import.meta.url);
|
|
561
602
|
if (isMain) {
|
|
562
|
-
main().catch((err) => {
|
|
603
|
+
main().catch(async (err) => {
|
|
563
604
|
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
564
605
|
if (verbose && err instanceof Error && err.stack) {
|
|
565
606
|
console.error(err.stack);
|
|
566
607
|
} else {
|
|
567
608
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
568
609
|
}
|
|
569
|
-
|
|
610
|
+
await flushAndExit(1);
|
|
570
611
|
});
|
|
571
612
|
}
|
|
@@ -135,6 +135,29 @@ describe("status", () => {
|
|
|
135
135
|
expect(rows[0].reconciliation).toBe("drifted");
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("surfaces machine-readable live + stack status when observed (#57 hardening)", () => {
|
|
139
|
+
const liveEvidence = new Map<string, LiveComponentEvidence>([
|
|
140
|
+
["search-service", { live: true, ownership: "owned", stack: { name: "app-prod-search", status: "CREATE_COMPLETE", healthy: true } }],
|
|
141
|
+
]);
|
|
142
|
+
const rows = reconcileStatus("prod", [record()], { liveEvidence });
|
|
143
|
+
expect(rows[0].live).toBe(true);
|
|
144
|
+
expect(rows[0].stack).toEqual({ name: "app-prod-search", status: "CREATE_COMPLETE", healthy: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("live is false (never undefined) under --live when a component's stack is absent", () => {
|
|
148
|
+
const liveEvidence = new Map<string, LiveComponentEvidence>([
|
|
149
|
+
["search-service", { live: false }],
|
|
150
|
+
]);
|
|
151
|
+
const rows = reconcileStatus("prod", [record()], { liveEvidence });
|
|
152
|
+
expect(rows[0].live).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("live is absent (not queried) when no liveEvidence is passed", () => {
|
|
156
|
+
const rows = reconcileStatus("prod", [record()]);
|
|
157
|
+
expect(rows[0].live).toBeUndefined();
|
|
158
|
+
expect(rows[0].stack).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
138
161
|
test("recorded but nothing live -> stale", () => {
|
|
139
162
|
const liveEvidence = new Map<string, LiveComponentEvidence>();
|
|
140
163
|
const rows = reconcileStatus("prod", [record()], { liveEvidence });
|
package/src/lifecycle/status.ts
CHANGED
|
@@ -60,6 +60,29 @@ export interface ComponentStatusRow {
|
|
|
60
60
|
reconciliation: "reconciled" | "unrecorded" | "stale" | "drifted" | "unknown";
|
|
61
61
|
/** Human-readable detail backing the verdict. */
|
|
62
62
|
detail: string;
|
|
63
|
+
/**
|
|
64
|
+
* Machine-readable "observed live", when live evidence was gathered (`--live`).
|
|
65
|
+
* A consumer joining this row onto a graph node should read this rather than
|
|
66
|
+
* string-matching `detail`. Absent when `--live` was not requested.
|
|
67
|
+
*/
|
|
68
|
+
live?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The owning deploy unit's raw status, when a lexicon reported it (AWS: the
|
|
71
|
+
* component's own CFN stack via `describeStackStatus`). Lets a renderer paint a
|
|
72
|
+
* richer palette than the reconciliation verdict — `healthy` green,
|
|
73
|
+
* present-but-not-healthy amber (mid-deploy) / red (rollback/failed).
|
|
74
|
+
*/
|
|
75
|
+
stack?: LiveStackInfo;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A component's owning deploy unit and its provider-native status. */
|
|
79
|
+
export interface LiveStackInfo {
|
|
80
|
+
/** The deploy-unit name (e.g. the CloudFormation stack name). */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Provider-native status string, e.g. "CREATE_COMPLETE". */
|
|
83
|
+
status?: string;
|
|
84
|
+
/** True when `status` is a terminal success state. */
|
|
85
|
+
healthy?: boolean;
|
|
63
86
|
}
|
|
64
87
|
|
|
65
88
|
export interface ComponentStatusResult {
|
|
@@ -84,6 +107,9 @@ export interface LiveComponentEvidence {
|
|
|
84
107
|
action?: ChangeAction;
|
|
85
108
|
/** Ownership verdict, when known. */
|
|
86
109
|
ownership?: "owned" | "foreign" | "unknown";
|
|
110
|
+
/** The owning deploy unit's raw status, when observed (AWS: the component's own
|
|
111
|
+
* CFN stack). Surfaced onto `ComponentStatusRow.stack` for a richer palette. */
|
|
112
|
+
stack?: LiveStackInfo;
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
/**
|
|
@@ -108,6 +134,7 @@ export function mergeLiveEvidence(
|
|
|
108
134
|
live: sup.live,
|
|
109
135
|
ownership: sup.ownership ?? b?.ownership,
|
|
110
136
|
action: b?.action,
|
|
137
|
+
stack: sup.stack ?? b?.stack,
|
|
111
138
|
});
|
|
112
139
|
}
|
|
113
140
|
return merged;
|
|
@@ -265,7 +292,17 @@ export function reconcileStatus(
|
|
|
265
292
|
detail = `recorded ${recorded!.timestamp} (digest ${recorded!.digest}), live and consistent`;
|
|
266
293
|
}
|
|
267
294
|
|
|
268
|
-
rows.push({
|
|
295
|
+
rows.push({
|
|
296
|
+
component,
|
|
297
|
+
env,
|
|
298
|
+
recorded,
|
|
299
|
+
build,
|
|
300
|
+
componentBom,
|
|
301
|
+
reconciliation,
|
|
302
|
+
detail,
|
|
303
|
+
...(liveEvidence ? { live: !!evidence?.live } : {}),
|
|
304
|
+
...(evidence?.stack ? { stack: evidence.stack } : {}),
|
|
305
|
+
});
|
|
269
306
|
}
|
|
270
307
|
|
|
271
308
|
return rows;
|