@kici-dev/compiler 0.1.20 → 0.1.22
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.d.ts +14 -0
- package/dist/cli.js +58 -7
- package/dist/commands/check-mode.d.ts +19 -0
- package/dist/commands/check-mode.js +21 -0
- package/dist/commands/compile.js +1 -1
- package/dist/commands/held-run-client.d.ts +7 -2
- package/dist/commands/held-run-client.js +9 -3
- package/dist/commands/held-run-resolve.d.ts +5 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +15 -7
- package/dist/commands/run-hold-watch.d.ts +57 -0
- package/dist/commands/run-hold-watch.js +87 -0
- package/dist/commands/run.d.ts +8 -0
- package/dist/commands/run.js +67 -4
- package/dist/commands/test.d.ts +17 -0
- package/dist/llm-context/llms-full.txt +643 -53
- package/dist/llm-context/llms.txt +3 -1
- package/dist/local-executor/index.js +15 -2
- package/dist/local-executor/job-runner.d.ts +3 -0
- package/dist/local-executor/job-runner.js +54 -5
- package/dist/local-executor/output-streamer.js +3 -1
- package/dist/local-executor/types.d.ts +7 -0
- package/dist/lockfile/generator.js +36 -17
- package/dist/remote/config.d.ts +2 -0
- package/dist/remote/config.js +1 -0
- package/dist/remote/platform-client.d.ts +12 -0
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/job-executor.d.ts +6 -1
- package/dist/test-runner/step-context.js +6 -1
- package/dist/types.d.ts +20 -6
- package/package.json +4 -4
- package/sbom.spdx.json +35 -35
package/dist/cli.d.ts
CHANGED
|
@@ -8,4 +8,18 @@ import { Command } from 'commander';
|
|
|
8
8
|
export declare function buildProgram(): Command;
|
|
9
9
|
/** Build the program and parse argv — the bin-shim entry point. */
|
|
10
10
|
export declare function runCli(argv?: string[]): void;
|
|
11
|
+
/**
|
|
12
|
+
* Decide whether this module is the process entry point, tolerating a
|
|
13
|
+
* symlinked `argv[1]`. A `node_modules/.bin/kici` entry is a symlink, and when
|
|
14
|
+
* it points at this compiled `cli.js` (the compiler package declares a `kici`
|
|
15
|
+
* bin), `process.argv[1]` is the symlink path while `import.meta.url` is the
|
|
16
|
+
* real file. A plain `resolve()` comparison sees two different paths and never
|
|
17
|
+
* matches, silently skipping `runCli()` — so `kici compile` (and every other
|
|
18
|
+
* subcommand) becomes a no-op when invoked through the bin symlink.
|
|
19
|
+
* Dereference both sides with `realpathSync` so a symlinked invocation is
|
|
20
|
+
* correctly recognised as the entry point. Falls back to a plain `resolve()`
|
|
21
|
+
* comparison when `argv[1]` doesn't resolve to a real file (e.g. a virtual
|
|
22
|
+
* entry point), preserving the previous behaviour for that edge case.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isMainEntryPoint(argv1: string | undefined, importMetaUrl: string): boolean;
|
|
11
25
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,11 @@ import "./chunk-BTugEXQM.js";
|
|
|
3
3
|
import { shouldSuppressBanner } from "./cli-banner.js";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
6
7
|
import { Argument, Command, Option } from "commander";
|
|
7
8
|
import pc from "picocolors";
|
|
8
9
|
//#region src/cli.ts
|
|
9
|
-
const version = "0.1.
|
|
10
|
+
const version = "0.1.22";
|
|
10
11
|
/**
|
|
11
12
|
* Build the kici Commander program with every command registered. Exported so
|
|
12
13
|
* the surface registry can walk the real command tree without parsing argv (no
|
|
@@ -43,7 +44,7 @@ function buildProgram() {
|
|
|
43
44
|
await fixtureCommand(event, options);
|
|
44
45
|
});
|
|
45
46
|
const runCommand = program.command("run").description("Execute workflows locally or remotely");
|
|
46
|
-
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).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("--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
48
|
if (options.pick && options.workflow) {
|
|
48
49
|
console.error("Error: --pick is mutually exclusive with --workflow.");
|
|
49
50
|
process.exit(2);
|
|
@@ -53,8 +54,20 @@ function buildProgram() {
|
|
|
53
54
|
process.exit(2);
|
|
54
55
|
}
|
|
55
56
|
const { runLocalCommand } = await import("./commands/index.js");
|
|
57
|
+
const { resolveCheckMode } = await import("./commands/check-mode.js");
|
|
58
|
+
let checkMode;
|
|
59
|
+
try {
|
|
60
|
+
checkMode = resolveCheckMode({
|
|
61
|
+
check: options.check,
|
|
62
|
+
failOnDrift: options.failOnDrift
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
56
68
|
const success = await runLocalCommand({
|
|
57
69
|
event,
|
|
70
|
+
checkMode,
|
|
58
71
|
pick: options.pick,
|
|
59
72
|
workflow: options.workflow,
|
|
60
73
|
job: options.job,
|
|
@@ -76,11 +89,26 @@ function buildProgram() {
|
|
|
76
89
|
});
|
|
77
90
|
process.exit(success ? 0 : 1);
|
|
78
91
|
});
|
|
79
|
-
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], []).action(async (fixture, options) => {
|
|
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).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("--approve-all, --yes", "Auto-approve every approval gate this run holds on (run-scoped; eligibility still enforced)", false).action(async (fixture, options) => {
|
|
80
93
|
const { runRemoteCommand } = await import("./commands/index.js");
|
|
94
|
+
const { resolveCheckMode } = await import("./commands/check-mode.js");
|
|
95
|
+
let checkMode;
|
|
96
|
+
try {
|
|
97
|
+
checkMode = resolveCheckMode({
|
|
98
|
+
check: options.check,
|
|
99
|
+
failOnDrift: options.failOnDrift
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
81
105
|
const success = await runRemoteCommand(fixture, {
|
|
82
106
|
...options,
|
|
83
|
-
|
|
107
|
+
checkMode,
|
|
108
|
+
envFlags: options.env,
|
|
109
|
+
targets: options.target,
|
|
110
|
+
targetAllowEmpty: options.targetAllowEmpty,
|
|
111
|
+
approveAll: options.approveAll
|
|
84
112
|
});
|
|
85
113
|
process.exit(success ? 0 : 1);
|
|
86
114
|
});
|
|
@@ -102,7 +130,7 @@ function buildProgram() {
|
|
|
102
130
|
const success = await hookInstallCommand({ git: options.git });
|
|
103
131
|
process.exit(success ? 0 : 1);
|
|
104
132
|
});
|
|
105
|
-
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", `
|
|
133
|
+
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", `
|
|
106
134
|
Environment variables:
|
|
107
135
|
KICI_BROWSER_CMD Custom browser command (use {url} placeholder, or 'none' to suppress)
|
|
108
136
|
KICI_CALLBACK_PORT Fixed port for OAuth PKCE callback server (default: random)
|
|
@@ -115,6 +143,7 @@ Environment variables:
|
|
|
115
143
|
token: options.token,
|
|
116
144
|
device: options.device,
|
|
117
145
|
platformEndpoint: options.platformEndpoint,
|
|
146
|
+
oidcIssuer: options.oidcIssuer,
|
|
118
147
|
routingKey: options.routingKey
|
|
119
148
|
});
|
|
120
149
|
process.exit(success ? 0 : 1);
|
|
@@ -263,8 +292,30 @@ Environment variables:
|
|
|
263
292
|
function runCli(argv = process.argv) {
|
|
264
293
|
buildProgram().parse(argv);
|
|
265
294
|
}
|
|
266
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Decide whether this module is the process entry point, tolerating a
|
|
297
|
+
* symlinked `argv[1]`. A `node_modules/.bin/kici` entry is a symlink, and when
|
|
298
|
+
* it points at this compiled `cli.js` (the compiler package declares a `kici`
|
|
299
|
+
* bin), `process.argv[1]` is the symlink path while `import.meta.url` is the
|
|
300
|
+
* real file. A plain `resolve()` comparison sees two different paths and never
|
|
301
|
+
* matches, silently skipping `runCli()` — so `kici compile` (and every other
|
|
302
|
+
* subcommand) becomes a no-op when invoked through the bin symlink.
|
|
303
|
+
* Dereference both sides with `realpathSync` so a symlinked invocation is
|
|
304
|
+
* correctly recognised as the entry point. Falls back to a plain `resolve()`
|
|
305
|
+
* comparison when `argv[1]` doesn't resolve to a real file (e.g. a virtual
|
|
306
|
+
* entry point), preserving the previous behaviour for that edge case.
|
|
307
|
+
*/
|
|
308
|
+
function isMainEntryPoint(argv1, importMetaUrl) {
|
|
309
|
+
if (!argv1) return false;
|
|
310
|
+
const modulePath = fileURLToPath(importMetaUrl);
|
|
311
|
+
try {
|
|
312
|
+
return realpathSync(argv1) === realpathSync(modulePath);
|
|
313
|
+
} catch {
|
|
314
|
+
return resolve(argv1) === resolve(modulePath);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (isMainEntryPoint(process.argv[1], import.meta.url)) runCli();
|
|
267
318
|
//#endregion
|
|
268
|
-
export { buildProgram, runCli };
|
|
319
|
+
export { buildProgram, isMainEntryPoint, runCli };
|
|
269
320
|
|
|
270
321
|
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CheckMode } from '@kici-dev/engine';
|
|
2
|
+
/** The two run-mode flags shared by `kici run local` and `kici run remote`. */
|
|
3
|
+
export interface CheckModeFlags {
|
|
4
|
+
/** --check: report drift, change nothing. */
|
|
5
|
+
check?: boolean;
|
|
6
|
+
/** --fail-on-drift: in check mode, exit non-zero if any step reports drift. */
|
|
7
|
+
failOnDrift?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the run {@link CheckMode} from the `--check` / `--fail-on-drift` flags.
|
|
11
|
+
*
|
|
12
|
+
* - no flags -> `apply` (the unchanged default: converge).
|
|
13
|
+
* - `--check` -> `check` (report-only, changes nothing).
|
|
14
|
+
* - `--check --fail-on-drift` -> `check-fail-on-drift` (fails the run on drift).
|
|
15
|
+
*
|
|
16
|
+
* `--fail-on-drift` without `--check` is an error — it only modifies check mode.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveCheckMode(flags: CheckModeFlags): CheckMode;
|
|
19
|
+
//# sourceMappingURL=check-mode.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "../chunk-BTugEXQM.js";
|
|
2
|
+
import { CheckMode } from "@kici-dev/engine";
|
|
3
|
+
//#region src/commands/check-mode.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the run {@link CheckMode} from the `--check` / `--fail-on-drift` flags.
|
|
6
|
+
*
|
|
7
|
+
* - no flags -> `apply` (the unchanged default: converge).
|
|
8
|
+
* - `--check` -> `check` (report-only, changes nothing).
|
|
9
|
+
* - `--check --fail-on-drift` -> `check-fail-on-drift` (fails the run on drift).
|
|
10
|
+
*
|
|
11
|
+
* `--fail-on-drift` without `--check` is an error — it only modifies check mode.
|
|
12
|
+
*/
|
|
13
|
+
function resolveCheckMode(flags) {
|
|
14
|
+
if (flags.failOnDrift && !flags.check) throw new Error("--fail-on-drift requires --check");
|
|
15
|
+
if (flags.check) return flags.failOnDrift ? CheckMode.enum["check-fail-on-drift"] : CheckMode.enum.check;
|
|
16
|
+
return CheckMode.enum.apply;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { resolveCheckMode };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=check-mode.js.map
|
package/dist/commands/compile.js
CHANGED
|
@@ -8,8 +8,8 @@ import "../validation/index.js";
|
|
|
8
8
|
import { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile } from "../lockfile/generator.js";
|
|
9
9
|
import "../lockfile/index.js";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import pc from "picocolors";
|
|
12
11
|
import { existsSync } from "node:fs";
|
|
12
|
+
import pc from "picocolors";
|
|
13
13
|
import fs from "node:fs/promises";
|
|
14
14
|
import { logger, toErrorMessage } from "@kici-dev/core";
|
|
15
15
|
import { PackageManager, detectPackageManagerSync } from "@kici-dev/core/package-manager";
|
|
@@ -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
|
-
/**
|
|
23
|
-
|
|
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
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
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 {
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -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 */
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
|
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.
|
|
79
|
-
if (
|
|
80
|
-
|
|
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!`));
|
|
@@ -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
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import type { HostTargetSelector } 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;
|
|
3
11
|
/**
|
|
4
12
|
* Run a workflow locally using the local executor.
|
|
5
13
|
* Thin wrapper that delegates to executeLocal from local-executor.
|
package/dist/commands/run.js
CHANGED
|
@@ -10,10 +10,14 @@ import { formatJsonResult } from "../remote/output/json.js";
|
|
|
10
10
|
import { formatJunitResult } from "../remote/output/junit.js";
|
|
11
11
|
import { formatErrorHighlight, formatMultiFixtureSummary, formatSummary } from "../remote/output/summary.js";
|
|
12
12
|
import { compileFixtures, filterFixtures } from "../fixtures/compiler.js";
|
|
13
|
+
import { listHeldRunsForRun, resolveHeldRunContext } from "./held-run-client.js";
|
|
14
|
+
import { handleNewHolds } from "./run-hold-watch.js";
|
|
13
15
|
import path from "node:path";
|
|
14
16
|
import pc from "picocolors";
|
|
15
17
|
import { readFile, writeFile } from "node:fs/promises";
|
|
16
18
|
import { formatBytes, logger, toErrorMessage } from "@kici-dev/core";
|
|
19
|
+
import { normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
|
|
20
|
+
import { confirm } from "@inquirer/prompts";
|
|
17
21
|
//#region src/commands/run.ts
|
|
18
22
|
/** Terminal run statuses returned by the Platform run-status snapshot. */
|
|
19
23
|
const TERMINAL_STATUSES = new Set([
|
|
@@ -22,6 +26,22 @@ const TERMINAL_STATUSES = new Set([
|
|
|
22
26
|
"cancelled",
|
|
23
27
|
"error"
|
|
24
28
|
]);
|
|
29
|
+
/**
|
|
30
|
+
* Compile `--target` selector strings into a {@link HostTargetSelector}. Each
|
|
31
|
+
* string becomes one AND value (its own include set); repeated values
|
|
32
|
+
* AND-combine. Returns undefined when no `--target` is given. Throws when
|
|
33
|
+
* `--target-allow-empty` is set without at least one `--target`.
|
|
34
|
+
*/
|
|
35
|
+
function buildTargetSelector(targets, allowEmpty) {
|
|
36
|
+
if (!targets || targets.length === 0) {
|
|
37
|
+
if (allowEmpty) throw new Error("--target-allow-empty requires at least one --target selector");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
values: targets.map((t) => normalizeRunsOnToMatchers(t, "kici run --target")),
|
|
42
|
+
allowEmpty
|
|
43
|
+
};
|
|
44
|
+
}
|
|
25
45
|
/** Interval between status/log polls while a run is active. */
|
|
26
46
|
const POLL_INTERVAL_MS = 750;
|
|
27
47
|
/**
|
|
@@ -58,6 +78,7 @@ async function runRemoteCommand(fixture, options) {
|
|
|
58
78
|
if (!options.quiet) logger.info(pc.gray("Debug mode enabled"));
|
|
59
79
|
}
|
|
60
80
|
try {
|
|
81
|
+
buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
|
|
61
82
|
if (options.workflow && !fixture && !options.all) return await runDirectWorkflow(options.workflow, options);
|
|
62
83
|
const kiciDir = resolveKiciDir(options.kiciDir);
|
|
63
84
|
if (options.history) {
|
|
@@ -294,7 +315,12 @@ async function runSingleFixture(fixture, ctx, options, config, history) {
|
|
|
294
315
|
},
|
|
295
316
|
workflowName: opts.workflowName,
|
|
296
317
|
inlineLockFile: overlay.inlineLockFile,
|
|
297
|
-
fullRepo: true
|
|
318
|
+
fullRepo: true,
|
|
319
|
+
...options.checkMode && { checkMode: options.checkMode },
|
|
320
|
+
...(() => {
|
|
321
|
+
const target = buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
|
|
322
|
+
return target ? { target } : {};
|
|
323
|
+
})()
|
|
298
324
|
});
|
|
299
325
|
if (triggerResult.status === "rejected") {
|
|
300
326
|
if (!options.quiet) logger.info(pc.red(`Rejected: ${triggerResult.reason ?? "unknown reason"}`));
|
|
@@ -371,6 +397,8 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
|
371
397
|
process.on("SIGINT", cancelHandler);
|
|
372
398
|
try {
|
|
373
399
|
let lastStatus = null;
|
|
400
|
+
const seenHolds = /* @__PURE__ */ new Set();
|
|
401
|
+
let heldCtx;
|
|
374
402
|
while (!cancelled) {
|
|
375
403
|
const logs = await ctx.client.runLogs(ctx.orgId, runId, cursor, ctx.target);
|
|
376
404
|
for (const line of logs.lines) {
|
|
@@ -380,7 +408,9 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
|
380
408
|
}
|
|
381
409
|
cursor = logs.nextCursor;
|
|
382
410
|
lastStatus = await ctx.client.runStatus(ctx.orgId, runId, ctx.target);
|
|
383
|
-
|
|
411
|
+
const terminal = lastStatus.done || TERMINAL_STATUSES.has(lastStatus.status);
|
|
412
|
+
if (terminal && logs.done) return finishRun(fixtureId, runId, lastStatus, startTime, tailLines, options);
|
|
413
|
+
if (!terminal && !options.quiet) await watchRunHolds(runId, seenHolds, () => heldCtx, (c) => heldCtx = c, options);
|
|
384
414
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
385
415
|
}
|
|
386
416
|
return {
|
|
@@ -394,6 +424,34 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
|
394
424
|
process.removeListener("SIGINT", cancelHandler);
|
|
395
425
|
}
|
|
396
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Fetch this run's pending holds and surface them to the operator. The held-run
|
|
429
|
+
* context is resolved lazily (cached across ticks via the getter/setter). A
|
|
430
|
+
* resolution / fetch failure is swallowed — hold-visibility is best-effort and
|
|
431
|
+
* must never abort the run watch.
|
|
432
|
+
*/
|
|
433
|
+
async function watchRunHolds(runId, seenHolds, getCtx, setCtx, options) {
|
|
434
|
+
try {
|
|
435
|
+
let ctx = getCtx();
|
|
436
|
+
if (ctx === void 0) {
|
|
437
|
+
ctx = await resolveHeldRunContext();
|
|
438
|
+
setCtx(ctx);
|
|
439
|
+
}
|
|
440
|
+
if (!ctx) return;
|
|
441
|
+
const holds = await listHeldRunsForRun(ctx, runId);
|
|
442
|
+
if (holds.length === 0) return;
|
|
443
|
+
await handleNewHolds({
|
|
444
|
+
holds,
|
|
445
|
+
seen: seenHolds,
|
|
446
|
+
isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
447
|
+
approveAll: Boolean(options.approveAll),
|
|
448
|
+
confirm: (message) => confirm({
|
|
449
|
+
message,
|
|
450
|
+
default: false
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
} catch {}
|
|
454
|
+
}
|
|
397
455
|
/** Build the final result + render the summary table for a completed run. */
|
|
398
456
|
function finishRun(fixtureId, runId, status, startTime, tailLines, options) {
|
|
399
457
|
const durationMs = Date.now() - startTime;
|
|
@@ -481,7 +539,12 @@ async function runDirectWorkflow(workflowName, options) {
|
|
|
481
539
|
},
|
|
482
540
|
workflowName,
|
|
483
541
|
inlineLockFile: overlay.inlineLockFile,
|
|
484
|
-
fullRepo: true
|
|
542
|
+
fullRepo: true,
|
|
543
|
+
...options.checkMode && { checkMode: options.checkMode },
|
|
544
|
+
...(() => {
|
|
545
|
+
const target = buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
|
|
546
|
+
return target ? { target } : {};
|
|
547
|
+
})()
|
|
485
548
|
});
|
|
486
549
|
if (!options.quiet) logger.info(pc.green(`Run started: ${triggerResult.runId}`));
|
|
487
550
|
if (options.wait === false) return triggerResult.status === "accepted";
|
|
@@ -509,6 +572,6 @@ function displayRemoteResults(results) {
|
|
|
509
572
|
logger.info("");
|
|
510
573
|
}
|
|
511
574
|
//#endregion
|
|
512
|
-
export { runLocalCommand, runRemoteCommand };
|
|
575
|
+
export { buildTargetSelector, runLocalCommand, runRemoteCommand };
|
|
513
576
|
|
|
514
577
|
//# sourceMappingURL=run.js.map
|
package/dist/commands/test.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PayloadOptions } from '../test-runner/payload-builder.js';
|
|
2
|
+
import { type CheckMode } from '@kici-dev/engine';
|
|
2
3
|
/** Options for the kici test command (dry-run trigger preview) */
|
|
3
4
|
export interface TestOptions extends PayloadOptions {
|
|
4
5
|
/** Filter to specific workflow */
|
|
@@ -38,6 +39,22 @@ export interface RemoteRunOptions extends TestOptions {
|
|
|
38
39
|
org?: string;
|
|
39
40
|
/** Target orchestrator cluster name (overrides the per-org default). */
|
|
40
41
|
orchestrator?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Run mode resolved from --check / --fail-on-drift, threaded onto the dispatch
|
|
44
|
+
* payload so the orchestrator runs the agent step loop in the requested mode.
|
|
45
|
+
* Defaults to `apply`.
|
|
46
|
+
*/
|
|
47
|
+
checkMode?: CheckMode;
|
|
48
|
+
/** `--target <selector>` values (repeatable), AND-combined into host narrowing. */
|
|
49
|
+
targets?: string[];
|
|
50
|
+
/** `--target-allow-empty`: a target that zeroes a runsOnAll job skips it instead of failing. */
|
|
51
|
+
targetAllowEmpty?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* `--approve-all` (alias `--yes`): auto-approve every approval gate this run
|
|
54
|
+
* holds on (run-scoped only — the run id this invocation dispatched). The
|
|
55
|
+
* operator must still be clause-eligible per hold; an ineligible hold blocks.
|
|
56
|
+
*/
|
|
57
|
+
approveAll?: boolean;
|
|
41
58
|
}
|
|
42
59
|
/** Result of a single remote fixture run */
|
|
43
60
|
export interface RemoteRunResult {
|