@kici-dev/compiler 0.1.21 → 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.js +8 -4
- 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 +65 -4
- package/dist/commands/test.d.ts +10 -0
- package/dist/llm-context/llms-full.txt +344 -48
- package/dist/llm-context/llms.txt +2 -1
- package/dist/local-executor/job-runner.js +2 -1
- package/dist/lockfile/generator.js +34 -17
- package/dist/remote/config.d.ts +2 -0
- package/dist/remote/config.js +1 -0
- package/dist/remote/platform-client.d.ts +6 -1
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/step-context.js +2 -1
- package/dist/types.d.ts +11 -6
- package/package.json +4 -4
- 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.
|
|
10
|
+
const version = "0.1.22";
|
|
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
|
|
@@ -89,7 +89,7 @@ function buildProgram() {
|
|
|
89
89
|
});
|
|
90
90
|
process.exit(success ? 0 : 1);
|
|
91
91
|
});
|
|
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) => {
|
|
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) => {
|
|
93
93
|
const { runRemoteCommand } = await import("./commands/index.js");
|
|
94
94
|
const { resolveCheckMode } = await import("./commands/check-mode.js");
|
|
95
95
|
let checkMode;
|
|
@@ -105,7 +105,10 @@ function buildProgram() {
|
|
|
105
105
|
const success = await runRemoteCommand(fixture, {
|
|
106
106
|
...options,
|
|
107
107
|
checkMode,
|
|
108
|
-
envFlags: options.env
|
|
108
|
+
envFlags: options.env,
|
|
109
|
+
targets: options.target,
|
|
110
|
+
targetAllowEmpty: options.targetAllowEmpty,
|
|
111
|
+
approveAll: options.approveAll
|
|
109
112
|
});
|
|
110
113
|
process.exit(success ? 0 : 1);
|
|
111
114
|
});
|
|
@@ -127,7 +130,7 @@ function buildProgram() {
|
|
|
127
130
|
const success = await hookInstallCommand({ git: options.git });
|
|
128
131
|
process.exit(success ? 0 : 1);
|
|
129
132
|
});
|
|
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", `
|
|
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", `
|
|
131
134
|
Environment variables:
|
|
132
135
|
KICI_BROWSER_CMD Custom browser command (use {url} placeholder, or 'none' to suppress)
|
|
133
136
|
KICI_CALLBACK_PORT Fixed port for OAuth PKCE callback server (default: random)
|
|
@@ -140,6 +143,7 @@ Environment variables:
|
|
|
140
143
|
token: options.token,
|
|
141
144
|
device: options.device,
|
|
142
145
|
platformEndpoint: options.platformEndpoint,
|
|
146
|
+
oidcIssuer: options.oidcIssuer,
|
|
143
147
|
routingKey: options.routingKey
|
|
144
148
|
});
|
|
145
149
|
process.exit(success ? 0 : 1);
|
|
@@ -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) {
|
|
@@ -295,7 +316,11 @@ async function runSingleFixture(fixture, ctx, options, config, history) {
|
|
|
295
316
|
workflowName: opts.workflowName,
|
|
296
317
|
inlineLockFile: overlay.inlineLockFile,
|
|
297
318
|
fullRepo: true,
|
|
298
|
-
...options.checkMode && { checkMode: options.checkMode }
|
|
319
|
+
...options.checkMode && { checkMode: options.checkMode },
|
|
320
|
+
...(() => {
|
|
321
|
+
const target = buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
|
|
322
|
+
return target ? { target } : {};
|
|
323
|
+
})()
|
|
299
324
|
});
|
|
300
325
|
if (triggerResult.status === "rejected") {
|
|
301
326
|
if (!options.quiet) logger.info(pc.red(`Rejected: ${triggerResult.reason ?? "unknown reason"}`));
|
|
@@ -372,6 +397,8 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
|
372
397
|
process.on("SIGINT", cancelHandler);
|
|
373
398
|
try {
|
|
374
399
|
let lastStatus = null;
|
|
400
|
+
const seenHolds = /* @__PURE__ */ new Set();
|
|
401
|
+
let heldCtx;
|
|
375
402
|
while (!cancelled) {
|
|
376
403
|
const logs = await ctx.client.runLogs(ctx.orgId, runId, cursor, ctx.target);
|
|
377
404
|
for (const line of logs.lines) {
|
|
@@ -381,7 +408,9 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
|
381
408
|
}
|
|
382
409
|
cursor = logs.nextCursor;
|
|
383
410
|
lastStatus = await ctx.client.runStatus(ctx.orgId, runId, ctx.target);
|
|
384
|
-
|
|
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);
|
|
385
414
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
386
415
|
}
|
|
387
416
|
return {
|
|
@@ -395,6 +424,34 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
|
|
|
395
424
|
process.removeListener("SIGINT", cancelHandler);
|
|
396
425
|
}
|
|
397
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
|
+
}
|
|
398
455
|
/** Build the final result + render the summary table for a completed run. */
|
|
399
456
|
function finishRun(fixtureId, runId, status, startTime, tailLines, options) {
|
|
400
457
|
const durationMs = Date.now() - startTime;
|
|
@@ -483,7 +540,11 @@ async function runDirectWorkflow(workflowName, options) {
|
|
|
483
540
|
workflowName,
|
|
484
541
|
inlineLockFile: overlay.inlineLockFile,
|
|
485
542
|
fullRepo: true,
|
|
486
|
-
...options.checkMode && { checkMode: options.checkMode }
|
|
543
|
+
...options.checkMode && { checkMode: options.checkMode },
|
|
544
|
+
...(() => {
|
|
545
|
+
const target = buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
|
|
546
|
+
return target ? { target } : {};
|
|
547
|
+
})()
|
|
487
548
|
});
|
|
488
549
|
if (!options.quiet) logger.info(pc.green(`Run started: ${triggerResult.runId}`));
|
|
489
550
|
if (options.wait === false) return triggerResult.status === "accepted";
|
|
@@ -511,6 +572,6 @@ function displayRemoteResults(results) {
|
|
|
511
572
|
logger.info("");
|
|
512
573
|
}
|
|
513
574
|
//#endregion
|
|
514
|
-
export { runLocalCommand, runRemoteCommand };
|
|
575
|
+
export { buildTargetSelector, runLocalCommand, runRemoteCommand };
|
|
515
576
|
|
|
516
577
|
//# sourceMappingURL=run.js.map
|
package/dist/commands/test.d.ts
CHANGED
|
@@ -45,6 +45,16 @@ export interface RemoteRunOptions extends TestOptions {
|
|
|
45
45
|
* Defaults to `apply`.
|
|
46
46
|
*/
|
|
47
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;
|
|
48
58
|
}
|
|
49
59
|
/** Result of a single remote fixture run */
|
|
50
60
|
export interface RemoteRunResult {
|