@kici-dev/compiler 0.1.21 → 0.1.23

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.
Files changed (48) hide show
  1. package/dist/cli.js +25 -7
  2. package/dist/commands/compile.d.ts +6 -0
  3. package/dist/commands/compile.js +6 -3
  4. package/dist/commands/docs.d.ts +8 -8
  5. package/dist/commands/docs.js +35 -16
  6. package/dist/commands/held-run-client.d.ts +7 -2
  7. package/dist/commands/held-run-client.js +9 -3
  8. package/dist/commands/held-run-resolve.d.ts +5 -0
  9. package/dist/commands/login.d.ts +2 -0
  10. package/dist/commands/login.js +15 -7
  11. package/dist/commands/org.js +2 -2
  12. package/dist/commands/run-hold-watch.d.ts +57 -0
  13. package/dist/commands/run-hold-watch.js +87 -0
  14. package/dist/commands/run.d.ts +23 -0
  15. package/dist/commands/run.js +150 -17
  16. package/dist/commands/test.d.ts +14 -0
  17. package/dist/commands/types.d.ts +2 -0
  18. package/dist/commands/types.js +1 -1
  19. package/dist/fixtures/describe-event.d.ts +6 -0
  20. package/dist/fixtures/describe-event.js +18 -0
  21. package/dist/fixtures/picker.d.ts +19 -0
  22. package/dist/fixtures/picker.js +64 -0
  23. package/dist/llm-context/llms-architecture.txt +1440 -0
  24. package/dist/llm-context/llms-cli.txt +2386 -0
  25. package/dist/llm-context/llms-features.txt +2389 -0
  26. package/dist/llm-context/llms-full.txt +1304 -349
  27. package/dist/llm-context/llms-getting-started.txt +519 -0
  28. package/dist/llm-context/llms-patterns.txt +1324 -0
  29. package/dist/llm-context/llms-providers.txt +805 -0
  30. package/dist/llm-context/llms-sdk.txt +3725 -0
  31. package/dist/llm-context/llms.txt +15 -1
  32. package/dist/local-executor/index.js +40 -3
  33. package/dist/local-executor/job-runner.d.ts +2 -0
  34. package/dist/local-executor/job-runner.js +37 -4
  35. package/dist/local-executor/types.d.ts +2 -0
  36. package/dist/lockfile/generator.js +46 -20
  37. package/dist/remote/config.d.ts +2 -0
  38. package/dist/remote/config.js +1 -0
  39. package/dist/remote/platform-client.d.ts +12 -1
  40. package/dist/remote/uploader.js +1 -0
  41. package/dist/templates/package-json.js +1 -1
  42. package/dist/test-runner/rule-evaluator.d.ts +1 -1
  43. package/dist/test-runner/rule-evaluator.js +2 -1
  44. package/dist/test-runner/step-context.d.ts +1 -1
  45. package/dist/test-runner/step-context.js +8 -2
  46. package/dist/types.d.ts +15 -6
  47. package/package.json +4 -4
  48. package/sbom.spdx.json +35 -35
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { realpathSync } from "node:fs";
7
7
  import { Argument, Command, Option } from "commander";
8
8
  import pc from "picocolors";
9
9
  //#region src/cli.ts
