@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
@@ -9,11 +9,19 @@ import { createOverlayTarball, getSizeWarning, uploadTarball } from "../remote/u
9
9
  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
+ import { compileCommand } from "./compile.js";
12
13
  import { compileFixtures, filterFixtures } from "../fixtures/compiler.js";
14
+ import { describeEvent } from "../fixtures/describe-event.js";
15
+ import { FixturePickerCancelledError, runFixturePicker } from "../fixtures/picker.js";
16
+ import { listHeldRunsForRun, resolveHeldRunContext } from "./held-run-client.js";
17
+ import { handleNewHolds } from "./run-hold-watch.js";
13
18
  import path from "node:path";
14
19
  import pc from "picocolors";
15
20
  import { readFile, writeFile } from "node:fs/promises";
16
21
  import { formatBytes, logger, toErrorMessage } from "@kici-dev/core";
22
+ import { coerceDispatchInputs, parseInputPairs } from "@kici-dev/engine";
23
+ import { normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
24
+ import { confirm } from "@inquirer/prompts";
17
25
  //#region src/commands/run.ts
18
26
  /** Terminal run statuses returned by the Platform run-status snapshot. */
19
27
  const TERMINAL_STATUSES = new Set([
@@ -22,9 +30,81 @@ const TERMINAL_STATUSES = new Set([
22
30
  "cancelled",
23
31
  "error"
24
32
  ]);
33
+ /**
34
+ * Compile `--target` selector strings into a {@link HostTargetSelector}. Each
35
+ * string becomes one AND value (its own include set); repeated values
36
+ * AND-combine. Returns undefined when no `--target` is given. Throws when
37
+ * `--target-allow-empty` is set without at least one `--target`.
38
+ */
39
+ function buildTargetSelector(targets, allowEmpty) {
40
+ if (!targets || targets.length === 0) {
41
+ if (allowEmpty) throw new Error("--target-allow-empty requires at least one --target selector");
42
+ return;
43
+ }
44
+ return {
45
+ values: targets.map((t) => normalizeRunsOnToMatchers(t, "kici run --target")),
46
+ allowEmpty
47
+ };
48
+ }
49
+ /**
50
+ * Look up the dispatch-trigger `inputs` descriptor for a workflow from a parsed
51
+ * inline lock file. When `workflowName` is given, only that workflow's dispatch
52
+ * triggers are considered; otherwise descriptors across all workflows are merged
53
+ * (best-effort fast-fail — the orchestrator re-validates against the matched
54
+ * workflow authoritatively). Returns undefined when no dispatch inputs declared.
55
+ */
56
+ function lookupDispatchInputsDescriptor(inlineLockFile, workflowName) {
57
+ if (!inlineLockFile) return void 0;
58
+ let lock;
59
+ try {
60
+ lock = JSON.parse(inlineLockFile);
61
+ } catch {
62
+ return;
63
+ }
64
+ const merged = {};
65
+ let found = false;
66
+ for (const wf of lock.workflows ?? []) {
67
+ if (workflowName && wf.name !== workflowName) continue;
68
+ for (const trigger of wf.triggers ?? []) if (trigger._type === "dispatch" && trigger.inputs) {
69
+ Object.assign(merged, trigger.inputs);
70
+ found = true;
71
+ }
72
+ }
73
+ return found ? merged : void 0;
74
+ }
75
+ /**
76
+ * Validate raw `--input KEY=VALUE` pairs against the (optional) lock descriptor
77
+ * and return the raw operator pairs verbatim. The CLI fast-fails on malformed /
78
+ * invalid input for UX, but forwards the **raw** strings — the orchestrator is
79
+ * authoritative and applies coercion + defaults exactly once.
80
+ */
81
+ function buildDispatchInputs(pairs, descriptor) {
82
+ if (!pairs.length) return {};
83
+ const raw = parseInputPairs(pairs);
84
+ if (descriptor) {
85
+ const r = coerceDispatchInputs(raw, descriptor);
86
+ if ("error" in r) throw r.error;
87
+ }
88
+ return raw;
89
+ }
25
90
  /** Interval between status/log polls while a run is active. */
26
91
  const POLL_INTERVAL_MS = 750;
27
92
  /**
93
+ * Recompile `.kici/workflows` → `kici.lock.json` before a remote run, mirroring
94
+ * `kici run local`. The orchestrator matches triggers and dispatches against the
95
+ * inline lock, so a stale lock would route an edited or newly-added workflow
96
+ * incorrectly. Returns false on a compile/validation error so the caller can
97
+ * abort before any upload or dispatch.
98
+ */
99
+ async function compileBeforeRemoteRun(options) {
100
+ return compileCommand({
101
+ kiciDir: options.kiciDir ?? ".kici",
102
+ check: false,
103
+ verbose: options.debug ?? false,
104
+ quiet: Boolean(options.json || options.quiet)
105
+ });
106
+ }
107
+ /**
28
108
  * Run a workflow locally using the local executor.
29
109
  * Thin wrapper that delegates to executeLocal from local-executor.
30
110
  *
@@ -58,6 +138,7 @@ async function runRemoteCommand(fixture, options) {
58
138
  if (!options.quiet) logger.info(pc.gray("Debug mode enabled"));
59
139
  }
60
140
  try {
141
+ buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
61
142
  if (options.workflow && !fixture && !options.all) return await runDirectWorkflow(options.workflow, options);
62
143
  const kiciDir = resolveKiciDir(options.kiciDir);
63
144
  if (options.history) {
@@ -68,9 +149,20 @@ async function runRemoteCommand(fixture, options) {
68
149
  return true;
69
150
  }
70
151
  const fixtures = await compileFixtures(path.join(kiciDir, "tests"));
71
- if (!fixture && !options.all) return listFixtures(fixtures);
72
152
  let selected;
73
- if (options.all) selected = fixtures;
153
+ if (options.pick) {
154
+ if (fixtures.length === 0) return listFixtures(fixtures);
155
+ try {
156
+ selected = await runFixturePicker(fixtures);
157
+ } catch (err) {
158
+ if (err instanceof FixturePickerCancelledError) {
159
+ logger.info(pc.gray(err.message));
160
+ return false;
161
+ }
162
+ throw err;
163
+ }
164
+ } else if (!fixture && !options.all) return listFixtures(fixtures);
165
+ else if (options.all) selected = fixtures;
74
166
  else selected = filterFixtures(fixtures, fixture);
75
167
  if (selected.length === 0) {
76
168
  logger.info(pc.yellow(`No fixtures matched: ${fixture ?? "(none)"}`));
@@ -111,17 +203,6 @@ function listFixtures(fixtures) {
111
203
  return true;
112
204
  }
113
205
  /**
114
- * Describe the event type from a fixture's trigger config.
115
- */
116
- function describeEvent(event) {
117
- if (!event || typeof event !== "object") return "unknown";
118
- const e = event;
119
- if (e._type === "push") return "push";
120
- if (e._type === "pr") return `pr:${e.action ?? "open"}`;
121
- if (typeof e._type === "string") return String(e._type);
122
- return "custom";
123
- }
124
- /**
125
206
  * Resolve the authenticated Platform client and the run target (org + cluster).
126
207
  *
127
208
  * Org resolution: `--org` → `config.activeOrgId` → error.
@@ -161,6 +242,7 @@ function resolvePlatformContext(config, options) {
161
242
  * Run fixtures remotely against the Platform.
162
243
  */
163
244
  async function runFixturesRemotely(fixtures, options) {
245
+ if (!await compileBeforeRemoteRun(options)) return false;
164
246
  const config = await loadGlobalConfig();
165
247
  const ctx = resolvePlatformContext(config, options);
166
248
  if (!ctx) return false;
@@ -295,7 +377,16 @@ async function runSingleFixture(fixture, ctx, options, config, history) {
295
377
  workflowName: opts.workflowName,
296
378
  inlineLockFile: overlay.inlineLockFile,
297
379
  fullRepo: true,
298
- ...options.checkMode && { checkMode: options.checkMode }
380
+ ...options.checkMode && { checkMode: options.checkMode },
381
+ ...(() => {
382
+ const target = buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
383
+ return target ? { target } : {};
384
+ })(),
385
+ ...(() => {
386
+ const descriptor = lookupDispatchInputsDescriptor(overlay.inlineLockFile, opts.workflowName);
387
+ const dispatchInputs = buildDispatchInputs(options.inputs ?? [], descriptor);
388
+ return Object.keys(dispatchInputs).length ? { dispatchInputs } : {};
389
+ })()
299
390
  });
300
391
  if (triggerResult.status === "rejected") {
301
392
  if (!options.quiet) logger.info(pc.red(`Rejected: ${triggerResult.reason ?? "unknown reason"}`));
@@ -372,6 +463,8 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
372
463
  process.on("SIGINT", cancelHandler);
373
464
  try {
374
465
  let lastStatus = null;
466
+ const seenHolds = /* @__PURE__ */ new Set();
467
+ let heldCtx;
375
468
  while (!cancelled) {
376
469
  const logs = await ctx.client.runLogs(ctx.orgId, runId, cursor, ctx.target);
377
470
  for (const line of logs.lines) {
@@ -381,7 +474,9 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
381
474
  }
382
475
  cursor = logs.nextCursor;
383
476
  lastStatus = await ctx.client.runStatus(ctx.orgId, runId, ctx.target);
384
- if ((lastStatus.done || TERMINAL_STATUSES.has(lastStatus.status)) && logs.done) return finishRun(fixtureId, runId, lastStatus, startTime, tailLines, options);
477
+ const terminal = lastStatus.done || TERMINAL_STATUSES.has(lastStatus.status);
478
+ if (terminal && logs.done) return finishRun(fixtureId, runId, lastStatus, startTime, tailLines, options);
479
+ if (!terminal && !options.quiet) await watchRunHolds(runId, seenHolds, () => heldCtx, (c) => heldCtx = c, options);
385
480
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
386
481
  }
387
482
  return {
@@ -395,6 +490,34 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
395
490
  process.removeListener("SIGINT", cancelHandler);
396
491
  }
397
492
  }
493
+ /**
494
+ * Fetch this run's pending holds and surface them to the operator. The held-run
495
+ * context is resolved lazily (cached across ticks via the getter/setter). A
496
+ * resolution / fetch failure is swallowed — hold-visibility is best-effort and
497
+ * must never abort the run watch.
498
+ */
499
+ async function watchRunHolds(runId, seenHolds, getCtx, setCtx, options) {
500
+ try {
501
+ let ctx = getCtx();
502
+ if (ctx === void 0) {
503
+ ctx = await resolveHeldRunContext();
504
+ setCtx(ctx);
505
+ }
506
+ if (!ctx) return;
507
+ const holds = await listHeldRunsForRun(ctx, runId);
508
+ if (holds.length === 0) return;
509
+ await handleNewHolds({
510
+ holds,
511
+ seen: seenHolds,
512
+ isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
513
+ approveAll: Boolean(options.approveAll),
514
+ confirm: (message) => confirm({
515
+ message,
516
+ default: false
517
+ })
518
+ });
519
+ } catch {}
520
+ }
398
521
  /** Build the final result + render the summary table for a completed run. */
399
522
  function finishRun(fixtureId, runId, status, startTime, tailLines, options) {
400
523
  const durationMs = Date.now() - startTime;
@@ -456,6 +579,7 @@ function buildEventFromFixture(opts) {
456
579
  * Platform.
457
580
  */
458
581
  async function runDirectWorkflow(workflowName, options) {
582
+ if (!await compileBeforeRemoteRun(options)) return false;
459
583
  const ctx = resolvePlatformContext(await loadGlobalConfig(), options);
460
584
  if (!ctx) return false;
461
585
  if (options.json) options.quiet = true;
@@ -483,7 +607,16 @@ async function runDirectWorkflow(workflowName, options) {
483
607
  workflowName,
484
608
  inlineLockFile: overlay.inlineLockFile,
485
609
  fullRepo: true,
486
- ...options.checkMode && { checkMode: options.checkMode }
610
+ ...options.checkMode && { checkMode: options.checkMode },
611
+ ...(() => {
612
+ const target = buildTargetSelector(options.targets, options.targetAllowEmpty ?? false);
613
+ return target ? { target } : {};
614
+ })(),
615
+ ...(() => {
616
+ const descriptor = lookupDispatchInputsDescriptor(overlay.inlineLockFile, workflowName);
617
+ const dispatchInputs = buildDispatchInputs(options.inputs ?? [], descriptor);
618
+ return Object.keys(dispatchInputs).length ? { dispatchInputs } : {};
619
+ })()
487
620
  });
488
621
  if (!options.quiet) logger.info(pc.green(`Run started: ${triggerResult.runId}`));
489
622
  if (options.wait === false) return triggerResult.status === "accepted";
@@ -511,6 +644,6 @@ function displayRemoteResults(results) {
511
644
  logger.info("");
512
645
  }
513
646
  //#endregion
514
- export { runLocalCommand, runRemoteCommand };
647
+ export { buildDispatchInputs, buildTargetSelector, lookupDispatchInputsDescriptor, runLocalCommand, runRemoteCommand };
515
648
 
516
649
  //# sourceMappingURL=run.js.map
@@ -19,6 +19,8 @@ export interface TestOptions extends PayloadOptions {
19
19
  export interface RemoteRunOptions extends TestOptions {
20
20
  /** Run all available fixtures */
21
21
  all?: boolean;
22
+ /** Interactively pick fixtures to run (multi-select checkbox). */
23
+ pick?: boolean;
22
24
  /** Run matching fixtures concurrently */
23
25
  parallel?: boolean;
24
26
  /** Fire and forget (print runIds, don't stream) */
@@ -45,6 +47,18 @@ export interface RemoteRunOptions extends TestOptions {
45
47
  * Defaults to `apply`.
46
48
  */
47
49
  checkMode?: CheckMode;
50
+ /** `--target <selector>` values (repeatable), AND-combined into host narrowing. */
51
+ targets?: string[];
52
+ /** `--target-allow-empty`: a target that zeroes a runsOnAll job skips it instead of failing. */
53
+ targetAllowEmpty?: boolean;
54
+ /**
55
+ * `--approve-all` (alias `--yes`): auto-approve every approval gate this run
56
+ * holds on (run-scoped only — the run id this invocation dispatched). The
57
+ * operator must still be clause-eligible per hold; an ineligible hold blocks.
58
+ */
59
+ approveAll?: boolean;
60
+ /** `--input KEY=VALUE` values (repeatable): typed workflow-dispatch inputs. */
61
+ inputs?: string[];
48
62
  }
49
63
  /** Result of a single remote fixture run */
50
64
  export interface RemoteRunResult {
@@ -1,6 +1,8 @@
1
1
  export interface TypesOptions {
2
2
  /** Path to .kici directory (defaults to .kici) */
3
3
  kiciDir?: string;
4
+ /** Suppress the success line on stdout (so machine-readable output stays pure). */
5
+ quiet?: boolean;
4
6
  }
5
7
  /**
6
8
  * Generate TypeScript declarations for environment secrets.
@@ -33,7 +33,7 @@ async function typesCommand(options = {}) {
33
33
  await fs.mkdir(typesDir, { recursive: true });
34
34
  const outputPath = path.join(typesDir, "secrets.d.ts");
35
35
  await fs.writeFile(outputPath, dtsContent, "utf-8");
36
- console.log(pc.green("Types generated") + pc.dim(` ${outputPath}`));
36
+ if (!options.quiet) console.log(pc.green("Types generated") + pc.dim(` ${outputPath}`));
37
37
  return true;
38
38
  } catch (err) {
39
39
  if (err instanceof DashboardClientError) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Describe the event type from a fixture's trigger config, for the fixtures
3
+ * table and the interactive picker.
4
+ */
5
+ export declare function describeEvent(event: unknown): string;
6
+ //# sourceMappingURL=describe-event.d.ts.map
@@ -0,0 +1,18 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ //#region src/fixtures/describe-event.ts
3
+ /**
4
+ * Describe the event type from a fixture's trigger config, for the fixtures
5
+ * table and the interactive picker.
6
+ */
7
+ function describeEvent(event) {
8
+ if (!event || typeof event !== "object") return "unknown";
9
+ const e = event;
10
+ if (e._type === "push") return "push";
11
+ if (e._type === "pr") return `pr:${e.action ?? "open"}`;
12
+ if (typeof e._type === "string") return String(e._type);
13
+ return "custom";
14
+ }
15
+ //#endregion
16
+ export { describeEvent };
17
+
18
+ //# sourceMappingURL=describe-event.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Interactive multi-select fixture picker for `kici run remote --pick`.
3
+ *
4
+ * Lists every available fixture with its source and event type, lets the user
5
+ * toggle one or more via a checkbox prompt, and returns the chosen fixtures.
6
+ * The caller feeds them into the standard remote-dispatch pipeline, so a picked
7
+ * run is identical to one selected by name / glob / --all.
8
+ */
9
+ import type { CompiledFixture } from './compiler.js';
10
+ export declare class FixturePickerCancelledError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /**
14
+ * Run the interactive multi-select fixture picker.
15
+ *
16
+ * @throws FixturePickerCancelledError when stdin is not a TTY or the user aborts.
17
+ */
18
+ export declare function runFixturePicker(fixtures: CompiledFixture[]): Promise<CompiledFixture[]>;
19
+ //# sourceMappingURL=picker.d.ts.map
@@ -0,0 +1,64 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ import { describeEvent } from "./describe-event.js";
3
+ import path from "node:path";
4
+ import pc from "picocolors";
5
+ import { logger } from "@kici-dev/core";
6
+ import { checkbox } from "@inquirer/prompts";
7
+ //#region src/fixtures/picker.ts
8
+ /**
9
+ * Interactive multi-select fixture picker for `kici run remote --pick`.
10
+ *
11
+ * Lists every available fixture with its source and event type, lets the user
12
+ * toggle one or more via a checkbox prompt, and returns the chosen fixtures.
13
+ * The caller feeds them into the standard remote-dispatch pipeline, so a picked
14
+ * run is identical to one selected by name / glob / --all.
15
+ */
16
+ var FixturePickerCancelledError = class extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "FixturePickerCancelledError";
20
+ }
21
+ };
22
+ function isStdinTty() {
23
+ return Boolean(process.stdin.isTTY);
24
+ }
25
+ /** Build a single fixture's display row: `id <source> <event type>`. */
26
+ function fixtureRow(f) {
27
+ const opts = typeof f.fixture.options === "function" ? null : f.fixture.options;
28
+ const eventType = opts?.event ? describeEvent(opts.event) : "(async)";
29
+ const source = path.relative(process.cwd(), f.sourceFile);
30
+ return `${f.id} ${pc.gray(source)} ${eventType}`;
31
+ }
32
+ function printFixtureList(fixtures) {
33
+ logger.info(pc.bold("Available fixtures:"));
34
+ for (const f of fixtures) logger.info(` ${pc.cyan(f.id)} — ${fixtureRow(f)}`);
35
+ }
36
+ /**
37
+ * Run the interactive multi-select fixture picker.
38
+ *
39
+ * @throws FixturePickerCancelledError when stdin is not a TTY or the user aborts.
40
+ */
41
+ async function runFixturePicker(fixtures) {
42
+ if (!isStdinTty()) {
43
+ printFixtureList(fixtures);
44
+ throw new FixturePickerCancelledError("--pick requires an interactive terminal. Pass a fixture name instead.");
45
+ }
46
+ let chosenIds;
47
+ try {
48
+ chosenIds = await checkbox({
49
+ message: "Select fixtures to run",
50
+ choices: fixtures.map((f) => ({
51
+ name: fixtureRow(f),
52
+ value: f.id
53
+ })),
54
+ required: true
55
+ });
56
+ } catch (err) {
57
+ throw new FixturePickerCancelledError(`Picker cancelled: ${err instanceof Error ? err.message : String(err)}`);
58
+ }
59
+ return fixtures.filter((f) => chosenIds.includes(f.id));
60
+ }
61
+ //#endregion
62
+ export { FixturePickerCancelledError, runFixturePicker };
63
+
64
+ //# sourceMappingURL=picker.js.map