@kici-dev/compiler 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -2
- package/dist/commands/compile.js +5 -1
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +77 -12
- package/dist/commands/local.d.ts +9 -2
- package/dist/commands/local.js +14 -4
- package/dist/commands/preview.js +1 -1
- package/dist/commands/report/identity.d.ts +11 -0
- package/dist/commands/report/identity.js +7 -2
- package/dist/commands/run-routed.js +4 -0
- package/dist/commands/run.js +5 -2
- package/dist/commands/runs/logs.js +3 -2
- package/dist/commands/types.d.ts +6 -1
- package/dist/commands/types.js +2 -1
- package/dist/execution/executor.js +7 -1
- package/dist/llm-context/llms-architecture.txt +73 -87
- package/dist/llm-context/llms-cli-remote.txt +53 -8
- package/dist/llm-context/llms-cli.txt +71 -36
- package/dist/llm-context/llms-features-execution.txt +52 -6
- package/dist/llm-context/llms-features.txt +137 -6
- package/dist/llm-context/llms-full.txt +558 -180
- package/dist/llm-context/llms-getting-started.txt +5 -5
- package/dist/llm-context/llms-patterns.txt +81 -5
- package/dist/llm-context/llms-providers.txt +6 -2
- package/dist/llm-context/llms-sdk-runtime.txt +33 -18
- package/dist/llm-context/llms-sdk.txt +47 -7
- package/dist/llm-context/llms.txt +8 -8
- package/dist/local-plane/orchestrator-process.d.ts +0 -8
- package/dist/local-plane/orchestrator-process.js +6 -14
- package/dist/local-plane/paths.d.ts +1 -0
- package/dist/local-plane/paths.js +1 -0
- package/dist/local-plane/plane-log.d.ts +27 -0
- package/dist/local-plane/plane-log.js +39 -0
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/local-plane/plane-trigger.d.ts +28 -0
- package/dist/local-plane/plane-trigger.js +57 -2
- package/dist/local-plane/postgres.js +9 -6
- package/dist/local-plane/run-follow.js +2 -1
- package/dist/lockfile/generator.js +25 -9
- package/dist/lockfile/hasher.d.ts +5 -13
- package/dist/lockfile/hasher.js +1 -15
- package/dist/lockfile/workspace-siblings.d.ts +46 -0
- package/dist/lockfile/workspace-siblings.js +197 -0
- package/dist/remote/output/streaming.d.ts +12 -0
- package/dist/remote/output/streaming.js +20 -1
- package/dist/remote/platform-client.d.ts +2 -0
- package/dist/templates/package-json.d.ts +9 -7
- package/dist/templates/package-json.js +11 -9
- package/dist/test-runner/job-executor.js +1 -1
- package/dist/test-runner/rule-evaluator.js +1 -1
- package/dist/types.d.ts +6 -1
- package/package.json +7 -9
- package/sbom.spdx.json +123 -123
- package/dist/postinstall.d.ts +0 -9
- package/dist/postinstall.js +0 -62
- package/hack/postinstall.mjs +0 -105
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* The source's bundle hot-reload is debounced, so a first trigger can land
|
|
12
12
|
* before the plane has registered the (re-pointed) local source. This resends
|
|
13
13
|
* the webhook after a grace window until a run appears or the timeout elapses.
|
|
14
|
+
* When the budget still runs out, the timeout is explained rather than merely
|
|
15
|
+
* reported: the plane's own Raft role and its event-log record for this
|
|
16
|
+
* delivery decide which cause is named.
|
|
14
17
|
*/
|
|
15
18
|
/** A minimal admin-read client (AdminApiClient.get) — injectable for tests. */
|
|
16
19
|
export interface RunDiscoveryClient {
|
|
@@ -58,7 +61,32 @@ export interface TriggerRunOptions {
|
|
|
58
61
|
pollIntervalMs?: number;
|
|
59
62
|
resendAfterMs?: number;
|
|
60
63
|
timeoutMs?: number;
|
|
64
|
+
/** Workdir the plane's local source points at — named in the lock-file diagnosis. */
|
|
65
|
+
repoBasePath?: string;
|
|
66
|
+
/** Plane log path — named when nothing else explains the timeout. */
|
|
67
|
+
logPath?: string;
|
|
61
68
|
}
|
|
69
|
+
/** What the diagnosis knows about the trigger that just timed out. */
|
|
70
|
+
export interface TriggerTimeoutContext {
|
|
71
|
+
/** Routing-key-scoped delivery id, or null when the plane never accepted the webhook. */
|
|
72
|
+
deliveryId: string | null;
|
|
73
|
+
orgId: string;
|
|
74
|
+
/** Commit the synthetic push carried (the workdir's overlay commit). */
|
|
75
|
+
sha: string;
|
|
76
|
+
/** Absolute path the plane's local source points at — the run's workdir. */
|
|
77
|
+
repoBasePath?: string;
|
|
78
|
+
/** Plane log path, named in the fallback so the developer has somewhere to look. */
|
|
79
|
+
logPath?: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Explain a trigger timeout in one line, from what the plane itself recorded.
|
|
83
|
+
*
|
|
84
|
+
* Ordered most-specific-first, and every branch is a fact read back off the
|
|
85
|
+
* plane rather than an inference: the Raft role it reports, then the status it
|
|
86
|
+
* wrote for THIS delivery, then the delivery id + log path so the developer has
|
|
87
|
+
* a thread to pull even when neither surface answered.
|
|
88
|
+
*/
|
|
89
|
+
export declare function diagnoseTriggerTimeout(client: RunDiscoveryClient, ctx: TriggerTimeoutContext): Promise<string>;
|
|
62
90
|
/**
|
|
63
91
|
* Trigger the run and resolve its runId. Sends the synthetic push, then polls
|
|
64
92
|
* the admin runs list filtered by this webhook's routing-key-scoped delivery id
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import { EventLogStatus } from "@kici-dev/engine";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { AdminApiClient } from "@kici-dev/orchestrator";
|
|
4
5
|
//#region src/local-plane/plane-trigger.ts
|
|
@@ -15,6 +16,9 @@ import { AdminApiClient } from "@kici-dev/orchestrator";
|
|
|
15
16
|
* The source's bundle hot-reload is debounced, so a first trigger can land
|
|
16
17
|
* before the plane has registered the (re-pointed) local source. This resends
|
|
17
18
|
* the webhook after a grace window until a run appears or the timeout elapses.
|
|
19
|
+
* When the budget still runs out, the timeout is explained rather than merely
|
|
20
|
+
* reported: the plane's own Raft role and its event-log record for this
|
|
21
|
+
* delivery decide which cause is named.
|
|
18
22
|
*/
|
|
19
23
|
/**
|
|
20
24
|
* Build the GitHub-shaped webhook request the plane's local provider normalizer
|
|
@@ -62,6 +66,50 @@ async function sendLocalTrigger(planeUrl, req) {
|
|
|
62
66
|
deliveryId
|
|
63
67
|
};
|
|
64
68
|
}
|
|
69
|
+
/** Raft role the plane reports, or null when `/cluster/health` cannot be read. */
|
|
70
|
+
async function readPlaneRole(client) {
|
|
71
|
+
try {
|
|
72
|
+
const health = await client.get("/cluster/health");
|
|
73
|
+
return typeof health.role === "string" ? health.role : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The plane's own record of this delivery, or null when there is none to read. */
|
|
79
|
+
async function readDelivery(client, orgId, deliveryId) {
|
|
80
|
+
try {
|
|
81
|
+
const qs = new URLSearchParams({
|
|
82
|
+
deliveryId,
|
|
83
|
+
orgId,
|
|
84
|
+
limit: "1"
|
|
85
|
+
});
|
|
86
|
+
return (await client.get(`/api/v1/admin/event-log?${qs}`)).deliveries?.[0] ?? null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Explain a trigger timeout in one line, from what the plane itself recorded.
|
|
93
|
+
*
|
|
94
|
+
* Ordered most-specific-first, and every branch is a fact read back off the
|
|
95
|
+
* plane rather than an inference: the Raft role it reports, then the status it
|
|
96
|
+
* wrote for THIS delivery, then the delivery id + log path so the developer has
|
|
97
|
+
* a thread to pull even when neither surface answered.
|
|
98
|
+
*/
|
|
99
|
+
async function diagnoseTriggerTimeout(client, ctx) {
|
|
100
|
+
const role = await readPlaneRole(client);
|
|
101
|
+
if (role !== null && role !== "leader") return `plane is not leader yet (election grace period) — it reports Raft role "${role}". A single-node plane should elect itself within seconds; check ${ctx.logPath ?? "the plane log (`kici local logs`)"} for "self-electing as leader".`;
|
|
102
|
+
const delivery = ctx.deliveryId ? await readDelivery(client, ctx.orgId, ctx.deliveryId) : null;
|
|
103
|
+
if (delivery?.status === EventLogStatus.enum.lockfile_missing) {
|
|
104
|
+
const where = ctx.repoBasePath ? ` in ${ctx.repoBasePath}` : "";
|
|
105
|
+
return `no kici.lock.json at ${ctx.sha}${where} — the plane resolved no lock file for this commit, so nothing matched. Make sure .kici/kici.lock.json is committed or present in the working tree the run packs.`;
|
|
106
|
+
}
|
|
107
|
+
if (delivery?.status) {
|
|
108
|
+
const detail = delivery.errorMessage ? ` (${delivery.errorMessage})` : "";
|
|
109
|
+
return `the plane recorded delivery ${ctx.deliveryId} as "${delivery.status}"${detail} but created no run.`;
|
|
110
|
+
}
|
|
111
|
+
return `no run appeared for ${ctx.deliveryId ? `delivery ${ctx.deliveryId}` : "this trigger — the plane never accepted the webhook (no delivery id came back)"}${ctx.logPath ? ` — see ${ctx.logPath}` : ""}`;
|
|
112
|
+
}
|
|
65
113
|
/**
|
|
66
114
|
* Trigger the run and resolve its runId. Sends the synthetic push, then polls
|
|
67
115
|
* the admin runs list filtered by this webhook's routing-key-scoped delivery id
|
|
@@ -90,7 +138,14 @@ async function triggerRun(planeUrl, adminToken, input, opts = {}) {
|
|
|
90
138
|
}
|
|
91
139
|
await sleep(pollIntervalMs);
|
|
92
140
|
}
|
|
93
|
-
|
|
141
|
+
const cause = await diagnoseTriggerTimeout(client, {
|
|
142
|
+
deliveryId,
|
|
143
|
+
orgId: input.orgId,
|
|
144
|
+
sha: input.sha,
|
|
145
|
+
...opts.repoBasePath !== void 0 && { repoBasePath: opts.repoBasePath },
|
|
146
|
+
...opts.logPath !== void 0 && { logPath: opts.logPath }
|
|
147
|
+
});
|
|
148
|
+
throw new Error(`offline run: ${cause}`);
|
|
94
149
|
}
|
|
95
150
|
/** Return the run created by this webhook delivery, or null when none yet. */
|
|
96
151
|
async function findRunByDelivery(client, deliveryId) {
|
|
@@ -105,6 +160,6 @@ function sleep(ms) {
|
|
|
105
160
|
return new Promise((r) => setTimeout(r, ms));
|
|
106
161
|
}
|
|
107
162
|
//#endregion
|
|
108
|
-
export { buildLocalTriggerRequest, sendLocalTrigger, triggerRun };
|
|
163
|
+
export { buildLocalTriggerRequest, diagnoseTriggerTimeout, sendLocalTrigger, triggerRun };
|
|
109
164
|
|
|
110
165
|
//# sourceMappingURL=plane-trigger.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
2
|
import { planePaths, planePorts } from "./paths.js";
|
|
3
|
+
import { rotatePlaneLogIfOversized } from "./plane-log.js";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import fs from "node:fs";
|
|
@@ -104,14 +105,13 @@ async function embeddedClusterIsServing(port) {
|
|
|
104
105
|
/**
|
|
105
106
|
* Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
|
|
106
107
|
* this CLI process (embedded-postgres's in-process server is killed by its own
|
|
107
|
-
* exit hook, so it cannot back a warm plane).
|
|
108
|
-
*
|
|
108
|
+
* exit hook, so it cannot back a warm plane). The caller decides whether a
|
|
109
|
+
* cluster is already serving; this always starts one.
|
|
109
110
|
*/
|
|
110
111
|
async function defaultEmbeddedDaemon(port) {
|
|
111
|
-
const { pgData,
|
|
112
|
-
if (await embeddedClusterIsServing(port)) return;
|
|
112
|
+
const { pgData, pgLogFile } = planePaths();
|
|
113
113
|
const pgCtl = resolvePgCtl();
|
|
114
|
-
await $`${pgCtl} -D ${pgData} -o ${`-p ${port}`} -l ${
|
|
114
|
+
await $`${pgCtl} -D ${pgData} -o ${`-p ${port}`} -l ${pgLogFile} -w start`.quiet();
|
|
115
115
|
}
|
|
116
116
|
/** Stop the detached embedded postmaster (handle-independent, reads the data dir). */
|
|
117
117
|
async function stopEmbeddedDaemon() {
|
|
@@ -141,7 +141,10 @@ async function startPlanePostgres(opts = {}) {
|
|
|
141
141
|
const url = `postgres://kici:kici@127.0.0.1:${port}/kici_local`;
|
|
142
142
|
if (!(opts.forcePodman || process.env.KICI_LOCAL_PG_MODE === "podman")) try {
|
|
143
143
|
await ensureEmbeddedCluster(port);
|
|
144
|
-
await
|
|
144
|
+
if (!await embeddedClusterIsServing(port)) {
|
|
145
|
+
rotatePlaneLogIfOversized(planePaths().pgLogFile);
|
|
146
|
+
await embeddedDaemon(port);
|
|
147
|
+
}
|
|
145
148
|
return {
|
|
146
149
|
url,
|
|
147
150
|
kind: "embedded",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import { unwrapStoredLogLine } from "../remote/output/streaming.js";
|
|
2
3
|
import { ExecutionJobStatus, ExecutionRunStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
|
|
3
4
|
import { AdminApiClient } from "@kici-dev/orchestrator";
|
|
4
5
|
//#region src/local-plane/run-follow.ts
|
|
@@ -131,7 +132,7 @@ async function drainLogs(client, runId, cursors, onLine) {
|
|
|
131
132
|
const qs = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
|
|
132
133
|
const page = await client.get(`/api/v1/admin/runs/${runId}/jobs/${job.jobId}/steps/${step.stepIndex}/logs${qs}`);
|
|
133
134
|
for (const l of page.lines) {
|
|
134
|
-
onLine(l.value);
|
|
135
|
+
onLine(unwrapStoredLogLine(l.value));
|
|
135
136
|
emitted++;
|
|
136
137
|
}
|
|
137
138
|
cursor = page.nextCursor ?? String(page.totalLines);
|
|
@@ -2,18 +2,20 @@ import "../rolldown-runtime-ClRpJifh.js";
|
|
|
2
2
|
import { compilerError } from "../errors/formatter.js";
|
|
3
3
|
import { locationForJob, locationForWorkflow } from "../errors/source-location.js";
|
|
4
4
|
import "../errors/index.js";
|
|
5
|
+
import { computeSiblingsDigest } from "./workspace-siblings.js";
|
|
5
6
|
import { BREAKING_FLOOR as BREAKING_FLOOR$1, SCHEMA_VERSION as SCHEMA_VERSION$1 } from "../types.js";
|
|
6
|
-
import { computeContentHash } from "./hasher.js";
|
|
7
|
+
import { COMPILE_SCHEMA_VERSION, computeContentHash } from "./hasher.js";
|
|
7
8
|
import { resolveHashFiles } from "./hash-files.js";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
10
|
-
import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject
|
|
11
|
+
import { getDynamicJobGitCredentials, getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject } from "@kici-dev/sdk";
|
|
11
12
|
import { sha256 } from "@kici-dev/core";
|
|
12
13
|
import { PackageManager, detectPackageManagerSync, detectYarnFlavorSync } from "@kici-dev/core/package-manager";
|
|
13
|
-
import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveContentFormat, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
|
|
14
|
+
import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveContentFormat, resolveWhenToRunOn, stripStatefulRegexFlags, validateResourceRequest } from "@kici-dev/engine";
|
|
14
15
|
import { assertSafeRegex } from "@kici-dev/engine/safe-regex";
|
|
15
|
-
import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
|
|
16
16
|
import { execSync } from "node:child_process";
|
|
17
|
+
import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
|
|
18
|
+
import { normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk/internal";
|
|
17
19
|
//#region src/lockfile/generator.ts
|
|
18
20
|
/**
|
|
19
21
|
* Courtesy compatibility warning for `kici compile`.
|
|
@@ -124,6 +126,7 @@ function formatExportRef(source) {
|
|
|
124
126
|
function generateLockFile(workflowsWithSource) {
|
|
125
127
|
const gitRoot = detectGitRoot();
|
|
126
128
|
const lockfileHash = computeLockfileHash(gitRoot);
|
|
129
|
+
const siblingsDigest = computeSiblingsDigest(gitRoot);
|
|
127
130
|
const firstSource = workflowsWithSource[0]?.source;
|
|
128
131
|
const topLevelSource = firstSource ? {
|
|
129
132
|
file: path.relative(gitRoot, firstSource.file).replaceAll("\\", "/"),
|
|
@@ -147,6 +150,7 @@ function generateLockFile(workflowsWithSource) {
|
|
|
147
150
|
source: topLevelSource,
|
|
148
151
|
contentHash,
|
|
149
152
|
...lockfileHash && { lockfileHash },
|
|
153
|
+
...siblingsDigest && { siblingsDigest },
|
|
150
154
|
workflows
|
|
151
155
|
};
|
|
152
156
|
}
|
|
@@ -169,8 +173,8 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
|
|
|
169
173
|
resolvedHashFiles = resolved.resolvedPaths;
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
|
-
const contentHash = bundleSource !== void 0 ? computeContentHash(bundleSource,
|
|
173
|
-
const compileSchemaVersion = bundleSource !== void 0 ?
|
|
176
|
+
const contentHash = bundleSource !== void 0 ? computeContentHash(bundleSource, COMPILE_SCHEMA_VERSION, assetDigest) : "";
|
|
177
|
+
const compileSchemaVersion = bundleSource !== void 0 ? COMPILE_SCHEMA_VERSION : 0;
|
|
174
178
|
return {
|
|
175
179
|
name: workflow.name,
|
|
176
180
|
source: {
|
|
@@ -220,15 +224,25 @@ function toArray(v) {
|
|
|
220
224
|
return Array.isArray(v) ? v : [v];
|
|
221
225
|
}
|
|
222
226
|
/**
|
|
227
|
+
* The `/pattern/flags` shape the lock file stores.
|
|
228
|
+
*
|
|
229
|
+
* The flag class must name every ECMAScript flag, not just the ones that
|
|
230
|
+
* predate ES2022. When it read `[gimsuy]`, a `d`- or `v`-flagged regex failed
|
|
231
|
+
* to unwrap, so `pattern` became the whole `/release-\d+/v` string — which
|
|
232
|
+
* `new RegExp` happily accepts as a pattern matching literal slashes. The lock
|
|
233
|
+
* then stored `/\/release-\d+\/v/`, which compiles green and can never match.
|
|
234
|
+
*/
|
|
235
|
+
const REGEX_ENTRY = /^\/(.+)\/([dgimsuvy]*)$/;
|
|
236
|
+
/**
|
|
223
237
|
* Normalize one regex entry to the lock's `/pattern/flags` form, rejecting an
|
|
224
238
|
* invalid or ReDoS-prone pattern at compile time so a catastrophic pattern fails
|
|
225
239
|
* `kici compile` with author feedback instead of reaching the orchestrator.
|
|
226
240
|
*/
|
|
227
241
|
function serializeRegexEntry(entry, ctx) {
|
|
228
242
|
const source = entry instanceof RegExp ? `/${entry.source}/${entry.flags}` : entry;
|
|
229
|
-
const wrapped =
|
|
243
|
+
const wrapped = REGEX_ENTRY.exec(source);
|
|
230
244
|
const pattern = wrapped ? wrapped[1] : source;
|
|
231
|
-
const flags = wrapped ? wrapped[2] : "";
|
|
245
|
+
const flags = stripStatefulRegexFlags(wrapped ? wrapped[2] : "");
|
|
232
246
|
let re;
|
|
233
247
|
try {
|
|
234
248
|
re = new RegExp(pattern, flags);
|
|
@@ -605,6 +619,7 @@ function transformJobs(jobs, configPath, gitRoot) {
|
|
|
605
619
|
if (isDynamicJobFn(jobOrFactory)) {
|
|
606
620
|
const groupName = getDynamicJobGroup(jobOrFactory);
|
|
607
621
|
const declaredNeeds = getDynamicJobNeeds(jobOrFactory);
|
|
622
|
+
const declaredGitCredentials = getDynamicJobGitCredentials(jobOrFactory);
|
|
608
623
|
const resolvedNeeds = declaredNeeds ? resolveNeedsForLock(declaredNeeds, uuidToName) : void 0;
|
|
609
624
|
return {
|
|
610
625
|
_type: "dynamic",
|
|
@@ -616,7 +631,8 @@ function transformJobs(jobs, configPath, gitRoot) {
|
|
|
616
631
|
...resolvedNeeds && {
|
|
617
632
|
needs: resolvedNeeds.needs,
|
|
618
633
|
resultAware: true
|
|
619
|
-
}
|
|
634
|
+
},
|
|
635
|
+
...declaredGitCredentials && { gitCredentials: declaredGitCredentials }
|
|
620
636
|
};
|
|
621
637
|
}
|
|
622
638
|
const j = jobOrFactory;
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compile schema version
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Bumped 3 → 4: the agent artifact switched from a Rolldown-bundled
|
|
7
|
-
* `.compiled.mjs` to a raw `.kici/` source tarball that the agent extracts
|
|
8
|
-
* and imports via the shared oxc-transform ESM loader hook.
|
|
9
|
-
*
|
|
10
|
-
* Bumped 4 → 5: the hash input is now line-ending-normalized (CRLF → LF) so a
|
|
11
|
-
* lockfile produced on Linux (LF) matches the agent's hash on Windows, where
|
|
12
|
-
* Git's `core.autocrlf=true` system default rewrites checked-out text files to
|
|
13
|
-
* CRLF. Old lockfiles must be regenerated via `kici compile`.
|
|
2
|
+
* Compile schema version, re-exported from its single definition in
|
|
3
|
+
* `@kici-dev/core/kici-source-digest` — where it sits beside the digest it
|
|
4
|
+
* qualifies, so the compiler that writes a `contentHash` and the agent that
|
|
5
|
+
* re-verifies it cannot drift onto different numbers.
|
|
14
6
|
*/
|
|
15
|
-
export
|
|
7
|
+
export { COMPILE_SCHEMA_VERSION } from '@kici-dev/core/kici-source-digest';
|
|
16
8
|
/**
|
|
17
9
|
* Compute content hash for a compiled workflow bundle, optionally including asset file contents.
|
|
18
10
|
* Hash = SHA-256(schemaVersion + ":" + bundleSource + "\0" + assetDigest) when assetDigest is provided,
|
package/dist/lockfile/hasher.js
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import { COMPILE_SCHEMA_VERSION } from "@kici-dev/core/kici-source-digest";
|
|
2
3
|
import { normalizeLineEndings, sha256 } from "@kici-dev/core";
|
|
3
4
|
//#region src/lockfile/hasher.ts
|
|
4
5
|
/**
|
|
5
|
-
* Compile schema version -- bump when compilation approach changes
|
|
6
|
-
* (bundler change, bundling config, output format, etc.).
|
|
7
|
-
* This is NOT the lock file schema version.
|
|
8
|
-
*
|
|
9
|
-
* Bumped 3 → 4: the agent artifact switched from a Rolldown-bundled
|
|
10
|
-
* `.compiled.mjs` to a raw `.kici/` source tarball that the agent extracts
|
|
11
|
-
* and imports via the shared oxc-transform ESM loader hook.
|
|
12
|
-
*
|
|
13
|
-
* Bumped 4 → 5: the hash input is now line-ending-normalized (CRLF → LF) so a
|
|
14
|
-
* lockfile produced on Linux (LF) matches the agent's hash on Windows, where
|
|
15
|
-
* Git's `core.autocrlf=true` system default rewrites checked-out text files to
|
|
16
|
-
* CRLF. Old lockfiles must be regenerated via `kici compile`.
|
|
17
|
-
*/
|
|
18
|
-
const COMPILE_SCHEMA_VERSION = 5;
|
|
19
|
-
/**
|
|
20
6
|
* Compute content hash for a compiled workflow bundle, optionally including asset file contents.
|
|
21
7
|
* Hash = SHA-256(schemaVersion + ":" + bundleSource + "\0" + assetDigest) when assetDigest is provided,
|
|
22
8
|
* otherwise SHA-256(schemaVersion + ":" + bundleSource) for backward compatibility.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static resolution of the in-repo `workspace:` sibling closure `.kici` depends
|
|
3
|
+
* on, and a digest over its source.
|
|
4
|
+
*
|
|
5
|
+
* The deps tarball carries those sibling package directories **with their built
|
|
6
|
+
* output** (`packKiciDeps`), while the pointer that names it is keyed on
|
|
7
|
+
* `lockfileHash` — a hash of the package-manager lock file. Editing a sibling's
|
|
8
|
+
* source moves no package-manager lock file, so a repo whose `.kici` depends on
|
|
9
|
+
* an in-repo `workspace:` package restored that sibling's STALE build over the
|
|
10
|
+
* fresh clone on every warm-cache run. `siblingsDigest` closes that: it enters
|
|
11
|
+
* the dep pointer key, so a sibling edit is a pointer miss and the deps are
|
|
12
|
+
* rebuilt.
|
|
13
|
+
*
|
|
14
|
+
* Resolution is **static** — manifests and `git ls-files`, never an installed
|
|
15
|
+
* `node_modules`. The agent's own `collectInRepoSiblings` walks an installed
|
|
16
|
+
* tree, which cannot be used here: `kici compile` would then emit a different
|
|
17
|
+
* lock file before and after an install, and a lock file whose contents depend
|
|
18
|
+
* on whether you have installed is not hermetic.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The workspace globs declared at the repo root — `pnpm-workspace.yaml`'s
|
|
22
|
+
* `packages:` first, then `package.json#workspaces` (npm / yarn / bun). Empty
|
|
23
|
+
* when the repo declares no workspace, in which case a `workspace:` specifier
|
|
24
|
+
* resolves to nothing and is skipped rather than guessed at.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readWorkspaceGlobs(gitRoot: string): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Every in-repo sibling directory `.kici` depends on, transitively, as sorted
|
|
29
|
+
* repo-relative POSIX paths.
|
|
30
|
+
*
|
|
31
|
+
* `.kici` itself is excluded: its own source is already covered by
|
|
32
|
+
* `contentHash`, and including it here would make a `.kici` edit invalidate the
|
|
33
|
+
* dep cache for no reason.
|
|
34
|
+
*/
|
|
35
|
+
export declare function collectInRepoSiblings(gitRoot: string, kiciDir?: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* SHA-256 over the git-tracked source of every resolved sibling directory, or
|
|
38
|
+
* null when `.kici` depends on none.
|
|
39
|
+
*
|
|
40
|
+
* `git ls-files` is install-independent and is already the compiler's world (it
|
|
41
|
+
* resolves `gitRoot` to generate the lock at all). Content is read as text and
|
|
42
|
+
* `\0`-delimited against its path, exactly as the `.kici/` tree digest is, so a
|
|
43
|
+
* rename cannot be disguised as a content edit.
|
|
44
|
+
*/
|
|
45
|
+
export declare function computeSiblingsDigest(gitRoot: string, kiciDir?: string): string | null;
|
|
46
|
+
//# sourceMappingURL=workspace-siblings.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { sha256 } from "@kici-dev/core";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
//#region src/lockfile/workspace-siblings.ts
|
|
8
|
+
/**
|
|
9
|
+
* Static resolution of the in-repo `workspace:` sibling closure `.kici` depends
|
|
10
|
+
* on, and a digest over its source.
|
|
11
|
+
*
|
|
12
|
+
* The deps tarball carries those sibling package directories **with their built
|
|
13
|
+
* output** (`packKiciDeps`), while the pointer that names it is keyed on
|
|
14
|
+
* `lockfileHash` — a hash of the package-manager lock file. Editing a sibling's
|
|
15
|
+
* source moves no package-manager lock file, so a repo whose `.kici` depends on
|
|
16
|
+
* an in-repo `workspace:` package restored that sibling's STALE build over the
|
|
17
|
+
* fresh clone on every warm-cache run. `siblingsDigest` closes that: it enters
|
|
18
|
+
* the dep pointer key, so a sibling edit is a pointer miss and the deps are
|
|
19
|
+
* rebuilt.
|
|
20
|
+
*
|
|
21
|
+
* Resolution is **static** — manifests and `git ls-files`, never an installed
|
|
22
|
+
* `node_modules`. The agent's own `collectInRepoSiblings` walks an installed
|
|
23
|
+
* tree, which cannot be used here: `kici compile` would then emit a different
|
|
24
|
+
* lock file before and after an install, and a lock file whose contents depend
|
|
25
|
+
* on whether you have installed is not hermetic.
|
|
26
|
+
*/
|
|
27
|
+
/** Dependency protocols that name a directory inside this repository. */
|
|
28
|
+
const IN_REPO_PROTOCOLS = [
|
|
29
|
+
"workspace:",
|
|
30
|
+
"file:",
|
|
31
|
+
"link:",
|
|
32
|
+
"portal:"
|
|
33
|
+
];
|
|
34
|
+
function readManifest(dir) {
|
|
35
|
+
const file = path.join(dir, "package.json");
|
|
36
|
+
if (!existsSync(file)) return null;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Every dependency specifier in a manifest, across all four dependency maps. */
|
|
44
|
+
function allSpecifiers(m) {
|
|
45
|
+
return Object.entries({
|
|
46
|
+
...m.dependencies,
|
|
47
|
+
...m.devDependencies,
|
|
48
|
+
...m.optionalDependencies,
|
|
49
|
+
...m.peerDependencies
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The workspace globs declared at the repo root — `pnpm-workspace.yaml`'s
|
|
54
|
+
* `packages:` first, then `package.json#workspaces` (npm / yarn / bun). Empty
|
|
55
|
+
* when the repo declares no workspace, in which case a `workspace:` specifier
|
|
56
|
+
* resolves to nothing and is skipped rather than guessed at.
|
|
57
|
+
*/
|
|
58
|
+
function readWorkspaceGlobs(gitRoot) {
|
|
59
|
+
const pnpmFile = path.join(gitRoot, "pnpm-workspace.yaml");
|
|
60
|
+
if (existsSync(pnpmFile)) try {
|
|
61
|
+
const doc = parse(readFileSync(pnpmFile, "utf-8"));
|
|
62
|
+
if (Array.isArray(doc?.packages)) return doc.packages.filter((p) => typeof p === "string");
|
|
63
|
+
} catch {}
|
|
64
|
+
const ws = readManifest(gitRoot)?.workspaces;
|
|
65
|
+
if (Array.isArray(ws)) return ws;
|
|
66
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Directories the workspace globs match, each carrying a manifest. Resolved with
|
|
71
|
+
* `git ls-files` so the set is exactly what is tracked — the same authority the
|
|
72
|
+
* digest itself uses, and one that needs no install.
|
|
73
|
+
*/
|
|
74
|
+
function workspacePackageDirs(gitRoot, globs) {
|
|
75
|
+
const byName = /* @__PURE__ */ new Map();
|
|
76
|
+
if (globs.length === 0) return byName;
|
|
77
|
+
let manifests;
|
|
78
|
+
try {
|
|
79
|
+
manifests = execFileSync("git", [
|
|
80
|
+
"ls-files",
|
|
81
|
+
"--",
|
|
82
|
+
"*package.json",
|
|
83
|
+
"package.json"
|
|
84
|
+
], {
|
|
85
|
+
cwd: gitRoot,
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
maxBuffer: 33554432
|
|
88
|
+
}).split("\n").filter(Boolean);
|
|
89
|
+
} catch {
|
|
90
|
+
return byName;
|
|
91
|
+
}
|
|
92
|
+
for (const rel of manifests) {
|
|
93
|
+
const dir = path.dirname(rel);
|
|
94
|
+
if (dir.split("/").includes("node_modules")) continue;
|
|
95
|
+
const m = readManifest(path.join(gitRoot, dir));
|
|
96
|
+
if (m?.name) byName.set(m.name, dir);
|
|
97
|
+
}
|
|
98
|
+
return byName;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve one specifier to a repo-relative directory, or null when it does not
|
|
102
|
+
* name one. `file:` / `link:` / `portal:` carry the path; `workspace:` carries a
|
|
103
|
+
* version range, so the package NAME is resolved through the workspace globs.
|
|
104
|
+
*/
|
|
105
|
+
function resolveSpecifier(pkgName, spec, fromDir, gitRoot, workspacePkgs) {
|
|
106
|
+
if (spec.startsWith("workspace:")) return workspacePkgs.get(pkgName) ?? null;
|
|
107
|
+
for (const proto of [
|
|
108
|
+
"file:",
|
|
109
|
+
"link:",
|
|
110
|
+
"portal:"
|
|
111
|
+
]) {
|
|
112
|
+
if (!spec.startsWith(proto)) continue;
|
|
113
|
+
const target = spec.slice(proto.length);
|
|
114
|
+
const abs = path.resolve(gitRoot, fromDir, target);
|
|
115
|
+
const rel = path.relative(gitRoot, abs).replaceAll("\\", "/");
|
|
116
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
117
|
+
return rel;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every in-repo sibling directory `.kici` depends on, transitively, as sorted
|
|
123
|
+
* repo-relative POSIX paths.
|
|
124
|
+
*
|
|
125
|
+
* `.kici` itself is excluded: its own source is already covered by
|
|
126
|
+
* `contentHash`, and including it here would make a `.kici` edit invalidate the
|
|
127
|
+
* dep cache for no reason.
|
|
128
|
+
*/
|
|
129
|
+
function collectInRepoSiblings(gitRoot, kiciDir = ".kici") {
|
|
130
|
+
const kiciManifest = readManifest(path.join(gitRoot, kiciDir));
|
|
131
|
+
if (!kiciManifest) return [];
|
|
132
|
+
const workspacePkgs = workspacePackageDirs(gitRoot, readWorkspaceGlobs(gitRoot));
|
|
133
|
+
const found = /* @__PURE__ */ new Set();
|
|
134
|
+
const queue = [{
|
|
135
|
+
dir: kiciDir,
|
|
136
|
+
manifest: kiciManifest
|
|
137
|
+
}];
|
|
138
|
+
const visited = /* @__PURE__ */ new Set([kiciDir]);
|
|
139
|
+
while (queue.length > 0) {
|
|
140
|
+
const { dir, manifest } = queue.shift();
|
|
141
|
+
for (const [name, spec] of allSpecifiers(manifest)) {
|
|
142
|
+
if (!IN_REPO_PROTOCOLS.some((p) => spec.startsWith(p))) continue;
|
|
143
|
+
const target = resolveSpecifier(name, spec, dir, gitRoot, workspacePkgs);
|
|
144
|
+
if (target === null || visited.has(target)) continue;
|
|
145
|
+
visited.add(target);
|
|
146
|
+
found.add(target);
|
|
147
|
+
const m = readManifest(path.join(gitRoot, target));
|
|
148
|
+
if (m) queue.push({
|
|
149
|
+
dir: target,
|
|
150
|
+
manifest: m
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return [...found].sort();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* SHA-256 over the git-tracked source of every resolved sibling directory, or
|
|
158
|
+
* null when `.kici` depends on none.
|
|
159
|
+
*
|
|
160
|
+
* `git ls-files` is install-independent and is already the compiler's world (it
|
|
161
|
+
* resolves `gitRoot` to generate the lock at all). Content is read as text and
|
|
162
|
+
* `\0`-delimited against its path, exactly as the `.kici/` tree digest is, so a
|
|
163
|
+
* rename cannot be disguised as a content edit.
|
|
164
|
+
*/
|
|
165
|
+
function computeSiblingsDigest(gitRoot, kiciDir = ".kici") {
|
|
166
|
+
const siblings = collectInRepoSiblings(gitRoot, kiciDir);
|
|
167
|
+
if (siblings.length === 0) return null;
|
|
168
|
+
const parts = [];
|
|
169
|
+
for (const dir of siblings) {
|
|
170
|
+
let files;
|
|
171
|
+
try {
|
|
172
|
+
files = execFileSync("git", [
|
|
173
|
+
"ls-files",
|
|
174
|
+
"--",
|
|
175
|
+
dir
|
|
176
|
+
], {
|
|
177
|
+
cwd: gitRoot,
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
maxBuffer: 67108864
|
|
180
|
+
}).split("\n").filter(Boolean).sort();
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const rel of files) {
|
|
185
|
+
let content = "";
|
|
186
|
+
try {
|
|
187
|
+
content = readFileSync(path.join(gitRoot, rel), "utf-8");
|
|
188
|
+
} catch {}
|
|
189
|
+
parts.push(`${rel}\0${content.replaceAll("\r\n", "\n")}\0`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return parts.length > 0 ? sha256(parts.join("")) : null;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { collectInRepoSiblings, computeSiblingsDigest, readWorkspaceGlobs };
|
|
196
|
+
|
|
197
|
+
//# sourceMappingURL=workspace-siblings.js.map
|
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
* - Prints step transition headers
|
|
8
8
|
* - Tracks elapsed time with in-place status updates
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* The text a stored log line carries.
|
|
12
|
+
*
|
|
13
|
+
* The orchestrator stores every step log line as a JSON envelope —
|
|
14
|
+
* `{"ts":…,"level":"stdout","msg":"…","meta":{}}` — and the Platform relay
|
|
15
|
+
* returns those envelopes verbatim, so a run's log stream is the envelope
|
|
16
|
+
* stream. The dashboard unwraps `msg` before rendering; the terminal must too,
|
|
17
|
+
* or the developer watching `kici run remote` reads raw JSON. A line that is
|
|
18
|
+
* not an envelope (an orchestrator phase marker, a plain line from an older
|
|
19
|
+
* store) passes through unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export declare function unwrapStoredLogLine(line: string): string;
|
|
10
22
|
export declare class StreamingFormatter {
|
|
11
23
|
/** Color assignment per job name. */
|
|
12
24
|
private readonly jobColors;
|
|
@@ -19,6 +19,25 @@ const COLOR_PALETTE = [
|
|
|
19
19
|
pc.magenta,
|
|
20
20
|
pc.cyan
|
|
21
21
|
];
|
|
22
|
+
/**
|
|
23
|
+
* The text a stored log line carries.
|
|
24
|
+
*
|
|
25
|
+
* The orchestrator stores every step log line as a JSON envelope —
|
|
26
|
+
* `{"ts":…,"level":"stdout","msg":"…","meta":{}}` — and the Platform relay
|
|
27
|
+
* returns those envelopes verbatim, so a run's log stream is the envelope
|
|
28
|
+
* stream. The dashboard unwraps `msg` before rendering; the terminal must too,
|
|
29
|
+
* or the developer watching `kici run remote` reads raw JSON. A line that is
|
|
30
|
+
* not an envelope (an orchestrator phase marker, a plain line from an older
|
|
31
|
+
* store) passes through unchanged.
|
|
32
|
+
*/
|
|
33
|
+
function unwrapStoredLogLine(line) {
|
|
34
|
+
if (!line.startsWith("{")) return line;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(line);
|
|
37
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.msg === "string") return parsed.msg;
|
|
38
|
+
} catch {}
|
|
39
|
+
return line;
|
|
40
|
+
}
|
|
22
41
|
var StreamingFormatter = class {
|
|
23
42
|
/** Color assignment per job name. */
|
|
24
43
|
jobColors = /* @__PURE__ */ new Map();
|
|
@@ -119,6 +138,6 @@ var StreamingFormatter = class {
|
|
|
119
138
|
}
|
|
120
139
|
};
|
|
121
140
|
//#endregion
|
|
122
|
-
export { StreamingFormatter };
|
|
141
|
+
export { StreamingFormatter, unwrapStoredLogLine };
|
|
123
142
|
|
|
124
143
|
//# sourceMappingURL=streaming.js.map
|
|
@@ -106,6 +106,8 @@ export interface PlatformRunStatusResponse {
|
|
|
106
106
|
status: string;
|
|
107
107
|
exitCode?: number | null;
|
|
108
108
|
errorMessage?: string | null;
|
|
109
|
+
/** Absent or null from an orchestrator that does not report it. */
|
|
110
|
+
durationMs?: number | null;
|
|
109
111
|
}>;
|
|
110
112
|
done: boolean;
|
|
111
113
|
}
|