10
- const version = "0.1.21";
10
+ const version = "0.1.23";
11
11
  /**
12
12
  * Build the kici Commander program with every command registered. Exported so
13
13
  * the surface registry can walk the real command tree without parsing argv (no
@@ -44,7 +44,7 @@ function buildProgram() {
44
44
  await fixtureCommand(event, options);
45
45
  });
46
46
  const runCommand = program.command("run").description("Execute workflows locally or remotely");
47
- runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).action(async (event, options) => {
47
+ runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--input <KEY=VALUE>", "Typed workflow-dispatch input (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).action(async (event, options) => {
48
48
  if (options.pick && options.workflow) {
49
49
  console.error("Error: --pick is mutually exclusive with --workflow.");
50
50
  process.exit(2);
@@ -78,6 +78,7 @@ function buildProgram() {
78
78
  keepGoing: options.keepGoing,
79
79
  container: options.container,
80
80
  env: options.env,
81
+ inputs: options.input,
81
82
  quiet: options.quiet,
82
83
  json: options.json,
83
84
  junit: options.junit,
@@ -89,7 +90,19 @@ function buildProgram() {
89
90
  });
90
91
  process.exit(success ? 0 : 1);
91
92
  });
92
- runCommand.command("remote").argument("[fixture]", "Fixture name or glob pattern (omit to list available)").description("Execute fixtures remotely via orchestrator").option("--workflow <name>", "Run a specific workflow directly (bypass triggers)").option("--all", "Run all available fixtures", false).option("--parallel", "Run matching fixtures concurrently", false).option("--no-wait", "Fire and forget (print runIds, don't stream)").option("--quiet", "Suppress output except final result", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--history", "Show recent run history", false).option("--routing-key <key>", "Override routing key for this run").option("--org <id>", "Target organization (overrides the active org)").option("--orchestrator <name>", "Target orchestrator cluster (overrides the per-org default)").option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--context <ctx.key=value>", "Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable)", (val, prev) => [...prev, val], []).option("--env <KEY=VALUE>", "Provide a per-run secret (repeatable); uploaded encrypted to the orchestrator", (val, prev) => [...prev, val], []).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).action(async (fixture, options) => {
93
+ runCommand.command("remote").argument("[fixture]", "Fixture name or glob pattern (omit to list available)").description("Execute fixtures remotely via orchestrator").option("--workflow <name>", "Run a specific workflow directly (bypass triggers)").option("--all", "Run all available fixtures", false).option("-p, --pick", "Interactively pick fixtures to run", false).option("--parallel", "Run matching fixtures concurrently", false).option("--no-wait", "Fire and forget (print runIds, don't stream)").option("--quiet", "Suppress output except final result", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--history", "Show recent run history", false).option("--routing-key <key>", "Override routing key for this run").option("--org <id>", "Target organization (overrides the active org)").option("--orchestrator <name>", "Target orchestrator cluster (overrides the per-org default)").option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--context <ctx.key=value>", "Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable)", (val, prev) => [...prev, val], []).option("--env <KEY=VALUE>", "Provide a per-run secret (repeatable); uploaded encrypted to the orchestrator", (val, prev) => [...prev, val], []).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).option("--target <selector>", "Narrow runsOnAll jobs to hosts matching this label selector (repeatable, AND-combined)", (val, prev) => [...prev, val], []).option("--target-allow-empty", "A --target that narrows a runsOnAll job to zero hosts skips it instead of failing", false).option("--input <KEY=VALUE>", "Typed workflow-dispatch input (repeatable)", (val, prev) => [...prev, val], []).option("--approve-all, --yes", "Auto-approve every approval gate this run holds on (run-scoped; eligibility still enforced)", false).action(async (fixture, options) => {
94
+ if (options.pick && fixture) {
95
+ console.error("Error: --pick selects fixtures interactively; do not also pass a fixture name.");
96
+ process.exit(2);
97
+ }
98
+ if (options.pick && options.all) {
99
+ console.error("Error: --pick is mutually exclusive with --all.");
100
+ process.exit(2);
101
+ }
102
+ if (options.pick && options.workflow) {
103
+ console.error("Error: --pick is mutually exclusive with --workflow.");
104
+ process.exit(2);
105
+ }
93
106
  const { runRemoteCommand } = await import("./commands/index.js");
94
107
  const { resolveCheckMode } = await import("./commands/check-mode.js");
95
108
  let checkMode;
@@ -105,7 +118,11 @@ function buildProgram() {
105
118
  const success = await runRemoteCommand(fixture, {
106
119
  ...options,
107
120
  checkMode,
108
- envFlags: options.env
121
+ envFlags: options.env,
122
+ targets: options.target,
123
+ targetAllowEmpty: options.targetAllowEmpty,
124
+ approveAll: options.approveAll,
125
+ inputs: options.input
109
126
  });
110
127
  process.exit(success ? 0 : 1);
111
128
  });
@@ -127,7 +144,7 @@ function buildProgram() {
127
144
  const success = await hookInstallCommand({ git: options.git });
128
145
  process.exit(success ? 0 : 1);
129
146
  });
130
- program.command("login").description("Authenticate with KiCI via browser OAuth (default) or API key (--token)").option("--token <key>", "API key for direct authentication (legacy)").option("--device", "Force device authorization flow (for headless/SSH environments)").option("--platform-endpoint <url>", "Platform relay URL").option("--routing-key <key>", "Routing key for webhook source identification").addHelpText("after", `
147
+ program.command("login").description("Authenticate with KiCI via browser OAuth (default) or API key (--token)").option("--token <key>", "API key for direct authentication (legacy)").option("--device", "Force device authorization flow (for headless/SSH environments)").option("--platform-endpoint <url>", "Platform relay URL").option("--oidc-issuer <url>", "OIDC issuer URL (defaults to the hosted KiCI IdP unless a flag/env selects another)").option("--routing-key <key>", "Routing key for webhook source identification").addHelpText("after", `
131
148
  Environment variables:
132
149
  KICI_BROWSER_CMD Custom browser command (use {url} placeholder, or 'none' to suppress)
133
150
  KICI_CALLBACK_PORT Fixed port for OAuth PKCE callback server (default: random)
@@ -140,6 +157,7 @@ Environment variables:
140
157
  token: options.token,
141
158
  device: options.device,
142
159
  platformEndpoint: options.platformEndpoint,
160
+ oidcIssuer: options.oidcIssuer,
143
161
  routingKey: options.routingKey
144
162
  });
145
163
  process.exit(success ? 0 : 1);
@@ -264,10 +282,10 @@ Environment variables:
264
282
  const { docsCommand } = await import("./commands/index.js");
265
283
  const success = await docsCommand({ open: options.open });
266
284
  process.exit(success ? 0 : 1);
267
- }).command("llm").description("Print the bundled LLM context (llms-full.txt) to stdout").option("--index", "Print the curated llms.txt index instead of the full bundle", false).option("--out <path>", "Write the bundle to a file instead of stdout").action(async (options) => {
285
+ }).command("llm [topic]").description("Print KiCI LLM docs bundles. No topic prints the llms.txt index; <topic> prints a task bundle (e.g. sdk, cli, patterns, features, providers, architecture, getting-started); \"full\" prints the complete bundle.").option("--out <path>", "Write the bundle to a file instead of stdout").action(async (topic, options) => {
268
286
  const { docsLlmCommand } = await import("./commands/index.js");
269
287
  const success = await docsLlmCommand({
270
- index: options.index,
288
+ topic,
271
289
  out: options.out
272
290
  });
273
291
  process.exit(success ? 0 : 1);
@@ -6,6 +6,12 @@ export interface CompileOptions {
6
6
  check: boolean;
7
7
  /** Verbose output */
