@intentius/chant 0.18.8 → 0.18.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAEA,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;CACd;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;CACpB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAsP9E;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
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAEA,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;CACd;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;CACpB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAkR9E;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"}
@@ -7,6 +7,14 @@ export declare function runLifecycleSnapshot(ctx: CommandContext): Promise<numbe
7
7
  * chant lifecycle show <environment> [lexicon]
8
8
  */
9
9
  export declare function runLifecycleShow(ctx: CommandContext): Promise<number>;
10
+ /**
11
+ * chant lifecycle rollback [<environment>] --to <ref>
12
+ *
13
+ * Open a PR restoring `sourceDir` to a prior git revision (#873). Source-level,
14
+ * reviewable, no cloud mutation — a human merges, then a gated apply rolls the
15
+ * env back. The `<env>` positional is PR context.
16
+ */
17
+ export declare function runLifecycleRollback(ctx: CommandContext): Promise<number>;
10
18
  /**
11
19
  * chant lifecycle diff <environment> [lexicon]
12
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAoBlD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwE/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoC3E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C3E;AAuTD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6G3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E;AAkBD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C/E"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAoBlD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwE/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoC3E;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C3E;AA6TD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6G3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E;AAkBD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C/E"}
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA0ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AAID,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,CAwBtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAchE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+D1E;AA4ND;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiFrE;AA4KD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA0ClD,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,CAwBtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAchE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+D1E;AAkOD;;;;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"}
@@ -0,0 +1,23 @@
1
+ /** Branch name for a rollback PR. Pure. */
2
+ export declare function rollbackBranchName(env: string | undefined, shortRef: string): string;
3
+ /** PR title for a rollback. Pure. */
4
+ export declare function rollbackTitle(env: string | undefined, ref: string): string;
5
+ /** PR body — explains that merging + applying completes the rollback. Pure. */
6
+ export declare function rollbackBody(env: string | undefined, ref: string, sourceDir: string): string;
7
+ export interface RollbackResult {
8
+ /** true when the source already matched the ref — nothing to roll back. */
9
+ noop: boolean;
10
+ branch?: string;
11
+ prUrl?: string;
12
+ }
13
+ /**
14
+ * Open a rollback PR restoring `sourceDir` to `ref`. Isolated worktree; the
15
+ * caller's branch is untouched. Throws on an unknown ref or a git/gh failure.
16
+ */
17
+ export declare function rollbackToRevision(opts: {
18
+ ref: string;
19
+ env: string | undefined;
20
+ sourceDir: string;
21
+ cwd: string;
22
+ }): Promise<RollbackResult>;
23
+ //# sourceMappingURL=rollback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../../src/lifecycle/rollback.ts"],"names":[],"mappings":"AAiBA,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED,qCAAqC;AACrC,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAO5F;AAED,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,cAAc,CAAC,CA6B1B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.18.8",
3
+ "version": "0.18.11",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -201,6 +201,39 @@ export const testEntity = {
201
201
  expect(existsSync(join(testDir, "dist", "ops", "alb-deploy", "worker.ts"))).toBe(true);
202
202
  });
203
203
 
204
+ test("op worker files go to <project>/dist/ops even with no --output (#878)", async () => {
205
+ // The generated Op worker must land where `chant run <op> --temporal` reads it
206
+ // (`<project>/dist/ops/<name>/worker.ts`) even when the build has no --output
207
+ // (a Temporal Op project often has no primary resource manifest to route).
208
+ const opSerializer: Serializer = {
209
+ name: "multi",
210
+ rulePrefix: "MULTI",
211
+ serialize: () => ({
212
+ primary: "{}",
213
+ files: {
214
+ "ops/durable-hello/workflow.ts": "// workflow\n",
215
+ "ops/durable-hello/worker.ts": "// worker\n",
216
+ "ops/durable-hello/activities.ts": "// activities\n",
217
+ },
218
+ }),
219
+ };
220
+ await writeFile(
221
+ join(testDir, "infra.ts"),
222
+ `export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexicon: "multi", kind: "resource", props: {}, attributes: {} };`,
223
+ );
224
+
225
+ const result = await buildCommand({
226
+ path: testDir,
227
+ format: "json",
228
+ serializers: [opSerializer],
229
+ // no `output`
230
+ } as BuildOptions);
231
+
232
+ expect(result.errors).toEqual([]);
233
+ expect(existsSync(join(testDir, "dist", "ops", "durable-hello", "worker.ts"))).toBe(true);
234
+ expect(existsSync(join(testDir, "dist", "ops", "durable-hello", "workflow.ts"))).toBe(true);
235
+ });
236
+
204
237
  // ── #284 bug 2: array-of-objects must serialize to valid YAML ──────────
