@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.
- package/dist/cli-banner.d.ts +6 -3
- package/dist/cli-banner.js +13 -5
- package/dist/cli.js +66 -43
- package/dist/commands/check-mode.d.ts +1 -1
- package/dist/commands/compile.js +3 -3
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +6 -3
- package/dist/commands/init.js +1 -1
- package/dist/commands/local-trust-root.d.ts +6 -0
- package/dist/commands/local-trust-root.js +52 -0
- package/dist/commands/local.d.ts +39 -0
- package/dist/commands/local.js +137 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +40 -0
- package/dist/commands/logout.js +7 -0
- package/dist/commands/preview.js +1 -1
- package/dist/commands/run-banner.d.ts +31 -0
- package/dist/commands/run-banner.js +25 -0
- package/dist/commands/run-routed.d.ts +53 -0
- package/dist/commands/run-routed.js +176 -0
- package/dist/commands/run.d.ts +0 -17
- package/dist/commands/run.js +7 -50
- package/dist/commands/secrets-list.d.ts +2 -2
- package/dist/commands/secrets-list.js +4 -4
- package/dist/commands/types.d.ts +3 -3
- package/dist/commands/types.js +4 -4
- package/dist/execution/executor.js +7 -7
- package/dist/generators/secrets-dts.d.ts +9 -9
- package/dist/generators/secrets-dts.js +12 -12
- package/dist/llm-context/llms-architecture.txt +2 -2
- package/dist/llm-context/llms-cli.txt +674 -340
- package/dist/llm-context/llms-features.txt +334 -336
- package/dist/llm-context/llms-full.txt +1207 -892
- package/dist/llm-context/llms-getting-started.txt +7 -15
- package/dist/llm-context/llms-patterns.txt +5 -23
- package/dist/llm-context/llms-providers.txt +10 -1
- package/dist/llm-context/llms-sdk.txt +12 -12
- package/dist/llm-context/llms.txt +5 -5
- package/dist/local-executor/secret-loader.d.ts +1 -1
- package/dist/local-plane/orchestrator-process.d.ts +65 -0
- package/dist/local-plane/orchestrator-process.js +115 -0
- package/dist/local-plane/paths.d.ts +39 -0
- package/dist/local-plane/paths.js +54 -0
- package/dist/local-plane/plane-manager.d.ts +151 -0
- package/dist/local-plane/plane-manager.js +340 -0
- package/dist/local-plane/plane-seed.d.ts +76 -0
- package/dist/local-plane/plane-seed.js +59 -0
- package/dist/local-plane/plane-trigger.d.ts +62 -0
- package/dist/local-plane/plane-trigger.js +97 -0
- package/dist/local-plane/platform-attach.d.ts +53 -0
- package/dist/local-plane/platform-attach.js +111 -0
- package/dist/local-plane/postgres.d.ts +44 -0
- package/dist/local-plane/postgres.js +165 -0
- package/dist/local-plane/resolve-plane.d.ts +37 -0
- package/dist/local-plane/resolve-plane.js +76 -0
- package/dist/local-plane/run-follow.d.ts +41 -0
- package/dist/local-plane/run-follow.js +104 -0
- package/dist/local-plane/scaler-config.d.ts +54 -0
- package/dist/local-plane/scaler-config.js +114 -0
- package/dist/local-plane/secret-seed.d.ts +59 -0
- package/dist/local-plane/secret-seed.js +112 -0
- package/dist/local-plane/source-provider.d.ts +43 -0
- package/dist/local-plane/source-provider.js +165 -0
- package/dist/local-plane/trusted-routing.d.ts +32 -0
- package/dist/local-plane/trusted-routing.js +47 -0
- package/dist/lockfile/generator.js +7 -7
- package/dist/remote/dashboard-client.d.ts +7 -7
- package/dist/remote/dashboard-client.js +7 -7
- package/dist/remote/platform-client.d.ts +1 -1
- package/dist/remote/secret-loader.d.ts +18 -0
- package/dist/{local-executor → remote}/secret-loader.js +4 -4
- package/dist/remote/secret-upload.d.ts +1 -1
- package/dist/remote/secret-upload.js +2 -2
- package/dist/remote/uploader.d.ts +1 -1
- package/dist/remote/uploader.js +2 -2
- package/dist/templates/agents-md.d.ts +1 -1
- package/dist/templates/agents-md.js +2 -2
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/job-executor.js +1 -1
- package/dist/test-runner/runs-on-display.d.ts +9 -0
- package/dist/{local-executor → test-runner}/runs-on-display.js +1 -1
- package/dist/test-runner/step-context.d.ts +1 -1
- package/dist/test-runner/step-context.js +2 -2
- package/dist/types.d.ts +6 -6
- package/package.json +7 -4
- package/sbom.spdx.json +9884 -1459
- package/dist/local-executor/dag-scheduler.js +0 -183
- package/dist/local-executor/index.js +0 -378
- package/dist/local-executor/job-runner.js +0 -411
- package/dist/local-executor/materializer.js +0 -132
- package/dist/local-executor/output-streamer.js +0 -168
- package/dist/local-executor/payload-generator.js +0 -138
- package/dist/local-executor/picker.js +0 -109
- package/dist/local-executor/to-event-payload.js +0 -21
- package/dist/local-executor/types.js +0 -2
- 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
|