8
8
  verbose: boolean;
9
+ /**
10
+ * Suppress the success line on stdout (and the auto-types success line) so a
11
+ * caller emitting machine-readable output keeps stdout pure. Validation
12
+ * errors are still reported.
13
+ */
14
+ quiet?: boolean;
9
15
  }
10
16
  /**
11
17
  * Execute the compile command.
@@ -76,7 +76,7 @@ async function compileCommand(options) {
76
76
  const lockJson = serializeLockFile(generateLockFile(workflowsWithSource));
77
77
  if (!options.check) {
78
78
  await fs.writeFile(lockPath, lockJson, "utf-8");
79
- logger.info(pc.green("✓") + ` Compiled workflows → .kici/kici.lock.json` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
79
+ if (!options.quiet) logger.info(pc.green("✓") + ` Compiled workflows → .kici/kici.lock.json` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
80
80
  try {
81
81
  const { loadGlobalConfig } = await import("../remote/config.js");
82
82
  const config = await loadGlobalConfig();
@@ -84,12 +84,15 @@ async function compileCommand(options) {
84
84
  const hasEndpoint = Boolean(config.platformEndpoint ?? config.endpoint);
85
85
  if (hasToken && hasEndpoint && config.activeOrgId) {
86
86
  const { typesCommand } = await import("./types.js");
87
- await typesCommand({ kiciDir });
87
+ await typesCommand({
88
+ kiciDir,
89
+ quiet: options.quiet
90
+ });
88
91
  }
89
92
  } catch {
90
93
  logger.warn(pc.yellow("Could not refresh types (Platform unreachable). Compilation succeeded."));
91
94
  }
92
- } else logger.info(pc.green("✓") + ` Workflows are valid` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
95
+ } else if (!options.quiet) logger.info(pc.green("✓") + ` Workflows are valid` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
93
96
  return true;
94
97
  } catch (error) {
95
98
  if (isCompilerError(error)) logger.error(formatError(error));
@@ -3,11 +3,11 @@ export interface DocsOptions {
3
3
  open?: boolean;
4
4
  }
5
5
  export interface DocsLlmOptions {
6
- /** Print only the llms.txt index instead of the full markdown bundle. */
7
- index?: boolean;
6
+ /** Task bundle id to print. Undefined → the llms.txt index; 'full' llms-full.txt. */
7
+ topic?: string;
8
8
  /** Override the bundled output destination (overrides default stdout). */
9
9
  out?: string;
