@kici-dev/compiler 0.1.26 → 0.1.27

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 (96) hide show
  1. package/dist/cli-banner.d.ts +6 -3
  2. package/dist/cli-banner.js +13 -5
  3. package/dist/cli.js +66 -43
  4. package/dist/commands/check-mode.d.ts +1 -1
  5. package/dist/commands/compile.js +3 -3
  6. package/dist/commands/index.d.ts +5 -1
  7. package/dist/commands/index.js +6 -3
  8. package/dist/commands/init.js +1 -1
  9. package/dist/commands/local-trust-root.d.ts +6 -0
  10. package/dist/commands/local-trust-root.js +52 -0
  11. package/dist/commands/local.d.ts +39 -0
  12. package/dist/commands/local.js +137 -0
  13. package/dist/commands/login.d.ts +2 -0
  14. package/dist/commands/login.js +40 -0
  15. package/dist/commands/logout.js +7 -0
  16. package/dist/commands/preview.js +1 -1
  17. package/dist/commands/run-banner.d.ts +31 -0
  18. package/dist/commands/run-banner.js +25 -0
  19. package/dist/commands/run-routed.d.ts +53 -0
  20. package/dist/commands/run-routed.js +176 -0
  21. package/dist/commands/run.d.ts +0 -17
  22. package/dist/commands/run.js +7 -50
  23. package/dist/commands/secrets-list.d.ts +2 -2
  24. package/dist/commands/secrets-list.js +4 -4
  25. package/dist/commands/types.d.ts +3 -3
  26. package/dist/commands/types.js +4 -4
  27. package/dist/execution/executor.js +7 -7
  28. package/dist/generators/secrets-dts.d.ts +9 -9
  29. package/dist/generators/secrets-dts.js +12 -12
  30. package/dist/llm-context/llms-architecture.txt +2 -2
  31. package/dist/llm-context/llms-cli.txt +674 -340
  32. package/dist/llm-context/llms-features.txt +334 -336
  33. package/dist/llm-context/llms-full.txt +1207 -892
  34. package/dist/llm-context/llms-getting-started.txt +7 -15
  35. package/dist/llm-context/llms-patterns.txt +5 -23
  36. package/dist/llm-context/llms-providers.txt +10 -1
  37. package/dist/llm-context/llms-sdk.txt +12 -12
  38. package/dist/llm-context/llms.txt +5 -5
  39. package/dist/local-executor/secret-loader.d.ts +1 -1
  40. package/dist/local-plane/orchestrator-process.d.ts +65 -0
  41. package/dist/local-plane/orchestrator-process.js +115 -0
  42. package/dist/local-plane/paths.d.ts +39 -0
  43. package/dist/local-plane/paths.js +54 -0
  44. package/dist/local-plane/plane-manager.d.ts +151 -0
  45. package/dist/local-plane/plane-manager.js +340 -0
  46. package/dist/local-plane/plane-seed.d.ts +76 -0
  47. package/dist/local-plane/plane-seed.js +59 -0
  48. package/dist/local-plane/plane-trigger.d.ts +62 -0
  49. package/dist/local-plane/plane-trigger.js +97 -0
  50. package/dist/local-plane/platform-attach.d.ts +53 -0
  51. package/dist/local-plane/platform-attach.js +111 -0
  52. package/dist/local-plane/postgres.d.ts +44 -0
  53. package/dist/local-plane/postgres.js +165 -0
  54. package/dist/local-plane/resolve-plane.d.ts +37 -0
  55. package/dist/local-plane/resolve-plane.js +76 -0
  56. package/dist/local-plane/run-follow.d.ts +41 -0
  57. package/dist/local-plane/run-follow.js +104 -0
  58. package/dist/local-plane/scaler-config.d.ts +54 -0
  59. package/dist/local-plane/scaler-config.js +114 -0
  60. package/dist/local-plane/secret-seed.d.ts +59 -0
  61. package/dist/local-plane/secret-seed.js +112 -0
  62. package/dist/local-plane/source-provider.d.ts +43 -0
  63. package/dist/local-plane/source-provider.js +165 -0
  64. package/dist/local-plane/trusted-routing.d.ts +32 -0
  65. package/dist/local-plane/trusted-routing.js +47 -0
  66. package/dist/lockfile/generator.js +7 -7
  67. package/dist/remote/dashboard-client.d.ts +7 -7
  68. package/dist/remote/dashboard-client.js +7 -7
  69. package/dist/remote/platform-client.d.ts +1 -1
  70. package/dist/remote/secret-loader.d.ts +18 -0
  71. package/dist/{local-executor → remote}/secret-loader.js +4 -4
  72. package/dist/remote/secret-upload.d.ts +1 -1
  73. package/dist/remote/secret-upload.js +2 -2
  74. package/dist/remote/uploader.d.ts +1 -1
  75. package/dist/remote/uploader.js +2 -2
  76. package/dist/templates/agents-md.d.ts +1 -1
  77. package/dist/templates/agents-md.js +2 -2
  78. package/dist/templates/package-json.js +1 -1
  79. package/dist/test-runner/job-executor.js +1 -1
  80. package/dist/test-runner/runs-on-display.d.ts +9 -0
  81. package/dist/{local-executor → test-runner}/runs-on-display.js +1 -1
  82. package/dist/test-runner/step-context.d.ts +1 -1
  83. package/dist/test-runner/step-context.js +2 -2
  84. package/dist/types.d.ts +6 -6
  85. package/package.json +7 -4
  86. package/sbom.spdx.json +9884 -1459
  87. package/dist/local-executor/dag-scheduler.js +0 -183
  88. package/dist/local-executor/index.js +0 -378
  89. package/dist/local-executor/job-runner.js +0 -411
  90. package/dist/local-executor/materializer.js +0 -132
  91. package/dist/local-executor/output-streamer.js +0 -168
  92. package/dist/local-executor/payload-generator.js +0 -138
  93. package/dist/local-executor/picker.js +0 -109
  94. package/dist/local-executor/to-event-payload.js +0 -21
  95. package/dist/local-executor/types.js +0 -2
  96. package/dist/local-executor/workflow-lock.js +0 -0
