norn-cli 2.6.1 → 2.8.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.js CHANGED
@@ -101065,10 +101065,10 @@ var require_resolveCommand = __commonJS({
101065
101065
  }
101066
101066
  return resolved;
101067
101067
  }
101068
- function resolveCommand(parsed) {
101068
+ function resolveCommand2(parsed) {
101069
101069
  return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true);
101070
101070
  }
101071
- module2.exports = resolveCommand;
101071
+ module2.exports = resolveCommand2;
101072
101072
  }
101073
101073
  });
101074
101074
 
@@ -101152,19 +101152,19 @@ var require_parse2 = __commonJS({
101152
101152
  "node_modules/cross-spawn/lib/parse.js"(exports2, module2) {
101153
101153
  "use strict";
101154
101154
  var path20 = require("path");
101155
- var resolveCommand = require_resolveCommand();
101155
+ var resolveCommand2 = require_resolveCommand();
101156
101156
  var escape2 = require_escape();
101157
101157
  var readShebang = require_readShebang();
101158
101158
  var isWin = process.platform === "win32";
101159
101159
  var isExecutableRegExp = /\.(?:com|exe)$/i;
101160
101160
  var isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
101161
101161
  function detectShebang(parsed) {
101162
- parsed.file = resolveCommand(parsed);
101162
+ parsed.file = resolveCommand2(parsed);
101163
101163
  const shebang = parsed.file && readShebang(parsed.file);
101164
101164
  if (shebang) {
101165
101165
  parsed.args.unshift(parsed.file);
101166
101166
  parsed.command = shebang;
101167
- return resolveCommand(parsed);
101167
+ return resolveCommand2(parsed);
101168
101168
  }
101169
101169
  return parsed.file;
101170
101170
  }
@@ -101266,7 +101266,7 @@ var require_cross_spawn = __commonJS({
101266
101266
  var cp = require("child_process");
101267
101267
  var parse5 = require_parse2();
101268
101268
  var enoent = require_enoent();
101269
- function spawn5(command, args, options) {
101269
+ function spawn6(command, args, options) {
101270
101270
  const parsed = parse5(command, args, options);
101271
101271
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
101272
101272
  enoent.hookChildProcess(spawned, parsed);
@@ -101278,8 +101278,8 @@ var require_cross_spawn = __commonJS({
101278
101278
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
101279
101279
  return result;
101280
101280
  }
101281
- module2.exports = spawn5;
101282
- module2.exports.spawn = spawn5;
101281
+ module2.exports = spawn6;
101282
+ module2.exports.spawn = spawn6;
101283
101283
  module2.exports.sync = spawnSync3;
101284
101284
  module2.exports._parse = parse5;
101285
101285
  module2.exports._enoent = enoent;
@@ -134283,6 +134283,635 @@ function applyRequestTimeoutForPath(startPath, overrideMs) {
134283
134283
  return timeoutMs;
134284
134284
  }
134285
134285
 
134286
+ // src/k8s/k8sRunner.ts
134287
+ var import_child_process4 = require("child_process");
134288
+
134289
+ // src/k8s/k8sParser.ts
134290
+ function tokenizeCommandLine(line2) {
134291
+ const tokens = [];
134292
+ let current = "";
134293
+ let quote;
134294
+ let escaped = false;
134295
+ for (const char of line2) {
134296
+ if (escaped) {
134297
+ current += char;
134298
+ escaped = false;
134299
+ continue;
134300
+ }
134301
+ if (char === "\\" && quote !== "'") {
134302
+ escaped = true;
134303
+ continue;
134304
+ }
134305
+ if (quote) {
134306
+ if (char === quote) {
134307
+ quote = void 0;
134308
+ } else {
134309
+ current += char;
134310
+ }
134311
+ continue;
134312
+ }
134313
+ if (char === '"' || char === "'") {
134314
+ quote = char;
134315
+ continue;
134316
+ }
134317
+ if (/\s/.test(char)) {
134318
+ if (current) {
134319
+ tokens.push(current);
134320
+ current = "";
134321
+ }
134322
+ continue;
134323
+ }
134324
+ current += char;
134325
+ }
134326
+ if (escaped) {
134327
+ current += "\\";
134328
+ }
134329
+ if (quote) {
134330
+ return { tokens, error: "Unterminated quoted argument" };
134331
+ }
134332
+ if (current) {
134333
+ tokens.push(current);
134334
+ }
134335
+ return { tokens };
134336
+ }
134337
+ function parseCommonOptions(tokens) {
134338
+ const remaining = [];
134339
+ let namespace;
134340
+ let context;
134341
+ for (let index = 0; index < tokens.length; index++) {
134342
+ const token = tokens[index];
134343
+ if (token === "-n" || token === "--namespace") {
134344
+ const value = tokens[++index];
134345
+ if (!value) {
134346
+ return { remaining, error: `${token} requires a namespace` };
134347
+ }
134348
+ namespace = value;
134349
+ continue;
134350
+ }
134351
+ if (token.startsWith("--namespace=")) {
134352
+ namespace = token.slice("--namespace=".length);
134353
+ if (!namespace) {
134354
+ return { remaining, error: "--namespace requires a namespace" };
134355
+ }
134356
+ continue;
134357
+ }
134358
+ if (token === "--context") {
134359
+ const value = tokens[++index];
134360
+ if (!value) {
134361
+ return { remaining, error: "--context requires a context name" };
134362
+ }
134363
+ context = value;
134364
+ continue;
134365
+ }
134366
+ if (token.startsWith("--context=")) {
134367
+ context = token.slice("--context=".length);
134368
+ if (!context) {
134369
+ return { remaining, error: "--context requires a context name" };
134370
+ }
134371
+ continue;
134372
+ }
134373
+ remaining.push(token);
134374
+ }
134375
+ return { remaining, namespace, context };
134376
+ }
134377
+ function parseK8sCommand(rawLine, line2) {
134378
+ const raw = stripInlineComment(rawLine).trim();
134379
+ const captureMatch = raw.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*(.+)$/);
134380
+ if (captureMatch) {
134381
+ const [, captureVar, expression] = captureMatch;
134382
+ const parts = expression.split("|");
134383
+ if (parts.length !== 2) {
134384
+ return { error: { line: line2, message: "Expected: var <name> = get pods [-l selector] | first" } };
134385
+ }
134386
+ if (parts[1].trim() !== "first") {
134387
+ return { error: { line: line2, message: "Capture only supports '| first'" } };
134388
+ }
134389
+ const inner = parseK8sCommand(parts[0].trim(), line2);
134390
+ if (inner.error) {
134391
+ return { error: inner.error };
134392
+ }
134393
+ if (!inner.command || inner.command.kind !== "getPods") {
134394
+ return { error: { line: line2, message: "Capture must read from a get pods command" } };
134395
+ }
134396
+ if (inner.command.allNamespaces) {
134397
+ return { error: { line: line2, message: "Capture cannot use -A/--all-namespaces because the captured pod name would lose its namespace" } };
134398
+ }
134399
+ return { command: { ...inner.command, raw, captureVar, capturePick: "first" } };
134400
+ }
134401
+ const tokenized = tokenizeCommandLine(raw);
134402
+ if (tokenized.error) {
134403
+ return { error: { line: line2, message: tokenized.error } };
134404
+ }
134405
+ const tokens = tokenized.tokens;
134406
+ if (tokens.length === 0) {
134407
+ return {};
134408
+ }
134409
+ if (tokens[0] === "run") {
134410
+ if (tokens.length !== 2 || !/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(tokens[1])) {
134411
+ return { error: { line: line2, message: "Expected: run <SequenceName>" } };
134412
+ }
134413
+ return {
134414
+ command: {
134415
+ kind: "run",
134416
+ raw,
134417
+ line: line2,
134418
+ sequenceName: tokens[1]
134419
+ }
134420
+ };
134421
+ }
134422
+ if (tokens[0] === "get") {
134423
+ if (tokens[1] !== "pods") {
134424
+ return { error: { line: line2, message: "Expected: get pods [-l selector] [-A | -n namespace] [--context name]" } };
134425
+ }
134426
+ const commonTokens = [];
134427
+ let selector;
134428
+ let allNamespaces = false;
134429
+ const rest = tokens.slice(2);
134430
+ for (let index = 0; index < rest.length; index++) {
134431
+ const token = rest[index];
134432
+ if (token === "-l" || token === "--selector") {
134433
+ const value = rest[++index];
134434
+ if (!value) {
134435
+ return { error: { line: line2, message: `${token} requires a label selector` } };
134436
+ }
134437
+ selector = value;
134438
+ continue;
134439
+ }
134440
+ if (token.startsWith("--selector=") || token.startsWith("-l=")) {
134441
+ selector = token.slice(token.indexOf("=") + 1);
134442
+ if (!selector) {
134443
+ return { error: { line: line2, message: "Label selector flag requires a value" } };
134444
+ }
134445
+ continue;
134446
+ }
134447
+ if (token === "-A" || token === "--all-namespaces") {
134448
+ allNamespaces = true;
134449
+ continue;
134450
+ }
134451
+ commonTokens.push(token);
134452
+ }
134453
+ const options = parseCommonOptions(commonTokens);
134454
+ if (allNamespaces && options.namespace) {
134455
+ return { error: { line: line2, message: "get pods cannot combine -A/--all-namespaces with -n/--namespace" } };
134456
+ }
134457
+ if (options.error || options.remaining.length > 0) {
134458
+ return { error: { line: line2, message: options.error ?? "Expected: get pods [-l selector] [-A | -n namespace] [--context name]" } };
134459
+ }
134460
+ return {
134461
+ command: {
134462
+ kind: "getPods",
134463
+ raw,
134464
+ line: line2,
134465
+ selector,
134466
+ allNamespaces,
134467
+ namespace: options.namespace,
134468
+ context: options.context
134469
+ }
134470
+ };
134471
+ }
134472
+ if (tokens[0] === "describe") {
134473
+ if (tokens[1] !== "pod") {
134474
+ return { error: { line: line2, message: "Expected: describe pod <name> [-n namespace] [--context name]" } };
134475
+ }
134476
+ const pod = tokens[2];
134477
+ if (!pod) {
134478
+ return { error: { line: line2, message: "Expected: describe pod <name> [-n namespace] [--context name]" } };
134479
+ }
134480
+ const options = parseCommonOptions(tokens.slice(3));
134481
+ if (options.error || options.remaining.length > 0) {
134482
+ return { error: { line: line2, message: options.error ?? "Expected: describe pod <name> [-n namespace] [--context name]" } };
134483
+ }
134484
+ return {
134485
+ command: {
134486
+ kind: "describe",
134487
+ raw,
134488
+ line: line2,
134489
+ pod,
134490
+ namespace: options.namespace,
134491
+ context: options.context
134492
+ }
134493
+ };
134494
+ }
134495
+ if (tokens[0] === "logs") {
134496
+ const pod = tokens[1];
134497
+ if (!pod) {
134498
+ return { error: { line: line2, message: "Expected: logs <pod> [--tail N] [-n namespace] [--context name]" } };
134499
+ }
134500
+ const commonTokens = [];
134501
+ let tail;
134502
+ for (let index = 2; index < tokens.length; index++) {
134503
+ const token = tokens[index];
134504
+ if (token === "--tail") {
134505
+ const value = tokens[++index];
134506
+ if (!value || !/^\d+$/.test(value) && !/^\{\{[^}]+\}\}$/.test(value)) {
134507
+ return { error: { line: line2, message: "--tail requires a non-negative integer" } };
134508
+ }
134509
+ tail = /^\d+$/.test(value) ? Number(value) : value;
134510
+ continue;
134511
+ }
134512
+ if (token.startsWith("--tail=")) {
134513
+ const value = token.slice("--tail=".length);
134514
+ if (!/^\d+$/.test(value) && !/^\{\{[^}]+\}\}$/.test(value)) {
134515
+ return { error: { line: line2, message: "--tail requires a non-negative integer" } };
134516
+ }
134517
+ tail = /^\d+$/.test(value) ? Number(value) : value;
134518
+ continue;
134519
+ }
134520
+ commonTokens.push(token);
134521
+ }
134522
+ const options = parseCommonOptions(commonTokens);
134523
+ if (options.error || options.remaining.length > 0) {
134524
+ return { error: { line: line2, message: options.error ?? "Expected: logs <pod> [--tail N] [-n namespace] [--context name]" } };
134525
+ }
134526
+ return {
134527
+ command: {
134528
+ kind: "logs",
134529
+ raw,
134530
+ line: line2,
134531
+ pod,
134532
+ tail,
134533
+ namespace: options.namespace,
134534
+ context: options.context
134535
+ }
134536
+ };
134537
+ }
134538
+ if (tokens[0] === "restart") {
134539
+ const resource = tokens[1];
134540
+ const options = parseCommonOptions(tokens.slice(2));
134541
+ if (!resource?.startsWith("deployment/") || resource.length === "deployment/".length || options.error || options.remaining.length > 0) {
134542
+ return { error: { line: line2, message: options.error ?? "Expected: restart deployment/<name> [-n namespace] [--context name]" } };
134543
+ }
134544
+ return {
134545
+ command: {
134546
+ kind: "restart",
134547
+ raw,
134548
+ line: line2,
134549
+ resource,
134550
+ namespace: options.namespace,
134551
+ context: options.context
134552
+ }
134553
+ };
134554
+ }
134555
+ return { error: { line: line2, message: `Unsupported Kubernetes command: ${tokens[0]}` } };
134556
+ }
134557
+ function parseK8sDocument(text) {
134558
+ const lines = text.split(/\r?\n/);
134559
+ const document2 = {
134560
+ sequences: [],
134561
+ standaloneCommands: [],
134562
+ errors: []
134563
+ };
134564
+ let currentSequence;
134565
+ const sequenceNames = /* @__PURE__ */ new Set();
134566
+ for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
134567
+ const raw = lines[lineNumber];
134568
+ const line2 = stripInlineComment(raw).trim();
134569
+ if (!line2) {
134570
+ continue;
134571
+ }
134572
+ const sequenceMatch = line2.match(/^(runbook\s+)?sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)\s*$/);
134573
+ if (sequenceMatch) {
134574
+ if (currentSequence) {
134575
+ document2.errors.push({ line: lineNumber, message: "Nested sequences are not supported" });
134576
+ continue;
134577
+ }
134578
+ const name = sequenceMatch[2];
134579
+ if (sequenceNames.has(name)) {
134580
+ document2.errors.push({ line: lineNumber, message: `Duplicate sequence '${name}'` });
134581
+ }
134582
+ sequenceNames.add(name);
134583
+ currentSequence = {
134584
+ name,
134585
+ isRunbook: Boolean(sequenceMatch[1]),
134586
+ startLine: lineNumber,
134587
+ endLine: lineNumber,
134588
+ commands: []
134589
+ };
134590
+ continue;
134591
+ }
134592
+ if (line2 === "end sequence") {
134593
+ if (!currentSequence) {
134594
+ document2.errors.push({ line: lineNumber, message: "Unexpected end sequence" });
134595
+ continue;
134596
+ }
134597
+ currentSequence.endLine = lineNumber;
134598
+ document2.sequences.push(currentSequence);
134599
+ currentSequence = void 0;
134600
+ continue;
134601
+ }
134602
+ if (line2.startsWith("namespace ")) {
134603
+ if (currentSequence) {
134604
+ document2.errors.push({ line: lineNumber, message: "namespace is a file-level directive and cannot appear inside a sequence" });
134605
+ continue;
134606
+ }
134607
+ const value = line2.slice("namespace ".length).trim();
134608
+ if (!value) {
134609
+ document2.errors.push({ line: lineNumber, message: "namespace requires a value" });
134610
+ } else if (document2.namespace) {
134611
+ document2.errors.push({ line: lineNumber, message: "Only one namespace directive is allowed per file" });
134612
+ } else {
134613
+ document2.namespace = { value, line: lineNumber };
134614
+ }
134615
+ continue;
134616
+ }
134617
+ const parsed = parseK8sCommand(raw, lineNumber);
134618
+ if (parsed.error) {
134619
+ document2.errors.push(parsed.error);
134620
+ continue;
134621
+ }
134622
+ if (!parsed.command) {
134623
+ continue;
134624
+ }
134625
+ if (currentSequence) {
134626
+ currentSequence.commands.push(parsed.command);
134627
+ } else if (parsed.command.kind === "run") {
134628
+ document2.errors.push({ line: lineNumber, message: "run <SequenceName> is only valid inside a sequence" });
134629
+ } else if (parsed.command.captureVar) {
134630
+ document2.errors.push({ line: lineNumber, message: "var capture is only valid inside a sequence" });
134631
+ } else {
134632
+ document2.standaloneCommands.push(parsed.command);
134633
+ }
134634
+ }
134635
+ if (currentSequence) {
134636
+ document2.errors.push({ line: currentSequence.startLine, message: `Sequence '${currentSequence.name}' is missing end sequence` });
134637
+ }
134638
+ return document2;
134639
+ }
134640
+
134641
+ // src/k8s/k8sRunner.ts
134642
+ function quoteDisplayArg(value) {
134643
+ return /^[a-zA-Z0-9_./:=@-]+$/.test(value) ? value : JSON.stringify(value);
134644
+ }
134645
+ function unresolvedVariables(text) {
134646
+ return Array.from(text.matchAll(/\{\{([^}]+)\}\}/g), (match) => match[1]);
134647
+ }
134648
+ function substituteOrThrow(text, variables, description) {
134649
+ const substituted = substituteVariables(text, variables);
134650
+ const unresolved = unresolvedVariables(substituted);
134651
+ if (unresolved.length > 0) {
134652
+ throw new Error(`Unresolved variable${unresolved.length === 1 ? "" : "s"} in ${description}: ${unresolved.join(", ")}`);
134653
+ }
134654
+ return substituted;
134655
+ }
134656
+ function resolveCommand(command, variables) {
134657
+ const substituted = substituteOrThrow(command.raw, variables, `line ${command.line + 1}`);
134658
+ const parsed = parseK8sCommand(substituted, command.line);
134659
+ if (parsed.error || !parsed.command) {
134660
+ throw new Error(parsed.error?.message ?? `Invalid Kubernetes command on line ${command.line + 1}`);
134661
+ }
134662
+ return parsed.command;
134663
+ }
134664
+ function buildKubectlInvocation(command, defaultNamespace, selectedContext) {
134665
+ const args = [];
134666
+ const context = command.context ?? selectedContext;
134667
+ const namespace = command.namespace ?? defaultNamespace;
134668
+ if (context) {
134669
+ args.push("--context", context);
134670
+ }
134671
+ if (command.allNamespaces) {
134672
+ args.push("-A");
134673
+ } else if (namespace) {
134674
+ args.push("-n", namespace);
134675
+ }
134676
+ switch (command.kind) {
134677
+ case "getPods":
134678
+ args.push("get", "pods");
134679
+ if (command.selector) {
134680
+ args.push("-l", command.selector);
134681
+ }
134682
+ break;
134683
+ case "logs":
134684
+ args.push("logs", command.pod, "--tail", String(command.tail ?? 200));
134685
+ break;
134686
+ case "restart":
134687
+ args.push("rollout", "restart", command.resource);
134688
+ break;
134689
+ case "describe":
134690
+ args.push("describe", "pod", command.pod);
134691
+ break;
134692
+ case "run":
134693
+ throw new Error("Sequence calls cannot be converted directly to kubectl commands");
134694
+ }
134695
+ return { args, command };
134696
+ }
134697
+ function executeKubectl(args, options) {
134698
+ const command = options.kubectlCommand ?? "kubectl";
134699
+ const startTime = Date.now();
134700
+ return new Promise((resolve16) => {
134701
+ const child = (0, import_child_process4.spawn)(command, args, {
134702
+ cwd: options.workingDirectory,
134703
+ env: options.env ?? process.env,
134704
+ shell: false
134705
+ });
134706
+ let stdout = "";
134707
+ let stderr = "";
134708
+ let settled = false;
134709
+ const finish = (result) => {
134710
+ if (settled) {
134711
+ return;
134712
+ }
134713
+ settled = true;
134714
+ resolve16(result);
134715
+ };
134716
+ child.stdout.on("data", (data) => {
134717
+ const text = data.toString();
134718
+ stdout += text;
134719
+ options.onOutput?.(text, "stdout");
134720
+ });
134721
+ child.stderr.on("data", (data) => {
134722
+ const text = data.toString();
134723
+ stderr += text;
134724
+ options.onOutput?.(text, "stderr");
134725
+ });
134726
+ child.on("error", (error2) => {
134727
+ const message = `Failed to start ${command}: ${error2.message}`;
134728
+ options.onOutput?.(`${message}
134729
+ `, "stderr");
134730
+ finish({
134731
+ success: false,
134732
+ command,
134733
+ args,
134734
+ stdout,
134735
+ stderr: stderr || message,
134736
+ exitCode: 1,
134737
+ duration: Date.now() - startTime
134738
+ });
134739
+ });
134740
+ child.on("close", (code) => {
134741
+ finish({
134742
+ success: code === 0,
134743
+ command,
134744
+ args,
134745
+ stdout,
134746
+ stderr,
134747
+ exitCode: code ?? 1,
134748
+ duration: Date.now() - startTime
134749
+ });
134750
+ });
134751
+ });
134752
+ }
134753
+ async function captureFirstPod(command, defaultNamespace, options) {
134754
+ const context = command.context ?? options.selectedContext;
134755
+ const namespace = command.namespace ?? defaultNamespace;
134756
+ const args = [];
134757
+ if (context) {
134758
+ args.push("--context", context);
134759
+ }
134760
+ if (command.allNamespaces) {
134761
+ args.push("-A");
134762
+ } else if (namespace) {
134763
+ args.push("-n", namespace);
134764
+ }
134765
+ args.push("get", "pods");
134766
+ if (command.selector) {
134767
+ args.push("-l", command.selector);
134768
+ }
134769
+ args.push("-o", "name");
134770
+ const binary = options.kubectlCommand ?? "kubectl";
134771
+ options.onCommand?.([binary, ...args].map(quoteDisplayArg).join(" "));
134772
+ const result = options.execute ? await options.execute(args) : await executeKubectl(args, options);
134773
+ if (!result.success) {
134774
+ throw new Error(result.stderr.trim() || `kubectl exited with code ${result.exitCode}`);
134775
+ }
134776
+ const names = result.stdout.split(/\r?\n/).map((name) => name.trim().replace(/^pod\//, "")).filter(Boolean);
134777
+ if (names.length === 0) {
134778
+ throw new Error(command.selector ? `No pods matched selector ${command.selector}` : "No pods found to capture");
134779
+ }
134780
+ return { result, podName: names[0] };
134781
+ }
134782
+ async function authorizeRestart(command, options) {
134783
+ if (options.allowRestart) {
134784
+ return true;
134785
+ }
134786
+ if (options.confirmRestart) {
134787
+ return options.confirmRestart(command);
134788
+ }
134789
+ return false;
134790
+ }
134791
+ async function executeCommand(sourceCommand, defaultNamespace, options) {
134792
+ const command = resolveCommand(sourceCommand, options.variables ?? {});
134793
+ const confirmationCommand = {
134794
+ ...command,
134795
+ namespace: command.namespace ?? defaultNamespace,
134796
+ context: command.context ?? options.selectedContext
134797
+ };
134798
+ if (command.kind === "restart" && !await authorizeRestart(confirmationCommand, options)) {
134799
+ throw new Error(`Restart requires confirmation for ${command.resource}`);
134800
+ }
134801
+ const invocation = buildKubectlInvocation(command, defaultNamespace, options.selectedContext);
134802
+ const binary = options.kubectlCommand ?? "kubectl";
134803
+ options.onCommand?.([binary, ...invocation.args].map(quoteDisplayArg).join(" "));
134804
+ return options.execute ? options.execute(invocation.args) : executeKubectl(invocation.args, options);
134805
+ }
134806
+ function formatParseErrors(document2) {
134807
+ return document2.errors.map((error2) => `Line ${error2.line + 1}: ${error2.message}`);
134808
+ }
134809
+ async function runSequenceCommands(sequence, sequencesByName, defaultNamespace, options, callStack, results) {
134810
+ if (callStack.includes(sequence.name)) {
134811
+ return `Circular sequence call: ${[...callStack, sequence.name].join(" -> ")}`;
134812
+ }
134813
+ const nextStack = [...callStack, sequence.name];
134814
+ for (const command of sequence.commands) {
134815
+ if (command.kind === "run") {
134816
+ const target = sequencesByName.get(command.sequenceName);
134817
+ if (!target) {
134818
+ return `Line ${command.line + 1}: Sequence '${command.sequenceName}' not found`;
134819
+ }
134820
+ const nestedError = await runSequenceCommands(target, sequencesByName, defaultNamespace, options, nextStack, results);
134821
+ if (nestedError) {
134822
+ return nestedError;
134823
+ }
134824
+ continue;
134825
+ }
134826
+ if (command.captureVar) {
134827
+ try {
134828
+ const resolved = resolveCommand(command, options.variables ?? {});
134829
+ const { result, podName } = await captureFirstPod(resolved, defaultNamespace, options);
134830
+ results.push(result);
134831
+ (options.variables ??= {})[command.captureVar] = podName;
134832
+ options.onOutput?.(`# ${command.captureVar} = ${podName}
134833
+ `, "stdout");
134834
+ } catch (error2) {
134835
+ return `Line ${command.line + 1}: ${error2 instanceof Error ? error2.message : String(error2)}`;
134836
+ }
134837
+ continue;
134838
+ }
134839
+ try {
134840
+ const result = await executeCommand(command, defaultNamespace, options);
134841
+ results.push(result);
134842
+ if (!result.success) {
134843
+ return `Line ${command.line + 1}: kubectl exited with code ${result.exitCode}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ""}`;
134844
+ }
134845
+ } catch (error2) {
134846
+ return `Line ${command.line + 1}: ${error2 instanceof Error ? error2.message : String(error2)}`;
134847
+ }
134848
+ }
134849
+ return void 0;
134850
+ }
134851
+ function resolveDefaultNamespace(document2, variables) {
134852
+ if (!document2.namespace) {
134853
+ return void 0;
134854
+ }
134855
+ return substituteOrThrow(document2.namespace.value, variables, `namespace directive on line ${document2.namespace.line + 1}`);
134856
+ }
134857
+ async function runK8sSequence(text, sequenceName, options = {}) {
134858
+ const startTime = Date.now();
134859
+ const document2 = parseK8sDocument(text);
134860
+ const errors = formatParseErrors(document2);
134861
+ const commands = [];
134862
+ const sequence = document2.sequences.find((candidate) => candidate.name === sequenceName);
134863
+ if (!sequence) {
134864
+ errors.push(`Sequence '${sequenceName}' not found`);
134865
+ }
134866
+ if (errors.length > 0 || !sequence) {
134867
+ return { name: sequenceName, success: false, commands, errors, duration: Date.now() - startTime };
134868
+ }
134869
+ try {
134870
+ const runOptions = {
134871
+ ...options,
134872
+ variables: copyEnvironmentScope({ ...options.variables ?? {} }, options.variables ?? {})
134873
+ };
134874
+ const defaultNamespace = resolveDefaultNamespace(document2, runOptions.variables ?? {});
134875
+ const runtimeError = await runSequenceCommands(
134876
+ sequence,
134877
+ new Map(document2.sequences.map((candidate) => [candidate.name, candidate])),
134878
+ defaultNamespace,
134879
+ runOptions,
134880
+ [],
134881
+ commands
134882
+ );
134883
+ if (runtimeError) {
134884
+ errors.push(runtimeError);
134885
+ }
134886
+ } catch (error2) {
134887
+ errors.push(error2 instanceof Error ? error2.message : String(error2));
134888
+ }
134889
+ return {
134890
+ name: sequenceName,
134891
+ success: errors.length === 0,
134892
+ commands,
134893
+ errors,
134894
+ duration: Date.now() - startTime
134895
+ };
134896
+ }
134897
+ async function runK8sRunbooks(text, options = {}) {
134898
+ const document2 = parseK8sDocument(text);
134899
+ if (document2.errors.length > 0) {
134900
+ return [{
134901
+ name: "parse",
134902
+ success: false,
134903
+ commands: [],
134904
+ errors: formatParseErrors(document2),
134905
+ duration: 0
134906
+ }];
134907
+ }
134908
+ const results = [];
134909
+ for (const sequence of document2.sequences.filter((candidate) => candidate.isRunbook)) {
134910
+ results.push(await runK8sSequence(text, sequence.name, options));
134911
+ }
134912
+ return results;
134913
+ }
134914
+
134286
134915
  // src/cli.ts
134287
134916
  function handleImportResolutionErrors(errors, colors) {
134288
134917
  const { blockingErrors, warningErrors } = splitImportResolutionErrors(errors);
@@ -134407,7 +135036,8 @@ function parseArgs(args) {
134407
135036
  tagsFilter: [],
134408
135037
  insecure: false,
134409
135038
  refactorRegionPattern: false,
134410
- writeRefactor: false
135039
+ writeRefactor: false,
135040
+ yes: false
134411
135041
  };
134412
135042
  for (let i = 0; i < args.length; i++) {
134413
135043
  const arg = args[i];
@@ -134439,6 +135069,15 @@ function parseArgs(args) {
134439
135069
  options.refactorRegionPattern = true;
134440
135070
  } else if (arg === "--write") {
134441
135071
  options.writeRefactor = true;
135072
+ } else if (arg === "--context") {
135073
+ const contextName = args[++i];
135074
+ if (!contextName || contextName.startsWith("-")) {
135075
+ console.error("Error: --context requires a Kubernetes context name.");
135076
+ process.exit(1);
135077
+ }
135078
+ options.k8sContext = contextName;
135079
+ } else if (arg === "--yes" || arg === "-y") {
135080
+ options.yes = true;
134442
135081
  } else if (arg === "--no-fail") {
134443
135082
  options.failOnError = false;
134444
135083
  } else if (arg === "--no-redact") {
@@ -134471,18 +135110,20 @@ function parseArgs(args) {
134471
135110
  }
134472
135111
  function printHelp() {
134473
135112
  console.log(`
134474
- Norn CLI - Run HTTP requests and test sequences from .norn files
135113
+ Norn CLI - Run HTTP requests, test sequences, and Kubernetes runbooks
134475
135114
 
134476
- Usage: norn <file.norn|directory> [options]
135115
+ Usage: norn <file.norn|file.nornk8s|directory> [options]
134477
135116
  norn secrets <command> [options]
134478
135117
 
134479
- When given a directory, recursively discovers and runs all test sequences
134480
- from .norn files within that directory and subdirectories.
135118
+ When given a directory, recursively discovers .norn test sequences and
135119
+ .nornk8s runbook sequences within that directory and subdirectories.
134481
135120
 
134482
135121
  Options:
134483
135122
  -s, --sequence <name> Run a specific sequence by name (single file only)
134484
135123
  -r, --request <name> Run a specific named request (single file only)
134485
135124
  -e, --env <name> Use environment from .nornenv (e.g., dev, prod)
135125
+ --context <name> Use a Kubernetes context for .nornk8s execution
135126
+ -y, --yes Allow restart commands in .nornk8s execution
134486
135127
  -t, --timeout <time> Request timeout override (e.g. 180s, 3m, 300000ms; default: norn.config.json or 30s)
134487
135128
  --insecure Disable TLS certificate verification (dev/self-signed only)
134488
135129
  -j, --json Output results as JSON (for CI/CD)
@@ -134540,6 +135181,8 @@ Examples:
134540
135181
  norn api-tests.norn --html report.html # Generate HTML report (explicit)
134541
135182
  norn api-tests.norn --insecure # Allow self-signed/local TLS certs
134542
135183
  norn api-tests.norn --no-redact # Show all data (no redaction)
135184
+ norn triage.nornk8s --context docker-desktop
135185
+ norn triage.nornk8s -s Triage --context prelive --yes
134543
135186
  norn .nornenv --refactor-region-pattern # Print refactored .nornenv
134544
135187
  norn .nornenv --refactor-region-pattern --write
134545
135188
  norn secrets keygen --name team-main # Generate shared key and cache locally
@@ -134596,6 +135239,109 @@ function runNornenvRegionRefactor(filePath, options) {
134596
135239
  }
134597
135240
  process.exit(0);
134598
135241
  }
135242
+ function redactK8sResult(result, redaction) {
135243
+ return {
135244
+ ...result,
135245
+ errors: result.errors.map((error2) => redactString(error2, redaction)),
135246
+ commands: result.commands.map((command) => ({
135247
+ ...command,
135248
+ args: command.args.map((arg) => redactString(arg, redaction)),
135249
+ stdout: redactString(command.stdout, redaction),
135250
+ stderr: redactString(command.stderr, redaction)
135251
+ }))
135252
+ };
135253
+ }
135254
+ function writeK8sProcessOutput(text, stream4) {
135255
+ if (!text) {
135256
+ return;
135257
+ }
135258
+ stream4.write(text);
135259
+ if (!text.endsWith("\n")) {
135260
+ stream4.write("\n");
135261
+ }
135262
+ }
135263
+ async function runK8sCliFile(filePath, options, colors) {
135264
+ if (options.request) {
135265
+ console.error("Error: --request is not supported for .nornk8s files");
135266
+ return false;
135267
+ }
135268
+ if (options.junitOutput || options.htmlOutput || options.outputDir) {
135269
+ console.error(colors.warning("Warning: reports are not supported for .nornk8s files in the MVP."));
135270
+ }
135271
+ const secretUnlockResult = await ensureCliSecretsUnlocked(filePath);
135272
+ if (!secretUnlockResult.ok) {
135273
+ if (secretUnlockResult.errors.length > 0) {
135274
+ printSecretResolutionErrors(secretUnlockResult.errors, secretUnlockResult.envFilePath);
135275
+ } else {
135276
+ console.error("Unable to unlock encrypted secrets.");
135277
+ }
135278
+ return false;
135279
+ }
135280
+ const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
135281
+ if (resolvedEnv.envNotFound) {
135282
+ console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
135283
+ console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(", ") || "none"}`);
135284
+ return false;
135285
+ }
135286
+ if (resolvedEnv.secretErrors.length > 0) {
135287
+ for (const error2 of resolvedEnv.secretErrors) {
135288
+ console.error(`Error: ${error2}`);
135289
+ }
135290
+ return false;
135291
+ }
135292
+ if (!resolvedEnv.envFilePath && options.env) {
135293
+ console.error(colors.warning("Warning: --env specified but no .nornenv file found"));
135294
+ } else if (resolvedEnv.envFilePath && options.env && options.verbose) {
135295
+ console.log(colors.info(`Using environment: ${options.env}`));
135296
+ }
135297
+ const redaction = createRedactionOptions(resolvedEnv.secretNames, resolvedEnv.secretValues, !options.noRedact);
135298
+ const fileContent = fs19.readFileSync(filePath, "utf-8");
135299
+ const parsed = parseK8sDocument(fileContent);
135300
+ const variables = attachEnvironmentScope({ ...resolvedEnv.variables }, resolvedEnv.variables);
135301
+ const executionOptions = {
135302
+ variables,
135303
+ selectedContext: options.k8sContext,
135304
+ workingDirectory: path19.dirname(filePath),
135305
+ allowRestart: options.yes,
135306
+ onCommand: options.output === "pretty" ? (command) => console.log(colors.dim(`$ ${redactString(command, redaction)}`)) : void 0
135307
+ };
135308
+ let results;
135309
+ if (options.sequence) {
135310
+ results = [await runK8sSequence(fileContent, options.sequence, executionOptions)];
135311
+ } else {
135312
+ results = await runK8sRunbooks(fileContent, executionOptions);
135313
+ if (results.length === 0) {
135314
+ if (options.output === "json") {
135315
+ console.log(JSON.stringify({ success: true, results: [] }, null, 2));
135316
+ } else {
135317
+ const sequenceCount = parsed.sequences.length;
135318
+ console.log(colors.info(
135319
+ sequenceCount > 0 ? 'No Kubernetes runbooks found. Use "runbook sequence" to mark sequences for CLI execution.' : "No Kubernetes sequences found."
135320
+ ));
135321
+ }
135322
+ return true;
135323
+ }
135324
+ }
135325
+ const redactedResults = results.map((result) => redactK8sResult(result, redaction));
135326
+ const success2 = results.every((result) => result.success);
135327
+ if (options.output === "json") {
135328
+ console.log(JSON.stringify({ success: success2, results: redactedResults }, null, 2));
135329
+ } else {
135330
+ for (const result of redactedResults) {
135331
+ for (const command of result.commands) {
135332
+ writeK8sProcessOutput(command.stdout, process.stdout);
135333
+ writeK8sProcessOutput(command.stderr, process.stderr);
135334
+ }
135335
+ const status = result.success ? colors.checkmark : colors.cross;
135336
+ console.log(`${status} ${result.name} (${formatDuration(result.duration)})`);
135337
+ for (const error2 of result.errors) {
135338
+ const restartHint = error2.includes("Restart requires confirmation") ? `${error2}. Re-run with --yes to allow mutations.` : error2;
135339
+ console.error(` ${colors.bullet} ${colors.error(restartHint)}`);
135340
+ }
135341
+ }
135342
+ }
135343
+ return success2;
135344
+ }
134599
135345
  async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions, filePath, envContext) {
134600
135346
  const lines = fileContent.split("\n");
134601
135347
  const requestLines = [];
@@ -134643,6 +135389,24 @@ function discoverNornFiles(dirPath) {
134643
135389
  walkDir(dirPath);
134644
135390
  return files.sort();
134645
135391
  }
135392
+ function discoverNornK8sFiles(dirPath) {
135393
+ const files = [];
135394
+ function walkDir(currentPath) {
135395
+ const entries = fs19.readdirSync(currentPath, { withFileTypes: true });
135396
+ for (const entry of entries) {
135397
+ const fullPath = path19.join(currentPath, entry.name);
135398
+ if (entry.isDirectory()) {
135399
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
135400
+ walkDir(fullPath);
135401
+ }
135402
+ } else if (entry.isFile() && entry.name.endsWith(".nornk8s")) {
135403
+ files.push(fullPath);
135404
+ }
135405
+ }
135406
+ }
135407
+ walkDir(dirPath);
135408
+ return files.sort();
135409
+ }
134646
135410
  function countTestSequences(fileContent, tagFilterOptions) {
134647
135411
  const allSequences = extractSequences(fileContent);
134648
135412
  const testSequences = allSequences.filter((seq) => seq.isTest);
@@ -134771,15 +135535,20 @@ async function main() {
134771
135535
  }
134772
135536
  runNornenvRegionRefactor(inputPath, options);
134773
135537
  }
135538
+ if (!isDirectory && path19.extname(inputPath).toLowerCase() === ".nornk8s") {
135539
+ const success2 = await runK8sCliFile(inputPath, options, colors);
135540
+ process.exit(success2 || !options.failOnError ? 0 : 1);
135541
+ }
135542
+ const k8sFilesToRun = isDirectory ? discoverNornK8sFiles(inputPath) : [];
134774
135543
  let filesToRun;
134775
135544
  if (isDirectory) {
134776
135545
  filesToRun = discoverNornFiles(inputPath);
134777
- if (filesToRun.length === 0) {
134778
- console.log(colors.info(`No .norn files found in ${inputPath}`));
135546
+ if (filesToRun.length === 0 && k8sFilesToRun.length === 0) {
135547
+ console.log(colors.info(`No .norn or .nornk8s files found in ${inputPath}`));
134779
135548
  process.exit(0);
134780
135549
  }
134781
135550
  if (options.verbose) {
134782
- console.log(colors.info(`Discovered ${filesToRun.length} .norn file(s) in ${inputPath}`));
135551
+ console.log(colors.info(`Discovered ${filesToRun.length} .norn and ${k8sFilesToRun.length} .nornk8s file(s) in ${inputPath}`));
134783
135552
  }
134784
135553
  } else {
134785
135554
  filesToRun = [inputPath];
@@ -134806,6 +135575,22 @@ async function main() {
134806
135575
  console.error("Error: --sequence and --request flags require a specific file, not a directory");
134807
135576
  process.exit(1);
134808
135577
  }
135578
+ if (isDirectory && k8sFilesToRun.length > 0 && options.output === "json") {
135579
+ console.error("Error: --json is not supported for directory runs containing .nornk8s files in the MVP");
135580
+ process.exit(1);
135581
+ }
135582
+ for (const filePath of k8sFilesToRun) {
135583
+ if (options.output !== "json") {
135584
+ console.log(colors.info(`
135585
+ \u2501\u2501\u2501 ${path19.relative(inputPath, filePath)} \u2501\u2501\u2501`));
135586
+ }
135587
+ if (!await runK8sCliFile(filePath, options, colors)) {
135588
+ overallSuccess = false;
135589
+ }
135590
+ }
135591
+ if (filesToRun.length === 0) {
135592
+ process.exit(overallSuccess || !options.failOnError ? 0 : 1);
135593
+ }
134809
135594
  if (options.sequence || options.request) {
134810
135595
  const filePath = filesToRun[0];
134811
135596
  applyCliRequestTimeout(filePath, options, colors);
@@ -135000,6 +135785,9 @@ ${fileContent}` : fileContent;
135000
135785
  filteredTestCount += counts.filtered;
135001
135786
  }
135002
135787
  if (filteredTestCount === 0) {
135788
+ if (k8sFilesToRun.length > 0 && totalTestCount === 0) {
135789
+ process.exit(overallSuccess || !options.failOnError ? 0 : 1);
135790
+ }
135003
135791
  if (totalTestCount > 0 && tagFilterOptions) {
135004
135792
  console.log(colors.info(`No test sequences match the tag filter (${totalTestCount} total test sequences found)`));
135005
135793
  } else if (totalTestCount === 0) {