10
- /** Override the directory holding llms.txt + llms-full.txt (test seam). */
10
+ /** Override the directory holding the llms*.txt files (test seam). */
11
11
  bundleDir?: string;
12
12
  }
13
13
  /**
@@ -15,12 +15,12 @@ export interface DocsLlmOptions {
15
15
  */
16
16
  export declare function docsCommand(options?: DocsOptions): Promise<boolean>;
17
17
  /**
18
- * Print the bundled llms.txt or llms-full.txt content to stdout (or a file).
18
+ * Print a KiCI LLM docs bundle to stdout (or a file).
19
19
  *
20
- * The bundle is generated at build time by hack/postbuild.mjs and shipped at
21
- * dist/llm-context/{llms.txt,llms-full.txt}. Customer-facing LLM tools can
22
- * pipe `kici docs llm` straight into an Anthropic / OpenAI context buffer to
23
- * brief the agent on KiCI authoring conventions without an internet round-trip.
20
+ * No topic prints the llms.txt index (a router listing every task bundle).
21
+ * A topic prints that task bundle (e.g. `sdk`, `cli`, `patterns`); `full`
22
+ * prints the everything-bundle. Pipe straight into a coding agent's context
23
+ * to brief it on KiCI authoring without an internet round-trip.
24
24
  */
25
25
  export declare function docsLlmCommand(options?: DocsLlmOptions): Promise<boolean>;
26
26
  //# sourceMappingURL=docs.d.ts.map
@@ -2,12 +2,24 @@ import "../chunk-BTugEXQM.js";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import path from "node:path";
4
4
  import pc from "picocolors";
5
- import { readFile, writeFile } from "node:fs/promises";
5
+ import { readFile, readdir, writeFile } from "node:fs/promises";
6
6
  import { logger, toErrorMessage } from "@kici-dev/core";
7
7
  import open from "open";
8
8
  //#region src/commands/docs.ts
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const DOCS_HOME_URL = "https://kici.dev/docs/";
11
+ function bundleFilenameForTopic(topic) {
12
+ if (!topic) return "llms.txt";
13
+ if (topic === "full") return "llms-full.txt";
14
+ return `llms-${topic}.txt`;
15
+ }
16
+ async function availableTopics(bundleDir) {
17
+ try {
18
+ return [...(await readdir(bundleDir)).filter((n) => n.startsWith("llms-") && n.endsWith(".txt") && n !== "llms-full.txt").map((n) => n.slice(5, -4)).sort(), "full"];
19
+ } catch {
20
+ return ["full"];
21
+ }
22
+ }
11
23
  /**
12
24
  * Open the published documentation site in the user's default browser.
13
25
  */
@@ -27,32 +39,39 @@ async function docsCommand(options = {}) {
27
39
  }
28
40
  }
29
41
  /**
30
- * Print the bundled llms.txt or llms-full.txt content to stdout (or a file).
42
+ * Print a KiCI LLM docs bundle to stdout (or a file).
31
43
  *
32
- * The bundle is generated at build time by hack/postbuild.mjs and shipped at
33
- * dist/llm-context/{llms.txt,llms-full.txt}. Customer-facing LLM tools can
34
- * pipe `kici docs llm` straight into an Anthropic / OpenAI context buffer to
35
- * brief the agent on KiCI authoring conventions without an internet round-trip.
44
+ * No topic prints the llms.txt index (a router listing every task bundle).
45
+ * A topic prints that task bundle (e.g. `sdk`, `cli`, `patterns`); `full`
46
+ * prints the everything-bundle. Pipe straight into a coding agent's context
47
+ * to brief it on KiCI authoring without an internet round-trip.
36
48
  */