205
238
  test("yaml format serializes an array of objects to valid, round-trippable YAML", async () => {
206
239
  // A serializer that emits a CloudFormation-shaped template with a tag list.
@@ -264,6 +264,34 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
264
264
  }
265
265
  }
266
266
 
267
+ // Op worker artifacts (`ops/<name>/{workflow,worker,activities}.ts`) always
268
+ // go to `<project>/dist/ops/` — the fixed location `chant run <op> --temporal`
269
+ // reads (`join(projectPath, "dist", "ops", ...)`) — independent of `--output`,
270
+ // which routes the primary resource manifest. Without this, a bare `chant build`
271
+ // only printed them to stderr and `--output foo.yaml` scattered them next to
272
+ // `foo.yaml`, so the durable-run worker was never where `run --temporal` looks.
273
+ const projectDist = resolve(options.path ?? ".", "dist");
274
+ let opsWritten = 0;
275
+ for (const [filename, content] of [...additionalFiles]) {
276
+ if (!filename.startsWith("ops/")) continue;
277
+ additionalFiles.delete(filename);
278
+ try {
279
+ const targetPath = join(projectDist, filename);
280
+ mkdirSync(dirname(targetPath), { recursive: true });
281
+ writeFileSync(targetPath, content);
282
+ opsWritten += 1;
283
+ } catch (err) {
284
+ errors.push(
285
+ formatError({
286
+ message: `Failed to write Op worker file ${filename}: ${err instanceof Error ? err.message : String(err)}`,
287
+ }),
288
+ );
289
+ }
290
+ }
291
+ if (opsWritten > 0) {
292
+ console.error(formatInfo(`Wrote ${opsWritten} Op worker file(s) under ${join(options.path ?? ".", "dist", "ops")}/`));
293
+ }
294
+
267
295
  if (options.output) {
268
296
  // Write to file — ensure parent directories exist for both the primary
269
297
  // output path and any nested additional-file paths (e.g. ops/<name>/...).
@@ -6,6 +6,7 @@ import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
6
6
  import { diffLive, diffLiveArtifacts, diffSnapshots, type LiveDiffResult, type LiveArtifactDiffResult, type SnapshotDiffResult } from "../../lifecycle/live-diff";
7
7
  import { buildChangeSet, renderChangeSet, gitlabMrReport, type ChangeSet } from "../../lifecycle/change-set";
8
8
  import { affectedStacks } from "../../lifecycle/affected";
9
+ import { rollbackToRevision } from "../../lifecycle/rollback";
9
10
  import { loadChantConfig } from "../../config";
10
11
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
11
12
  import type { CommandContext } from "../registry";
@@ -146,6 +147,38 @@ export async function runLifecycleShow(ctx: CommandContext): Promise<number> {
146
147
  return 0;
147
148
  }
148
149
 
150
+ /**
151
+ * chant lifecycle rollback [<environment>] --to <ref>
152
+ *
153
+ * Open a PR restoring `sourceDir` to a prior git revision (#873). Source-level,
154
+ * reviewable, no cloud mutation — a human merges, then a gated apply rolls the
155
+ * env back. The `<env>` positional is PR context.
156
+ */
157
+ export async function runLifecycleRollback(ctx: CommandContext): Promise<number> {
158
+ const { args } = ctx;
159
+ const environment = args.extraPositional; // optional — PR context/title
160
+ const ref = args.migrateTo; // the --to value
161
+ if (!ref) {
162
+ console.error(formatError({ message: "Target revision required: chant lifecycle rollback [<env>] --to <ref>" }));
163
+ return 1;
164
+ }
165
+ const { config } = await loadChantConfig(resolve("."));
166
+ const sourceDir = config.sourceDir ?? ".";
167
+ try {
168
+ const result = await rollbackToRevision({ ref, env: environment, sourceDir, cwd: resolve(".") });
169
+ if (result.noop) {
170
+ console.error(formatSuccess(`${sourceDir} already matches ${ref} — nothing to roll back`));
171
+ return 0;
172
+ }
173
+ console.log(result.prUrl); // the PR URL — the consumer (behold) reads this from stdout
174
+ console.error(formatSuccess(`Opened rollback PR on ${result.branch}`));
175
+ return 0;
176
+ } catch (err) {
177
+ console.error(formatError({ message: `rollback failed — ${err instanceof Error ? err.message : String(err)}` }));
178
+ return 1;
179
+ }
180
+ }
181
+
149
182
  /**
150
183
  * chant lifecycle diff <environment> [lexicon]
151
184
  */
@@ -317,7 +350,10 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<number> {
317
350
  let totalLexiconsChecked = 0;
318
351
  // Collected per-lexicon results, emitted as one JSON object when --json is set
319
352
  // (parsed by programmatic consumers, e.g. behold's inspect diff — #852).
320
- const byLexicon: Record<string, { resources?: LiveDiffResult; artifacts?: LiveArtifactDiffResult }> = {};
353
+ const byLexicon: Record<
354
+ string,
355
+ { resources?: LiveDiffResult; observed?: Record<string, ResourceMetadata>; artifacts?: LiveArtifactDiffResult }
356
+ > = {};
321
357
 
322
358
  for (const lexiconName of args.lexicons) {
323
359
  const plugin = args.plugins.find((p) => p.name === lexiconName);
@@ -379,8 +415,11 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<number> {
379
415
  const observedThen = prevSnapshot?.resources;
380
416
  const diff = diffLive({ declared, observedNow, observedThen });
381
417
  totalDrift += diff.driftedSinceSnapshot.length + diff.missing.length + diff.orphan.length + diff.disappeared.length;
382
- if (args.json) (byLexicon[lexiconName] ??= {}).resources = diff;
383
- else renderLiveDiff(lexiconName, args.environment, diff);
418
+ if (args.json) {
419
+ const entry = (byLexicon[lexiconName] ??= {});
420
+ entry.resources = diff;
421
+ entry.observed = observedNow; // live state (#862) — status/attributes per resource
422
+ } else renderLiveDiff(lexiconName, args.environment, diff);
384
423
  lexiconChecked = true;
385
424
  }
386
425
 
@@ -59,6 +59,32 @@ export async function makeTemporalClient(profileName: string | undefined, projec
59
59
  return { client, profile, config };
60
60
  }
61
61
 
62
+ /**
63
+ * Register the Keyword search attributes the generated workflow upserts
64
+ * (`OpName`, `Phase`, plus any op-declared) on the namespace, so the first
65
+ * workflow task does not fail with `BadSearchAttributes`. Registered one at a
66
+ * time so an already-present attribute (`ALREADY_EXISTS`) does not block the
67
+ * rest. Only used for the autoStart dev server — a real server (autoStart=false)
68
+ * is expected to have these pre-registered (it may reject registration).
69
+ */
70
+ async function ensureSearchAttributes(client: unknown, namespace: string, names: string[]): Promise<void> {
71
+ const operatorService = (
72
+ client as { connection?: { operatorService?: { addSearchAttributes?: (r: unknown) => Promise<unknown> } } }
73
+ ).connection?.operatorService;
74
+ if (!operatorService?.addSearchAttributes) return;
75
+ const KEYWORD = 2; // Temporal IndexedValueType KEYWORD
76
+ for (const name of names) {
77
+ try {
78
+ await operatorService.addSearchAttributes({ namespace, searchAttributes: { [name]: KEYWORD } });
79
+ } catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ if (!/already|exists/i.test(msg)) {
82
+ console.error(formatWarning({ message: `Could not register search attribute "${name}": ${msg}` }));
83
+ }
84
+ }
85
+ }
86
+ }
87
+
62
88
  // ── chant run list ──────────────────────────────────���─────────────────────────
63
89
 
64
90
  export async function runOpList(ctx: CommandContext): Promise<number> {
@@ -803,6 +829,12 @@ async function runComponentTemporal(ctx: CommandContext, selector: string): Prom
803
829
  return 1;
804
830
  }
805
831
 
832
+ // Register the search attributes the generated component workflow upserts
833
+ // (ComponentName/Phase) on the autoStart dev server (mirrors runOpTemporal).
834
+ if (profile.autoStart) {
835
+ await ensureSearchAttributes(client, profile.namespace, ["ComponentName", "Phase"]);
836
+ }
837
+
806
838
  const profileName = ctx.args.profile ??
807
839
  (((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
808
840
  "local";
@@ -1098,6 +1130,17 @@ async function runOpTemporal(ctx: CommandContext): Promise<number> {
1098
1130
  return 1;
1099
1131
  }
1100
1132
 
1133
+ // On the autoStart dev server, register the search attributes the generated
1134
+ // workflow upserts (OpName/Phase + any op-declared) so the first workflow task
1135
+ // does not fail with BadSearchAttributes.
1136
+ if (profile.autoStart) {
1137
+ await ensureSearchAttributes(client, profile.namespace, [
1138
+ "OpName",
1139
+ "Phase",
1140
+ ...Object.keys((config as { searchAttributes?: Record<string, string> }).searchAttributes ?? {}),
1141
+ ]);
1142
+ }
1143
+
1101
1144
  // Spawn worker process
1102
1145
  const profileName = ctx.args.profile ??
1103
1146
  (((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
package/src/cli/main.ts CHANGED
@@ -16,7 +16,7 @@ import { runInit, runInitLexicon } from "./handlers/init";
16
16
  import { runList, runDescribe, runImport, runAudit, runUpdate, runDoctor } from "./handlers/misc";
17
17
  import { runVendor } from "./handlers/vendor";
18
18
  import { runMigrate } from "./handlers/migrate";
19
- import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
19
+ import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
20
20
  import { runComponentsStatus, runComponentsReleaseRecord, runComponentsUnknown } from "./handlers/components";
21
21
  import { runGraph } from "./handlers/graph";
22
22
  import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
@@ -445,6 +445,7 @@ const registry: CommandDef[] = [
445
445
  { name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
446
446
  { name: "lifecycle show", handler: runLifecycleShow },
447
447
  { name: "lifecycle diff", requiresPlugins: true, handler: runLifecycleDiff },
448
+ { name: "lifecycle rollback", handler: runLifecycleRollback },
448
449
  { name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
449
450
  { name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
450
451
  { name: "lifecycle log", handler: runLifecycleLog },
@@ -0,0 +1,25 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { rollbackBranchName, rollbackTitle, rollbackBody } from "./rollback";
3
+
4
+ describe("rollback helpers (#873)", () => {
5
+ test("branch name includes env and short ref", () => {
6
+ expect(rollbackBranchName("prod", "a1b2c3d")).toBe("chant/rollback-prod-a1b2c3d");
7
+ });
8
+
9
+ test("branch name falls back to 'src' when env is absent", () => {
10
+ expect(rollbackBranchName(undefined, "a1b2c3d")).toBe("chant/rollback-src-a1b2c3d");
11
+ });
12
+
13
+ test("title names the env and ref", () => {
14
+ expect(rollbackTitle("prod", "v1.2.0")).toBe("rollback prod source to v1.2.0");
15
+ expect(rollbackTitle(undefined, "v1.2.0")).toBe("rollback source to v1.2.0");
16
+ });
17
+
18
+ test("body references the sourceDir, ref, and the gated-apply step", () => {
19
+ const body = rollbackBody("prod", "abc123", "src");
20
+ expect(body).toContain("`src`");
21
+ expect(body).toContain("`abc123`");
22
+ expect(body).toContain("the prod environment");
23
+ expect(body).toMatch(/approval gate|Sync|apply/i);
24
+ });
25
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * lifecycle rollback (#873) — open a PR that restores the source to a prior git
3
+ * revision. Rollback = make an env match a past state of the declared source; the
4
+ * PR's diff *is* the rollback delta. No cloud mutation here — a human merges, then
5
+ * the existing gated apply rolls the env back.
6
+ *
7
+ * Runs entirely in an isolated `git worktree` (like {@link ./affected}), so the
8
+ * caller's working branch/tree is never touched — important because the consumer
9
+ * (behold) serves a live checkout.
10
+ */
11
+ import { execFile } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ const execFileAsync = promisify(execFile);
17
+
18
+ /** Branch name for a rollback PR. Pure. */
19
+ export function rollbackBranchName(env: string | undefined, shortRef: string): string {
20
+ return `chant/rollback-${env ?? "src"}-${shortRef}`;
21
+ }
22
+
23
+ /** PR title for a rollback. Pure. */
24
+ export function rollbackTitle(env: string | undefined, ref: string): string {
25
+ return `rollback ${env ? env + " " : ""}source to ${ref}`;
26
+ }
27
+
28
+ /** PR body — explains that merging + applying completes the rollback. Pure. */
29
+ export function rollbackBody(env: string | undefined, ref: string, sourceDir: string): string {
30
+ const target = env ? `the ${env} environment` : "the environment";
31
+ return (
32
+ `Restores \`${sourceDir}\` to its state at \`${ref}\`.\n\n` +
33
+ `The diff below is the rollback delta. Merge this PR, then apply (Sync) to roll ${target} back — ` +
34
+ `a destructive apply still passes through its approval gate. No cloud change was made by opening this PR.`
35
+ );
36
+ }
37
+
38
+ export interface RollbackResult {
39
+ /** true when the source already matched the ref — nothing to roll back. */
40
+ noop: boolean;
41
+ branch?: string;
42
+ prUrl?: string;
43
+ }
44
+
45
+ /**
46
+ * Open a rollback PR restoring `sourceDir` to `ref`. Isolated worktree; the
47
+ * caller's branch is untouched. Throws on an unknown ref or a git/gh failure.
48
+ */
49
+ export async function rollbackToRevision(opts: {
50
+ ref: string;
51
+ env: string | undefined;
52
+ sourceDir: string;
53
+ cwd: string;
54
+ }): Promise<RollbackResult> {
55
+ const { ref, env, sourceDir, cwd } = opts;
56
+ const git = (args: string[], wd: string): Promise<{ stdout: string }> => execFileAsync("git", args, { cwd: wd });
57
+
58
+ const repoRoot = (await git(["rev-parse", "--show-toplevel"], cwd)).stdout.trim();
59
+ // Verify the ref resolves to a commit (throws with a clear message otherwise).
60
+ await execFileAsync("git", ["rev-parse", "--verify", `${ref}^{commit}`], { cwd: repoRoot });
61
+ const shortRef = (await git(["rev-parse", "--short", ref], repoRoot)).stdout.trim();
62
+ const branch = rollbackBranchName(env, shortRef);
63
+ const wt = join(tmpdir(), `chant-rollback-${shortRef}-${process.pid}`);
64
+
65
+ await git(["worktree", "add", wt, "-b", branch, "HEAD"], repoRoot);
66
+ try {
67
+ // Restore just the source tree to the target revision (stages the changes).
68
+ await git(["checkout", ref, "--", sourceDir], wt);
69
+ const staged = (await git(["status", "--porcelain", "--", sourceDir], wt)).stdout.trim();
70
+ if (!staged) return { noop: true };
71
+
72
+ await git(["commit", "-m", rollbackTitle(env, ref)], wt);
73
+ await git(["push", "-u", "origin", branch], wt);
74
+ const { stdout } = await execFileAsync(
75
+ "gh",
76
+ ["pr", "create", "--title", rollbackTitle(env, ref), "--body", rollbackBody(env, ref, sourceDir), "--head", branch],
77
+ { cwd: wt },
78
+ );
79
+ return { noop: false, branch, prUrl: stdout.trim() };
80
+ } finally {
81
+ await git(["worktree", "remove", "--force", wt], repoRoot).catch(() => {});
82
+ }
83
+ }