@@ -0,0 +1,104 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { ExecutionJobStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
3
+ import { AdminApiClient } from "@kici-dev/orchestrator";
4
+ //#region src/local-plane/run-follow.ts
5
+ /**
6
+ * Follow an offline routed run against the local plane by polling the
7
+ * orchestrator admin API (no SSE in independent mode). Streams step log lines as
8
+ * they arrive and resolves when the run reaches a terminal state.
9
+ */
10
+ /** Job states that mean the job did NOT succeed (failed / stale / cancelled / dropped). */
11
+ const FAILED_JOB_STATES = /* @__PURE__ */ new Set([
12
+ ExecutionJobStatus.enum.failed,
13
+ ExecutionJobStatus.enum.timed_out_stale,
14
+ ExecutionJobStatus.enum.cancelled,
15
+ ExecutionJobStatus.enum.drift_dropped
16
+ ]);
17
+ /** Poll a run to completion, streaming step logs. */
18
+ async function followRun(planeUrl, adminToken, runId, opts = {}) {
19
+ const client = opts.client ?? new AdminApiClient(planeUrl, adminToken);
20
+ const pollIntervalMs = opts.pollIntervalMs ?? 750;
21
+ const idleTimeoutMs = opts.idleTimeoutMs ?? opts.timeoutMs ?? 9e5;
22
+ const maxTotalMs = opts.maxTotalMs ?? 72e5;
23
+ const cursors = /* @__PURE__ */ new Map();
24
+ const stream = !opts.quiet && opts.onLine ? opts.onLine : void 0;
25
+ const startedAt = Date.now();
26
+ let lastProgressAt = startedAt;
27
+ let lastStatus = "";
28
+ for (;;) {
29
+ const now = Date.now();
30
+ if (now - lastProgressAt > idleTimeoutMs) throw new Error(`offline run: follow timed out (no progress for ${idleTimeoutMs}ms) for run ${runId}`);
31
+ if (now - startedAt > maxTotalMs) throw new Error(`offline run: follow exceeded its max window (${maxTotalMs}ms) for run ${runId}`);
32
+ const { run } = await client.get(`/api/v1/admin/runs/${runId}`);
33
+ let progressed = false;
34
+ if (run.status !== lastStatus) {
35
+ lastStatus = run.status;
36
+ progressed = true;
37
+ }
38
+ if (stream) {
39
+ if (await drainLogs(client, runId, cursors, stream) > 0) progressed = true;
40
+ }
41
+ if (progressed) lastProgressAt = now;
42
+ if (TERMINAL_RUN_STATES.has(run.status)) {
43
+ const jobs = await fetchJobs(client, runId);
44
+ const allTerminal = jobs.length > 0 && jobs.every((j) => TERMINAL_JOB_STATES.has(j.status));
45
+ const anyFailed = jobs.some((j) => FAILED_JOB_STATES.has(j.status));
46
+ if (allTerminal && !(run.status === "success" && anyFailed)) {
47
+ if (stream) await drainLogs(client, runId, cursors, stream);
48
+ return {
49
+ runId,
50
+ status: run.status,
51
+ jobs
52
+ };
53
+ }
54
+ }
55
+ await sleep(pollIntervalMs);
56
+ }
57
+ }
58
+ /** Fetch the run's jobs mapped to the summary shape. */
59
+ async function fetchJobs(client, runId) {
60
+ const { jobs } = await client.get(`/api/v1/admin/runs/${runId}/jobs`);
61
+ return jobs.map((j) => ({
62
+ name: j.jobName,
63
+ status: j.status,
64
+ durationMs: j.durationMs ?? void 0
65
+ }));
66
+ }
67
+ /**
68
+ * Stream any new step-log lines. Best-effort: a per-step fetch error never
69
+ * aborts the follow (logs are observability, not the terminal signal).
70
+ */
71
+ async function drainLogs(client, runId, cursors, onLine) {
72
+ let emitted = 0;
73
+ let jobs;
74
+ try {
75
+ jobs = (await client.get(`/api/v1/admin/runs/${runId}/jobs?includeSteps=true`)).jobs;
76
+ } catch {
77
+ return emitted;
78
+ }
79
+ for (const job of jobs) for (const step of job.steps ?? []) {
80
+ const key = `${job.jobId}:${step.stepIndex}`;
81
+ try {
82
+ let cursor = cursors.get(key);
83
+ for (;;) {
84
+ const qs = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
85
+ const page = await client.get(`/api/v1/admin/runs/${runId}/jobs/${job.jobId}/steps/${step.stepIndex}/logs${qs}`);
86
+ for (const l of page.lines) {
87
+ onLine(l.value);
88
+ emitted++;
89
+ }
90
+ cursor = page.nextCursor ?? String(page.totalLines);
91
+ cursors.set(key, cursor);
92
+ if (!page.nextCursor) break;
93
+ }
94
+ } catch {}
95
+ }
96
+ return emitted;
97
+ }
98
+ function sleep(ms) {
99
+ return new Promise((r) => setTimeout(r, ms));
100
+ }
101
+ //#endregion
102
+ export { followRun };
103
+
104
+ //# sourceMappingURL=run-follow.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Bare-metal scaler configuration for the local dev plane.
3
+ *
4
+ * A dispatched job needs an agent to run it. The plane orchestrator boots with
5
+ * a single bare-metal scaler pointed at a label set matching the default
6
+ * `runsOn` (`default`), so an offline `kici run --local` dispatch auto-spawns an
7
+ * ephemeral, one-job-then-exit agent on this machine. The spawned binary is a
8
+ * small executable wrapper that execs `node <@kici-dev/agent server entry>` —
9
+ * mirroring the shape proven by the `local-file-source` E2E, and avoiding a
10
+ * dependency on the (non-executable) shipped `kici-agent` script.
11
+ */
12
+ /** Resolve the built `@kici-dev/agent` server entry the bare-metal scaler runs. */
13
+ export declare function resolveAgentBinary(): string;
14
+ /**
15
+ * Write the executable wrapper the bare-metal scaler spawns. The scaler invokes
16
+ * `spawn(binaryPath, [])` directly, so the target must be an executable file;
17
+ * the shipped agent entry is a plain Node module without an exec bit, so this
18
+ * wrapper bridges the gap. Returns the wrapper path.
19
+ */
20
+ export declare function writeAgentWrapper(): string;
21
+ /**
22
+ * The non-reserved routing label a `kici run --local --trusted` run appends to
23
+ * every job's `runsOn` so the dispatch lands on the plane's trusted label set.
24
+ * `self-hosted` (not a `kici:`-prefixed reserved label) is also the `runsOn` the
25
+ * CI deploy/CDN workflows already declare, so those route to the trusted profile
26
+ * on this plane by construction.
27
+ */
28
+ export declare const TRUSTED_ROUTING_LABEL = "self-hosted";
29
+ /**
30
+ * The non-reserved routing label a `kici run --local --trusted --in-place` run
31
+ * appends (alongside `self-hosted`) so the dispatch lands on the plane's trusted
32
+ * **in-place** label set — the agent runs the operator's real working tree
33
+ * directly (no clone). This is the profile KiCI's own routed `deploy:stg` uses.
34
+ */
35
+ export declare const IN_PLACE_ROUTING_LABEL = "in-place";
36
+ /**
37
+ * Write the plane's bare-metal scaler YAML and return its path. One scaler with
38
+ * two coexisting label sets spawning the agent wrapper against the plane
39
+ * orchestrator's localhost WS endpoint:
40
+ *
41
+ * - `default` — the sandboxed / credential-isolated profile a normal
42
+ * `kici run --local` dispatch (jobs with `runsOn: ['default']` or none) lands
43
+ * on. Smallest-set-wins routing keeps default runs here.
44
+ * - `default` + `self-hosted` — the TRUSTED fleet profile (`KICI_TRUSTED_ENV=true`,
45
+ * bwrap off) a `kici run --local --trusted` run (or a `runsOn: self-hosted`
46
+ * workflow) lands on: steps run with the ambient host env passed through,
47
+ * minus the agent's own KiCI identity secrets.
48
+ * - `default` + `self-hosted` + `in-place` — the TRUSTED **in-place** profile
49
+ * (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted --in-place` run
50
+ * lands on: same trusted env, but the agent uses the operator's real working
51
+ * tree directly (no clone) — the profile KiCI's own routed `deploy:stg` uses.
52
+ */
53
+ export declare function writeScalerConfig(orchestratorPort: number): string;
54
+ //# sourceMappingURL=scaler-config.d.ts.map
@@ -0,0 +1,114 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { planePaths } from "./paths.js";
3
+ import { createRequire } from "node:module";
4
+ import fs from "node:fs";
5
+ import { stringify } from "yaml";
6
+ //#region src/local-plane/scaler-config.ts
7
+ /**
8
+ * Bare-metal scaler configuration for the local dev plane.
9
+ *
10
+ * A dispatched job needs an agent to run it. The plane orchestrator boots with
11
+ * a single bare-metal scaler pointed at a label set matching the default
12
+ * `runsOn` (`default`), so an offline `kici run --local` dispatch auto-spawns an
13
+ * ephemeral, one-job-then-exit agent on this machine. The spawned binary is a
14
+ * small executable wrapper that execs `node <@kici-dev/agent server entry>` —
15
+ * mirroring the shape proven by the `local-file-source` E2E, and avoiding a
16
+ * dependency on the (non-executable) shipped `kici-agent` script.
17
+ */
18
+ /** Resolve the built `@kici-dev/agent` server entry the bare-metal scaler runs. */
19
+ function resolveAgentBinary() {
20
+ return createRequire(import.meta.url).resolve("@kici-dev/agent/server");
21
+ }
22
+ /**
23
+ * Write the executable wrapper the bare-metal scaler spawns. The scaler invokes
24
+ * `spawn(binaryPath, [])` directly, so the target must be an executable file;
25
+ * the shipped agent entry is a plain Node module without an exec bit, so this
26
+ * wrapper bridges the gap. Returns the wrapper path.
27
+ */
28
+ function writeAgentWrapper() {
29
+ const { agentWrapperFile, root } = planePaths();
30
+ fs.mkdirSync(root, { recursive: true });
31
+ const server = resolveAgentBinary();
32
+ fs.writeFileSync(agentWrapperFile, `#!/usr/bin/env bash\nexec node ${JSON.stringify(server)} "$@"\n`);
33
+ fs.chmodSync(agentWrapperFile, 493);
34
+ return agentWrapperFile;
35
+ }
36
+ /**
37
+ * The non-reserved routing label a `kici run --local --trusted` run appends to
38
+ * every job's `runsOn` so the dispatch lands on the plane's trusted label set.
39
+ * `self-hosted` (not a `kici:`-prefixed reserved label) is also the `runsOn` the
40
+ * CI deploy/CDN workflows already declare, so those route to the trusted profile
41
+ * on this plane by construction.
42
+ */
43
+ const TRUSTED_ROUTING_LABEL = "self-hosted";
44
+ /**
45
+ * The non-reserved routing label a `kici run --local --trusted --in-place` run
46
+ * appends (alongside `self-hosted`) so the dispatch lands on the plane's trusted
47
+ * **in-place** label set — the agent runs the operator's real working tree
48
+ * directly (no clone). This is the profile KiCI's own routed `deploy:stg` uses.
49
+ */
50
+ const IN_PLACE_ROUTING_LABEL = "in-place";
51
+ /**
52
+ * Write the plane's bare-metal scaler YAML and return its path. One scaler with
53
+ * two coexisting label sets spawning the agent wrapper against the plane
54
+ * orchestrator's localhost WS endpoint:
55
+ *
56
+ * - `default` — the sandboxed / credential-isolated profile a normal
57
+ * `kici run --local` dispatch (jobs with `runsOn: ['default']` or none) lands
58
+ * on. Smallest-set-wins routing keeps default runs here.
59
+ * - `default` + `self-hosted` — the TRUSTED fleet profile (`KICI_TRUSTED_ENV=true`,
60
+ * bwrap off) a `kici run --local --trusted` run (or a `runsOn: self-hosted`
61
+ * workflow) lands on: steps run with the ambient host env passed through,
62
+ * minus the agent's own KiCI identity secrets.
63
+ * - `default` + `self-hosted` + `in-place` — the TRUSTED **in-place** profile
64
+ * (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted --in-place` run
65
+ * lands on: same trusted env, but the agent uses the operator's real working
66
+ * tree directly (no clone) — the profile KiCI's own routed `deploy:stg` uses.
67
+ */
68
+ function writeScalerConfig(orchestratorPort) {
69
+ const { scalerConfigFile, root } = planePaths();
70
+ fs.mkdirSync(root, { recursive: true });
71
+ const binaryPath = writeAgentWrapper();
72
+ const yaml = stringify({
73
+ version: 1,
74
+ scalers: [{
75
+ name: "kici-local-bare-metal",
76
+ type: "bare-metal",
77
+ maxAgents: 10,
78
+ orchestratorUrl: `ws://127.0.0.1:${orchestratorPort}/ws`,
79
+ labelSets: [
80
+ {
81
+ labels: ["default"],
82
+ binaryPath
83
+ },
84
+ {
85
+ labels: ["default", TRUSTED_ROUTING_LABEL],
86
+ binaryPath,
87
+ env: {
88
+ KICI_TRUSTED_ENV: "true",
89
+ KICI_SANDBOX: "false"
90
+ }
91
+ },
92
+ {
93
+ labels: [
94
+ "default",
95
+ TRUSTED_ROUTING_LABEL,
96
+ IN_PLACE_ROUTING_LABEL
97
+ ],
98
+ binaryPath,
99
+ env: {
100
+ KICI_TRUSTED_ENV: "true",
101
+ KICI_SANDBOX: "false",
102
+ KICI_IN_PLACE: "true"
103
+ }
104
+ }
105
+ ]
106
+ }]
107
+ });
108
+ fs.writeFileSync(scalerConfigFile, yaml);
109
+ return scalerConfigFile;
110
+ }
111
+ //#endregion
112
+ export { IN_PLACE_ROUTING_LABEL, TRUSTED_ROUTING_LABEL, resolveAgentBinary, writeAgentWrapper, writeScalerConfig };
113
+
114
+ //# sourceMappingURL=scaler-config.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Seed a project's local `secrets.yaml` (and `.kici/.secrets` INI) contexts into
3
+ * the local dev plane's real `scoped_secrets` store, preserving context/scope
4
+ * structure so the plane's real `SecretResolver` honors scoping at dispatch —
5
+ * the routed local run resolves scoped secrets through the plane's real
6
+ * resolver rather than a flat merge.
7
+ *
8
+ * Local secret files are READ-ONLY here: values are only READ and transit into
9
+ * the local plane's Postgres via the admin secret API. Nothing is written back
10
+ * to a secret file, and nothing leaves this machine (the plane is local).
11
+ *
12
+ * Scope model: each `secrets.yaml` top-level key is a context name. The seeder
13
+ * creates a context of that name, binds it to a scope pattern equal to the
14
+ * context name, and stores each secret at that scope. So a workflow job bound to
15
+ * context `production` resolves `production`-scoped secrets through the resolver.
16
+ */
17
+ /** Minimal AdminApiClient surface the seeder needs (for test injection). */
18
+ export interface SecretSeedClient {
19
+ createContext(data: {
20
+ orgId: string;
21
+ name: string;
22
+ allowLocalExecution?: boolean;
23
+ }): Promise<{
24
+ envId: string;
25
+ created: boolean;
26
+ }>;
27
+ bindContext(data: {
28
+ orgId: string;
29
+ name: string;
30
+ scopePattern: string;
31
+ hostPattern?: string;
32
+ }): Promise<unknown>;
33
+ setSecret(orgId: string, scope: string, key: string, value: string): Promise<void>;
34
+ }
35
+ /** Result of a seed run: the contexts seeded and how many secret keys landed. */
36
+ export interface SeededSecrets {
37
+ contexts: string[];
38
+ secretCount: number;
39
+ }
40
+ /**
41
+ * Collect the per-context secrets from a project's `.kici/` directory,
42
+ * preserving context structure. Reads `.kici/.secrets` (INI sections) and
43
+ * `.kici/secrets.yaml` (top-level contexts). The yaml layer wins on key
44
+ * collisions within a context. Flat (context-less) secrets are ignored here —
45
+ * a routed job only resolves secrets for the contexts it binds.
46
+ */
47
+ export declare function loadLocalSecretContexts(kiciDir: string): Promise<Record<string, Record<string, string>>>;
48
+ /**
49
+ * Seed the local plane's `scoped_secrets` store from a project's `secrets.yaml`
50
+ * / `.secrets` contexts. Idempotent: `createContext` upserts, `bind` is a no-op
51
+ * when the scope is already bound, and `setSecret` overwrites in place. Returns
52
+ * the contexts + secret-key count seeded (never the values).
53
+ */
54
+ export declare function seedLocalSecrets(planeUrl: string, adminToken: string, opts: {
55
+ orgId: string;
56
+ kiciDir: string;
57
+ client?: SecretSeedClient;
58
+ }): Promise<SeededSecrets>;
59
+ //# sourceMappingURL=secret-seed.d.ts.map
@@ -0,0 +1,112 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { parseSecretsFile } from "../test-runner/secrets-file.js";
3
+ import path from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { parse } from "yaml";
6
+ import { AdminApiClient } from "@kici-dev/orchestrator";
7
+ //#region src/local-plane/secret-seed.ts
8
+ /**
9
+ * Seed a project's local `secrets.yaml` (and `.kici/.secrets` INI) contexts into
10
+ * the local dev plane's real `scoped_secrets` store, preserving context/scope
11
+ * structure so the plane's real `SecretResolver` honors scoping at dispatch —
12
+ * the routed local run resolves scoped secrets through the plane's real
13
+ * resolver rather than a flat merge.
14
+ *
15
+ * Local secret files are READ-ONLY here: values are only READ and transit into
16
+ * the local plane's Postgres via the admin secret API. Nothing is written back
17
+ * to a secret file, and nothing leaves this machine (the plane is local).
18
+ *
19
+ * Scope model: each `secrets.yaml` top-level key is a context name. The seeder
20
+ * creates a context of that name, binds it to a scope pattern equal to the
21
+ * context name, and stores each secret at that scope. So a workflow job bound to
22
+ * context `production` resolves `production`-scoped secrets through the resolver.
23
+ */
24
+ /**
25
+ * Read a file, returning null when it does not exist.
26
+ */
27
+ async function readFileOrNull(filePath) {
28
+ try {
29
+ return await readFile(filePath, "utf-8");
30
+ } catch (err) {
31
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return null;
32
+ throw err;
33
+ }
34
+ }
35
+ /**
36
+ * Parse `secrets.yaml`, PRESERVING its top-level context structure (a flat merge
37
+ * would collapse the contexts). Top-level keys are context names; values are
38
+ * key→value maps. Non-scalar leaf values are stringified.
39
+ */
40
+ function parseSecretsYamlContexts(content) {
41
+ const parsed = parse(content);
42
+ if (!parsed || typeof parsed !== "object") return {};
43
+ const out = {};
44
+ for (const [contextName, contextSecrets] of Object.entries(parsed)) if (contextSecrets && typeof contextSecrets === "object" && !Array.isArray(contextSecrets)) {
45
+ const map = {};
46
+ for (const [key, value] of Object.entries(contextSecrets)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") map[key] = String(value);
47
+ if (Object.keys(map).length > 0) out[contextName] = map;
48
+ }
49
+ return out;
50
+ }
51
+ /**
52
+ * Collect the per-context secrets from a project's `.kici/` directory,
53
+ * preserving context structure. Reads `.kici/.secrets` (INI sections) and
54
+ * `.kici/secrets.yaml` (top-level contexts). The yaml layer wins on key
55
+ * collisions within a context. Flat (context-less) secrets are ignored here —
56
+ * a routed job only resolves secrets for the contexts it binds.
57
+ */
58
+ async function loadLocalSecretContexts(kiciDir) {
59
+ const contexts = {};
60
+ const iniContent = await readFileOrNull(path.join(kiciDir, ".secrets"));
61
+ if (iniContent !== null) {
62
+ const parsed = parseSecretsFile(iniContent);
63
+ for (const [ctx, vals] of Object.entries(parsed.contexts)) contexts[ctx] = {
64
+ ...contexts[ctx] ?? {},
65
+ ...vals
66
+ };
67
+ }
68
+ const yamlContent = await readFileOrNull(path.join(kiciDir, "secrets.yaml"));
69
+ if (yamlContent !== null) for (const [ctx, vals] of Object.entries(parseSecretsYamlContexts(yamlContent))) contexts[ctx] = {
70
+ ...contexts[ctx] ?? {},
71
+ ...vals
72
+ };
73
+ return contexts;
74
+ }
75
+ /**
76
+ * Seed the local plane's `scoped_secrets` store from a project's `secrets.yaml`
77
+ * / `.secrets` contexts. Idempotent: `createContext` upserts, `bind` is a no-op
78
+ * when the scope is already bound, and `setSecret` overwrites in place. Returns
79
+ * the contexts + secret-key count seeded (never the values).
80
+ */
81
+ async function seedLocalSecrets(planeUrl, adminToken, opts) {
82
+ const client = opts.client ?? new AdminApiClient(planeUrl, adminToken);
83
+ const contexts = await loadLocalSecretContexts(opts.kiciDir);
84
+ const seeded = [];
85
+ let secretCount = 0;
86
+ for (const [contextName, secrets] of Object.entries(contexts)) {
87
+ await client.createContext({
88
+ orgId: opts.orgId,
89
+ name: contextName,
90
+ allowLocalExecution: true
91
+ });
92
+ await client.bindContext({
93
+ orgId: opts.orgId,
94
+ name: contextName,
95
+ scopePattern: contextName,
96
+ hostPattern: "**"
97
+ });
98
+ for (const [key, value] of Object.entries(secrets)) {
99
+ await client.setSecret(opts.orgId, contextName, key, value);
100
+ secretCount++;
101
+ }
102
+ seeded.push(contextName);
103
+ }
104
+ return {
105
+ contexts: seeded,
106
+ secretCount
107
+ };
108
+ }
109
+ //#endregion
110
+ export { loadLocalSecretContexts, seedLocalSecrets };
111
+
112
+ //# sourceMappingURL=secret-seed.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * LocalSourceProvider — resolve the workdir a `kici run --local` dispatch
3
+ * registers as a `file://` source for the plane.
4
+ *
5
+ * The plane orchestrator's local provider fetches the lock from
6
+ * `<repoBasePath>/.kici/kici.lock.json` and the ephemeral agent clones
7
+ * `file://<repoBasePath>` at a committed sha. So the resolved workdir must be a
8
+ * git repo whose HEAD carries the workflow + lock the run should execute.
9
+ *
10
+ * Two profiles:
11
+ * - **default (isolated):** an isolated tmp clone at HEAD with the local overlay
12
+ * (dirty + untracked files) applied and committed onto a `kici-local` branch,
13
+ * so the clone-by-sha the agent performs sees uncommitted work without
14
+ * touching the developer's tree.
15
+ * - **`--in-place`:** the repo root directly (ambient state, the profile the CI
16
+ * deploy jobs use). Cleanup is a no-op.
17
+ */
18
+ /** The `kici-local` branch the isolated profile commits its overlay onto. */
19
+ export declare const LOCAL_RUN_BRANCH = "kici-local";
20
+ /** A resolved workdir plus the git coordinates the plane trigger needs. */
21
+ export interface ResolvedWorkdir {
22
+ /** Absolute path registered as the local source `repoBasePath`. */
23
+ dir: string;
24
+ /** Git ref the synthetic push carries (`refs/heads/<branch>`). */
25
+ ref: string;
26
+ /** Committed HEAD sha the agent clones + checks out. */
27
+ sha: string;
28
+ /** Branch short name (matched by the workflow's push filter). */
29
+ branch: string;
30
+ /** Remove the tmp workdir (no-op for `--in-place`). */
31
+ cleanup: () => Promise<void>;
32
+ }
33
+ /**
34
+ * Resolve the workdir for an offline routed run.
35
+ *
36
+ * @param opts.inPlace - Use the repo root directly instead of an isolated clone.
37
+ * @param opts.repoRoot - The developer's repo root.
38
+ */
39
+ export declare function resolveWorkdir(opts: {
40
+ inPlace: boolean;
41
+ repoRoot: string;
42
+ }): Promise<ResolvedWorkdir>;
43
+ //# sourceMappingURL=source-provider.d.ts.map
@@ -0,0 +1,165 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { selectOverlayFiles } from "../remote/uploader.js";
3
+ import path from "node:path";
4
+ import fs from "node:fs/promises";
5
+ import { execFileSync, execSync } from "node:child_process";
6
+ import os from "node:os";
7
+ import { randomBytes } from "node:crypto";
8
+ //#region src/local-plane/source-provider.ts
9
+ /**
10
+ * LocalSourceProvider — resolve the workdir a `kici run --local` dispatch
11
+ * registers as a `file://` source for the plane.
12
+ *
13
+ * The plane orchestrator's local provider fetches the lock from
14
+ * `<repoBasePath>/.kici/kici.lock.json` and the ephemeral agent clones
15
+ * `file://<repoBasePath>` at a committed sha. So the resolved workdir must be a
16
+ * git repo whose HEAD carries the workflow + lock the run should execute.
17
+ *
18
+ * Two profiles:
19
+ * - **default (isolated):** an isolated tmp clone at HEAD with the local overlay
20
+ * (dirty + untracked files) applied and committed onto a `kici-local` branch,
21
+ * so the clone-by-sha the agent performs sees uncommitted work without
22
+ * touching the developer's tree.
23
+ * - **`--in-place`:** the repo root directly (ambient state, the profile the CI
24
+ * deploy jobs use). Cleanup is a no-op.
25
+ */
26
+ /** The `kici-local` branch the isolated profile commits its overlay onto. */
27
+ const LOCAL_RUN_BRANCH = "kici-local";
28
+ /** Max number of overlay files copied concurrently. */
29
+ const COPY_BATCH_SIZE = 32;
30
+ /**
31
+ * Resolve the workdir for an offline routed run.
32
+ *
33
+ * @param opts.inPlace - Use the repo root directly instead of an isolated clone.
34
+ * @param opts.repoRoot - The developer's repo root.
35
+ */
36
+ async function resolveWorkdir(opts) {
37
+ requireGitRepo(opts.repoRoot);
38
+ return opts.inPlace ? resolveInPlace(opts.repoRoot) : resolveIsolated(opts.repoRoot);
39
+ }
40
+ /** In-place: the repo root, triggered at its current branch + HEAD. */
41
+ function resolveInPlace(repoRoot) {
42
+ const branch = gitOut(repoRoot, [
43
+ "rev-parse",
44
+ "--abbrev-ref",
45
+ "HEAD"
46
+ ]);
47
+ const sha = gitOut(repoRoot, ["rev-parse", "HEAD"]);
48
+ return {
49
+ dir: repoRoot,
50
+ ref: `refs/heads/${branch}`,
51
+ sha,
52
+ branch,
53
+ cleanup: async () => {}
54
+ };
55
+ }
56
+ /**
57
+ * Isolated: a tmp clone at HEAD with the local overlay applied and committed
58
+ * onto the `kici-local` branch, then commits so the agent's clone-by-sha
59
+ * carries the work.
60
+ */
61
+ async function resolveIsolated(repoRoot) {
62
+ const base = os.tmpdir();
63
+ const tmpDir = path.join(base, `kici-local-run-${randomBytes(3).toString("hex")}`);
64
+ const { sha, existingFiles, deletedFiles } = await selectOverlayFiles(repoRoot);
65
+ execSync(`git clone --no-hardlinks --quiet ${shellQuote(repoRoot)} ${shellQuote(tmpDir)}`, { stdio: "ignore" });
66
+ execSync(`git checkout --quiet ${sha}`, {
67
+ cwd: tmpDir,
68
+ stdio: "ignore"
69
+ });
70
+ await applyOverlay(repoRoot, tmpDir, existingFiles, deletedFiles);
71
+ execFileSync("git", [
72
+ "checkout",
73
+ "-B",
74
+ LOCAL_RUN_BRANCH
75
+ ], {
76
+ cwd: tmpDir,
77
+ stdio: "ignore"
78
+ });
79
+ execFileSync("git", ["add", "-A"], {
80
+ cwd: tmpDir,
81
+ stdio: "ignore"
82
+ });
83
+ if (gitOut(tmpDir, ["status", "--porcelain"])) execFileSync("git", [
84
+ "-c",
85
+ "user.email=local@kici.dev",
86
+ "-c",
87
+ "user.name=kici local",
88
+ "commit",
89
+ "--no-verify",
90
+ "--no-gpg-sign",
91
+ "-m",
92
+ "kici run --local overlay"
93
+ ], {
94
+ cwd: tmpDir,
95
+ stdio: "ignore"
96
+ });
97
+ const committed = gitOut(tmpDir, ["rev-parse", "HEAD"]);
98
+ return {
99
+ dir: tmpDir,
100
+ ref: `refs/heads/${LOCAL_RUN_BRANCH}`,
101
+ sha: committed,
102
+ branch: LOCAL_RUN_BRANCH,
103
+ cleanup: async () => {
104
+ await fs.rm(tmpDir, {
105
+ recursive: true,
106
+ force: true
107
+ });
108
+ }
109
+ };
110
+ }
111
+ /** Copy overlay files onto the clone and remove local deletions. */
112
+ async function applyOverlay(repoRoot, tmpDir, existingFiles, deletedFiles) {
113
+ for (let i = 0; i < existingFiles.length; i += COPY_BATCH_SIZE) {
114
+ const batch = existingFiles.slice(i, i + COPY_BATCH_SIZE);
115
+ await Promise.all(batch.map((file) => copyOverlayFile(repoRoot, tmpDir, file)));
116
+ }
117
+ for (let i = 0; i < deletedFiles.length; i += COPY_BATCH_SIZE) {
118
+ const batch = deletedFiles.slice(i, i + COPY_BATCH_SIZE);
119
+ await Promise.all(batch.map((file) => fs.rm(path.join(tmpDir, file), { force: true })));
120
+ }
121
+ }
122
+ /**
123
+ * Copy a single overlay file, preserving its mode. Symlinks are recreated as
124
+ * links (not dereferenced) — the same shape the remote tarball preserves.
125
+ */
126
+ async function copyOverlayFile(repoRoot, tmpDir, file) {
127
+ const src = path.join(repoRoot, file);
128
+ const dest = path.join(tmpDir, file);
129
+ await fs.mkdir(path.dirname(dest), { recursive: true });
130
+ const srcStat = await fs.lstat(src);
131
+ if (srcStat.isSymbolicLink()) {
132
+ const target = await fs.readlink(src);
133
+ await fs.rm(dest, { force: true });
134
+ await fs.symlink(target, dest);
135
+ return;
136
+ }
137
+ await fs.copyFile(src, dest);
138
+ await fs.chmod(dest, srcStat.mode);
139
+ }
140
+ /** Throw an actionable error if `repoRoot` is not a git work tree. */
141
+ function requireGitRepo(repoRoot) {
142
+ try {
143
+ execSync("git rev-parse --is-inside-work-tree", {
144
+ cwd: repoRoot,
145
+ stdio: "ignore"
146
+ });
147
+ } catch {
148
+ throw new Error(`kici run --local needs a git repository, but "${repoRoot}" is not inside a git work tree. Initialize a repo (git init) before running.`);
149
+ }
150
+ }
151
+ /** Run a git command in `cwd` and return trimmed stdout. */
152
+ function gitOut(cwd, args) {
153
+ return execFileSync("git", args, {
154
+ cwd,
155
+ encoding: "utf8"
156
+ }).trim();
157
+ }
158
+ /** Minimal single-quote shell escaping for paths passed to git via execSync. */
159
+ function shellQuote(value) {
160
+ return `'${value.replace(/'/g, `'\\''`)}'`;
161
+ }
162
+ //#endregion
163
+ export { LOCAL_RUN_BRANCH, resolveWorkdir };
164
+
165
+ //# sourceMappingURL=source-provider.js.map