37
49
  async function docsLlmCommand(options = {}) {
38
- const filename = options.index ? "llms.txt" : "llms-full.txt";
50
+ const filename = bundleFilenameForTopic(options.topic);
39
51
  const bundleDir = options.bundleDir ?? path.join(__dirname, "..", "llm-context");
40
52
  const bundlePath = path.join(bundleDir, filename);
53
+ let content;
41
54
  try {
42
- const content = await readFile(bundlePath, "utf-8");
43
- if (options.out) {
44
- await writeFile(options.out, content, "utf-8");
45
- logger.info(pc.gray(`Wrote ${filename} to ${options.out}`));
46
- return true;
47
- }
48
- process.stdout.write(content);
49
- if (!content.endsWith("\n")) process.stdout.write("\n");
50
- return true;
55
+ content = await readFile(bundlePath, "utf-8");
51
56
  } catch (error) {
57
+ if (options.topic && options.topic !== "full") {
58
+ const topics = await availableTopics(bundleDir);
59
+ logger.error(pc.red(`Unknown docs bundle "${options.topic}".`));
60
+ logger.info(pc.gray(`Available topics: ${topics.join(", ")} (no topic prints the index).`));
61
+ return false;
62
+ }
52
63
  logger.error(pc.red(`Error reading bundled ${filename}: ${toErrorMessage(error)}`));
53
64
  logger.info(pc.gray("The bundle ships with the @kici-dev/compiler package. If you built the package locally, run `pnpm build` in packages/compiler/."));
54
65
  return false;
55
66
  }
67
+ if (options.out) {
68
+ await writeFile(options.out, content, "utf-8");
69
+ logger.info(pc.gray(`Wrote ${filename} to ${options.out}`));
70
+ return true;
71
+ }
72
+ process.stdout.write(content);
73
+ if (!content.endsWith("\n")) process.stdout.write("\n");
74
+ return true;
56
75
  }
57
76
  //#endregion
58
77
  export { docsCommand, docsLlmCommand };
@@ -19,8 +19,13 @@ export interface HeldRunContext {
19
19
  export declare function resolveHeldRunContext(): Promise<HeldRunContext | null>;
20
20
  /** List the held runs for a single run id. */
21
21
  export declare function listHeldRunsForRun(ctx: HeldRunContext, runId: string): Promise<HeldRunSummary[]>;
22
- /** POST an approve decision for a held run. Returns true on success. */
23
- export declare function postApprove(ctx: HeldRunContext, heldRunId: string): Promise<boolean>;
22
+ /**
23
+ * POST an approve decision for a held run. Returns true on success. When
24
+ * `autoApprove` is set, marks the approval as a `kici run --approve-all`
25
+ * breakglass so the orchestrator audits it as `held_run.auto_approve`
26
+ * (eligibility is still enforced server-side — never a bypass).
27
+ */
28
+ export declare function postApprove(ctx: HeldRunContext, heldRunId: string, autoApprove?: boolean): Promise<boolean>;
24
29
  /** POST a reject decision (with reason) for a held run. Returns true on success. */
25
30
  export declare function postReject(ctx: HeldRunContext, heldRunId: string, reason: string): Promise<boolean>;
26
31
  //# sourceMappingURL=held-run-client.d.ts.map
@@ -52,9 +52,15 @@ async function listHeldRunsForRun(ctx, runId) {
52
52
  if (!response.ok) throw new Error(await describeError(response));
53
53
  return (await response.json()).heldRuns ?? [];
54
54
  }
55
- /** POST an approve decision for a held run. Returns true on success. */
56
- async function postApprove(ctx, heldRunId) {
57
- const url = `${ctx.endpoint}/api/v1/orgs/${ctx.orgId}/held-runs/${heldRunId}/approve`;
55
+ /**
56
+ * POST an approve decision for a held run. Returns true on success. When
57
+ * `autoApprove` is set, marks the approval as a `kici run --approve-all`
58
+ * breakglass so the orchestrator audits it as `held_run.auto_approve`
59
+ * (eligibility is still enforced server-side — never a bypass).
60
+ */
61
+ async function postApprove(ctx, heldRunId, autoApprove = false) {
62
+ const query = autoApprove ? "?auto=1" : "";
63
+ const url = `${ctx.endpoint}/api/v1/orgs/${ctx.orgId}/held-runs/${heldRunId}/approve${query}`;
58
64
  const response = await fetch(url, {
59
65
  method: "POST",
60
66
  headers: authHeaders(ctx.token)
@@ -16,6 +16,11 @@ export interface HeldRunSummary {
16
16
  holdScope?: HeldRunScope;
17
17
  stepIndex?: number | null;
18
18
  status: string;
19
+ /** Computed drift payload for a `when: 'drift'` step hold; absent otherwise. */
20
+ payload?: {
21
+ summaryMarkdown: string;
22
+ drift?: unknown;
23
+ } | null;
19
24
  }
20
25
  /** Filters supplied on the command line. */
21
26
  export interface HeldRunFilter {
@@ -3,6 +3,8 @@ export interface LoginOptions {
3
3
  token?: string;
4
4
  /** Platform relay URL */
5
5
  platformEndpoint?: string;
6
+ /** OIDC issuer URL override */
7
+ oidcIssuer?: string;
6
8
  /** Routing key for webhook source identification */
7
9
  routingKey?: string;
8
10
  /** Force device authorization flow regardless of environment */
@@ -1,5 +1,5 @@
1
1
  import "../chunk-BTugEXQM.js";
2
- import { getConfigPath, mergeGlobalConfig } from "../remote/config.js";
2
+ import { getConfigPath, loadGlobalConfig, mergeGlobalConfig, saveGlobalConfig } from "../remote/config.js";
3
3
  import { deviceFlow, exchangeTokenForPat, pkceFlow } from "../remote/oauth.js";
4
4
  import "../remote/prod-defaults.js";
5
5
  import { isHeadless } from "../auth/headless-detect.js";
@@ -46,8 +46,9 @@ function checkPatExpiry(expiresAt) {
46
46
  * 4. Save PAT to global config
47
47
  */
48
48
  async function oauthLogin(options) {
49
+ const existing = await loadGlobalConfig();
49
50
  const platformUrl = options.platformEndpoint || process.env.KICI_PLATFORM_URL || "https://api.kici.dev";
50
- const issuer = process.env.KICI_OIDC_ISSUER || "https://auth.kici.dev/realms/kici-internal";
51
+ const issuer = options.oidcIssuer || process.env.KICI_OIDC_ISSUER || "https://auth.kici.dev/realms/kici-internal";
51
52
  const clientId = process.env.KICI_OIDC_CLIENT_ID || "kici-cli";
52
53
  console.log(pc.cyan("\n Step 1/4: Detecting environment..."));
53
54
  const browserCmdSet = !!process.env.KICI_BROWSER_CMD;
@@ -70,14 +71,21 @@ async function oauthLogin(options) {
70
71
  machineName
71
72
  });
72
73
  console.log(pc.cyan("\n Step 4/4: Saving credentials..."));
73
- const configUpdate = {
74
+ const endpointChanged = existing.platformEndpoint !== platformUrl;
75
+ const next = {
76
+ ...existing,
74
77
  pat: patResult.token,
75
78
  patId: patResult.id,
76
- patExpiresAt: patResult.expiresAt
79
+ patExpiresAt: patResult.expiresAt,
80
+ platformEndpoint: platformUrl,
81
+ oidcIssuer: issuer
77
82
  };
78
- if (options.platformEndpoint) configUpdate.platformEndpoint = options.platformEndpoint;
79
- if (options.routingKey) configUpdate.routingKey = options.routingKey;
80
- await mergeGlobalConfig(configUpdate);
83
+ if (options.routingKey) next.routingKey = options.routingKey;
84
+ if (endpointChanged) {
85
+ delete next.activeOrgId;
86
+ delete next.defaultClusters;
87
+ }
88
+ await saveGlobalConfig(next);
81
89
  const configPath = getConfigPath();
82
90
  const expiryDate = new Date(patResult.expiresAt).toLocaleDateString();
83
91
  console.log(pc.green(`\n Authenticated successfully!`));
@@ -65,9 +65,9 @@ async function orgListCommand() {
65
65
  const isActive = org.id === config.activeOrgId;
66
66
  const marker = isActive ? pc.green("* ") : " ";
67
67
  const name = isActive ? pc.bold(org.displayName) : org.displayName;
68
- const role = pc.gray(`(${org.role})`);
68
+ const ownership = pc.gray(`(${org.isOwner ? "owner" : "member"})`);
69
69
  const id = pc.gray(org.id);
70
- console.log(` ${marker}${name.padEnd(nameWidth)} ${role} ${id}`);
70
+ console.log(` ${marker}${name.padEnd(nameWidth)} ${ownership} ${id}`);
71
71
  }
72
72
  console.log("");
73
73
  return true;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Hold-visibility + inline approve/reject for `kici run remote`.
3
+ *
4
+ * While a run is being watched, the orchestrator may pause it on an approval
5
+ * gate (a `when: 'always'` step/job/workflow gate, or a `when: 'drift'` step
6
+ * gate that carries a computed-drift payload). The poll loop calls
7
+ * `handleNewHolds` each tick with the run's pending holds. For each hold not
8
+ * seen before it prints the payload (if any) and, in a TTY, prompts the
9
+ * operator to approve/reject inline — reusing the same Platform held-run HTTP
10
+ * path as `kici approve` / `kici reject`. In a non-TTY it prints guidance and
11
+ * keeps polling (the run stays held until resolved out-of-band).
12
+ */
13
+ import type { HeldRunSummary } from './held-run-resolve.js';
14
+ import { type HeldRunContext } from './held-run-client.js';
15
+ /** A yes/no prompt; injected so the poll branch is unit-testable. */
16
+ export type ConfirmPrompt = (message: string) => Promise<boolean>;
17
+ /** What to do with a newly-observed hold. Pure — no IO. */
18
+ export type HoldAction = {
19
+ kind: 'prompt';
20
+ hold: HeldRunSummary;
21
+ } | {
22
+ kind: 'notify';
23
+ hold: HeldRunSummary;
24
+ };
25
+ /**
26
+ * Classify the holds observed this tick against the set already seen. Returns
27
+ * one action per genuinely-new pending hold (TTY ⇒ prompt, non-TTY ⇒ notify),
28
+ * and the updated seen-set. Pure: the caller performs the IO.
29
+ */
30
+ export declare function classifyNewHolds(holds: readonly HeldRunSummary[], seen: Set<string>, isTty: boolean): {
31
+ actions: HoldAction[];
32
+ seen: Set<string>;
33
+ };
34
+ /**
35
+ * Process the new holds observed this tick: print each, and in a TTY prompt the
36
+ * operator to approve/reject inline (posting the decision via the shared
37
+ * held-run HTTP path). Returns the updated seen-set. A resolved context is
38
+ * resolved lazily on first need and cached for the run.
39
+ */
40
+ export declare function handleNewHolds(args: {
41
+ holds: readonly HeldRunSummary[];
42
+ seen: Set<string>;
43
+ isTty: boolean;
44
+ confirm: ConfirmPrompt;
45
+ /**
46
+ * `--approve-all` breakglass: auto-approve each hold (run-scoped) instead of
47
+ * prompting, marking the approval as `auto_approve` so the orchestrator
48
+ * audits it distinctly. Eligibility is still enforced server-side.
49
+ */
50
+ approveAll?: boolean;
51
+ /** Override for tests; defaults to the shared Platform held-run context. */
52
+ resolveContext?: () => Promise<HeldRunContext | null>;
53
+ /** Override for tests. */
54
+ approve?: (ctx: HeldRunContext, heldRunId: string, autoApprove?: boolean) => Promise<boolean>;
55
+ reject?: (ctx: HeldRunContext, heldRunId: string, reason: string) => Promise<boolean>;
56
+ }): Promise<Set<string>>;
57
+ //# sourceMappingURL=run-hold-watch.d.ts.map
@@ -0,0 +1,87 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ import { postApprove, postReject, resolveHeldRunContext } from "./held-run-client.js";
3
+ import pc from "picocolors";
4
+ import { logger } from "@kici-dev/core";
5
+ //#region src/commands/run-hold-watch.ts
6
+ /**
7
+ * Hold-visibility + inline approve/reject for `kici run remote`.
8
+ *
9
+ * While a run is being watched, the orchestrator may pause it on an approval
10
+ * gate (a `when: 'always'` step/job/workflow gate, or a `when: 'drift'` step
11
+ * gate that carries a computed-drift payload). The poll loop calls
12
+ * `handleNewHolds` each tick with the run's pending holds. For each hold not
13
+ * seen before it prints the payload (if any) and, in a TTY, prompts the
14
+ * operator to approve/reject inline — reusing the same Platform held-run HTTP
15
+ * path as `kici approve` / `kici reject`. In a non-TTY it prints guidance and
16
+ * keeps polling (the run stays held until resolved out-of-band).
17
+ */
18
+ /**
19
+ * Classify the holds observed this tick against the set already seen. Returns
20
+ * one action per genuinely-new pending hold (TTY ⇒ prompt, non-TTY ⇒ notify),
21
+ * and the updated seen-set. Pure: the caller performs the IO.
22
+ */
23
+ function classifyNewHolds(holds, seen, isTty) {
24
+ const next = new Set(seen);
25
+ const actions = [];
26
+ for (const hold of holds) {
27
+ if (hold.status !== "pending") continue;
28
+ if (next.has(hold.id)) continue;
29
+ next.add(hold.id);
30
+ actions.push({
31
+ kind: isTty ? "prompt" : "notify",
32
+ hold
33
+ });
34
+ }
35
+ return {
36
+ actions,
37
+ seen: next
38
+ };
39
+ }
40
+ /** Print a hold's identity + its drift payload (when present) to stdout. */
41
+ function printHold(hold) {
42
+ const scope = hold.holdScope ?? "job";
43
+ const where = scope === "step" && hold.stepIndex != null ? `step #${hold.stepIndex}${hold.jobId ? ` of job '${hold.jobId}'` : ""}` : hold.jobId ? `job '${hold.jobId}'` : scope;
44
+ logger.info(pc.yellow(`\n[kici] Run held for approval (${where}).`));
45
+ if (hold.payload?.summaryMarkdown) {
46
+ logger.info(pc.dim("Computed drift — review before approving:"));
47
+ for (const line of hold.payload.summaryMarkdown.split("\n")) logger.info(pc.dim(` ${line}`));
48
+ }
49
+ }
50
+ /**
51
+ * Process the new holds observed this tick: print each, and in a TTY prompt the
52
+ * operator to approve/reject inline (posting the decision via the shared
53
+ * held-run HTTP path). Returns the updated seen-set. A resolved context is
54
+ * resolved lazily on first need and cached for the run.
55
+ */
56
+ async function handleNewHolds(args) {
57
+ const { actions, seen } = classifyNewHolds(args.holds, args.seen, args.isTty);
58
+ if (actions.length === 0) return seen;
59
+ const resolveCtx = args.resolveContext ?? resolveHeldRunContext;
60
+ const doApprove = args.approve ?? postApprove;
61
+ const doReject = args.reject ?? postReject;
62
+ let ctx = null;
63
+ if (args.approveAll) logger.info(pc.yellow("[kici] --approve-all: auto-approving every gate for this run (eligibility enforced)."));
64
+ for (const action of actions) {
65
+ printHold(action.hold);
66
+ ctx = ctx ?? await resolveCtx();
67
+ if (!ctx) {
68
+ logger.info(pc.dim(`Run held; approve via \`kici approve ${action.hold.runId}\`.`));
69
+ continue;
70
+ }
71
+ if (args.approveAll) {
72
+ if (await doApprove(ctx, action.hold.id, true)) logger.info(pc.green("[kici] Auto-approved."));
73
+ continue;
74
+ }
75
+ if (action.kind === "notify") {
76
+ logger.info(pc.dim(`Run held; approve via the dashboard or \`kici approve ${action.hold.runId}\`.`));
77
+ continue;
78
+ }
79
+ const approved = await args.confirm("Approve this gate?");
80
+ if (approved ? await doApprove(ctx, action.hold.id) : await doReject(ctx, action.hold.id, "rejected via kici run")) logger.info(approved ? pc.green("[kici] Approved.") : pc.red("[kici] Rejected."));
81
+ }
82
+ return seen;
83
+ }
84
+ //#endregion
85
+ export { classifyNewHolds, handleNewHolds };
86
+
87
+ //# sourceMappingURL=run-hold-watch.js.map
@@ -1,5 +1,28 @@
1
+ import { type HostTargetSelector, type InputsDescriptorMap } from '@kici-dev/engine';
1
2
  import type { RunLocalOptions } from '../local-executor/types.js';
2
3
  import type { RemoteRunOptions } from './test.js';
4
+ /**
5
+ * Compile `--target` selector strings into a {@link HostTargetSelector}. Each
6
+ * string becomes one AND value (its own include set); repeated values
7
+ * AND-combine. Returns undefined when no `--target` is given. Throws when
8
+ * `--target-allow-empty` is set without at least one `--target`.
9
+ */
10
+ export declare function buildTargetSelector(targets: string[] | undefined, allowEmpty: boolean): HostTargetSelector | undefined;
11
+ /**
12
+ * Look up the dispatch-trigger `inputs` descriptor for a workflow from a parsed
13
+ * inline lock file. When `workflowName` is given, only that workflow's dispatch
14
+ * triggers are considered; otherwise descriptors across all workflows are merged
15
+ * (best-effort fast-fail — the orchestrator re-validates against the matched
16
+ * workflow authoritatively). Returns undefined when no dispatch inputs declared.
17
+ */
18
+ export declare function lookupDispatchInputsDescriptor(inlineLockFile: string | undefined, workflowName: string | undefined): InputsDescriptorMap | undefined;
19
+ /**
20
+ * Validate raw `--input KEY=VALUE` pairs against the (optional) lock descriptor
21
+ * and return the raw operator pairs verbatim. The CLI fast-fails on malformed /
22
+ * invalid input for UX, but forwards the **raw** strings — the orchestrator is
23
+ * authoritative and applies coercion + defaults exactly once.
24
+ */
25
+ export declare function buildDispatchInputs(pairs: string[], descriptor: InputsDescriptorMap | undefined): Record<string, string>;
3
26
  /**
4
27
  * Run a workflow locally using the local executor.
5
28
  * Thin wrapper that delegates to executeLocal from local-executor.