@kontextmind/kxm 0.7.128 → 0.7.130
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +20 -0
- package/docs/architecture/inventory.md +1 -1
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/migration.md +12 -8
- package/docs/contributing/development.md +3 -1
- package/docs/operations/backup-and-restore.md +3 -3
- package/docs/operations/upgrade.md +2 -2
- package/docs/reference/cli-reference.md +11 -6
- package/docs/reference/config-reference.md +5 -2
- package/package.json +8 -8
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +2284 -222
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +461 -106
- package/plugins/kxm/dist/runtime.js +470 -114
- package/plugins/kxm/dist/server.js +17 -14
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/lanes.ts +109 -4
- package/plugins/kxm/src/cli/project.ts +2 -2
- package/plugins/kxm/src/database.ts +25 -19
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-service.ts +1 -0
- package/plugins/kxm/src/runtime-store.ts +390 -77
- package/plugins/kxm/src/runtime-supervisor.ts +87 -3
- package/scripts/run-bounded.mjs +53 -0
|
@@ -18414,9 +18414,10 @@ function legacyConfigFilesAt(root) {
|
|
|
18414
18414
|
}
|
|
18415
18415
|
|
|
18416
18416
|
// plugins/kxm/src/runtime-store.ts
|
|
18417
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
18417
18418
|
import { createHash as createHash7, randomUUID } from "node:crypto";
|
|
18418
|
-
import { existsSync as existsSync6, lstatSync as lstatSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18419
|
-
import { dirname as dirname5, resolve as resolve6 } from "node:path";
|
|
18419
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync3, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18420
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
18420
18421
|
|
|
18421
18422
|
// plugins/kxm/src/sqlite.ts
|
|
18422
18423
|
import { existsSync as existsSync4 } from "node:fs";
|
|
@@ -18934,22 +18935,25 @@ function ensureWalJournalMode(database, file, description, timeoutMs = 5e3) {
|
|
|
18934
18935
|
Atomics.wait(sleeper, 0, 0, 10);
|
|
18935
18936
|
}
|
|
18936
18937
|
}
|
|
18938
|
+
function assertDatabaseFile(file, description) {
|
|
18939
|
+
checkedParent(file, description);
|
|
18940
|
+
const stat = lstatSync2(file, { throwIfNoEntry: false });
|
|
18941
|
+
if (stat) {
|
|
18942
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
18943
|
+
throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
|
|
18944
|
+
}
|
|
18945
|
+
}
|
|
18946
|
+
for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
|
|
18947
|
+
const info = lstatSync2(sidecar, { throwIfNoEntry: false });
|
|
18948
|
+
if (info?.isSymbolicLink()) {
|
|
18949
|
+
throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
|
|
18950
|
+
}
|
|
18951
|
+
}
|
|
18952
|
+
}
|
|
18937
18953
|
function openDatabase(file, description, spec) {
|
|
18938
18954
|
const isMemory = file === ":memory:";
|
|
18939
18955
|
if (!isMemory) {
|
|
18940
|
-
|
|
18941
|
-
const stat = lstatSync2(file, { throwIfNoEntry: false });
|
|
18942
|
-
if (stat) {
|
|
18943
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
18944
|
-
throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
|
|
18945
|
-
}
|
|
18946
|
-
}
|
|
18947
|
-
for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
|
|
18948
|
-
const info = lstatSync2(sidecar, { throwIfNoEntry: false });
|
|
18949
|
-
if (info?.isSymbolicLink()) {
|
|
18950
|
-
throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
|
|
18951
|
-
}
|
|
18952
|
-
}
|
|
18956
|
+
assertDatabaseFile(file, description);
|
|
18953
18957
|
}
|
|
18954
18958
|
const database = new DatabaseSync(file);
|
|
18955
18959
|
let transaction = false;
|
|
@@ -18978,7 +18982,7 @@ function openDatabase(file, description, spec) {
|
|
|
18978
18982
|
throw databaseError(
|
|
18979
18983
|
"runtime_schema_outdated",
|
|
18980
18984
|
file,
|
|
18981
|
-
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for
|
|
18985
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for event stores). The Runtime registry copy from schema 1 to 2 is the one exception. \`kxm init\` is project-only and rebuilds no database. Upgrading any other old state in place is deliberately unsupported`
|
|
18982
18986
|
);
|
|
18983
18987
|
}
|
|
18984
18988
|
if (spec.tables) {
|
|
@@ -19216,7 +19220,7 @@ function restoreDatabaseFile(backupPath, targetPath, storeId, expectedSchemaVers
|
|
|
19216
19220
|
}
|
|
19217
19221
|
var KXM_BACKUP_CEILINGS = {
|
|
19218
19222
|
"hub-store": 5,
|
|
19219
|
-
registry:
|
|
19223
|
+
registry: 2,
|
|
19220
19224
|
"binding-store": 1,
|
|
19221
19225
|
events: 7
|
|
19222
19226
|
};
|
|
@@ -19582,10 +19586,10 @@ function projectRuntimeOwnsRun(projectRoot, runId, env) {
|
|
|
19582
19586
|
database.close();
|
|
19583
19587
|
}
|
|
19584
19588
|
}
|
|
19585
|
-
var KXM_REGISTRY_SCHEMA_VERSION =
|
|
19589
|
+
var KXM_REGISTRY_SCHEMA_VERSION = 2;
|
|
19586
19590
|
var REGISTRY_TABLES = {
|
|
19587
19591
|
supervisor: ["singleton_id", "runtime_id", "pid", "port", "token_hash", "started_at", "heartbeat_at", "state"],
|
|
19588
|
-
projects: ["project_id", "project_root", "project_key", "home_runtime_id", "config_revision", "registered_at"]
|
|
19592
|
+
projects: ["project_id", "project_root", "project_key", "home_runtime_id", "config_revision", "registered_at", "lane_of"]
|
|
19589
19593
|
};
|
|
19590
19594
|
var REGISTRY_SCHEMA = `
|
|
19591
19595
|
CREATE TABLE supervisor (
|
|
@@ -19599,14 +19603,117 @@ CREATE TABLE supervisor (
|
|
|
19599
19603
|
state TEXT NOT NULL
|
|
19600
19604
|
) STRICT;
|
|
19601
19605
|
CREATE TABLE projects (
|
|
19602
|
-
|
|
19606
|
+
project_key TEXT PRIMARY KEY,
|
|
19607
|
+
project_id TEXT NOT NULL,
|
|
19603
19608
|
project_root TEXT NOT NULL,
|
|
19604
|
-
project_key TEXT NOT NULL UNIQUE,
|
|
19605
19609
|
home_runtime_id TEXT NOT NULL,
|
|
19606
19610
|
config_revision TEXT,
|
|
19607
|
-
registered_at TEXT NOT NULL
|
|
19611
|
+
registered_at TEXT NOT NULL,
|
|
19612
|
+
lane_of TEXT
|
|
19608
19613
|
) STRICT;
|
|
19614
|
+
CREATE INDEX projects_by_project_id ON projects (project_id);
|
|
19609
19615
|
`;
|
|
19616
|
+
var PROJECT_COLUMNS = "project_id, project_root, project_key, home_runtime_id, config_revision, registered_at, lane_of";
|
|
19617
|
+
function registrationFromRow(row) {
|
|
19618
|
+
return {
|
|
19619
|
+
projectId: row.project_id,
|
|
19620
|
+
projectRoot: row.project_root,
|
|
19621
|
+
projectKey: row.project_key,
|
|
19622
|
+
homeRuntimeId: row.home_runtime_id,
|
|
19623
|
+
...row.config_revision !== null ? { configRevision: row.config_revision } : {},
|
|
19624
|
+
...row.lane_of !== null ? { laneOf: row.lane_of } : {},
|
|
19625
|
+
registeredAt: row.registered_at
|
|
19626
|
+
};
|
|
19627
|
+
}
|
|
19628
|
+
function controlRootDirectoryExists(projectRoot) {
|
|
19629
|
+
const stat = statSync(projectRoot, { throwIfNoEntry: false });
|
|
19630
|
+
return Boolean(stat?.isDirectory());
|
|
19631
|
+
}
|
|
19632
|
+
function gitEnv() {
|
|
19633
|
+
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
|
|
19634
|
+
}
|
|
19635
|
+
function gitCommonDirectory(projectRoot) {
|
|
19636
|
+
const result = spawnSync3("git", ["-C", projectRoot, "rev-parse", "--git-common-dir"], {
|
|
19637
|
+
encoding: "utf8",
|
|
19638
|
+
env: gitEnv(),
|
|
19639
|
+
timeout: 3e4,
|
|
19640
|
+
windowsHide: true
|
|
19641
|
+
});
|
|
19642
|
+
if (result.status !== 0) return void 0;
|
|
19643
|
+
const text = (result.stdout ?? "").trim();
|
|
19644
|
+
if (!text) return void 0;
|
|
19645
|
+
const absolute = isAbsolute4(text) ? text : resolve6(projectRoot, text);
|
|
19646
|
+
let canonical;
|
|
19647
|
+
try {
|
|
19648
|
+
canonical = realpathSync3.native(absolute);
|
|
19649
|
+
} catch {
|
|
19650
|
+
canonical = resolve6(absolute);
|
|
19651
|
+
}
|
|
19652
|
+
return process.platform === "win32" ? canonical.toLocaleLowerCase("en-US") : canonical;
|
|
19653
|
+
}
|
|
19654
|
+
function isPrimaryWorktree(projectRoot, commonDir) {
|
|
19655
|
+
if (commonDir === void 0) return false;
|
|
19656
|
+
const dotGit = resolve6(projectRoot, ".git");
|
|
19657
|
+
let canonical;
|
|
19658
|
+
try {
|
|
19659
|
+
canonical = realpathSync3.native(dotGit);
|
|
19660
|
+
} catch {
|
|
19661
|
+
canonical = resolve6(dotGit);
|
|
19662
|
+
}
|
|
19663
|
+
if (process.platform === "win32") canonical = canonical.toLocaleLowerCase("en-US");
|
|
19664
|
+
return canonical === commonDir;
|
|
19665
|
+
}
|
|
19666
|
+
function migrateRegistryLanes(file) {
|
|
19667
|
+
const stat = lstatSync3(file, { throwIfNoEntry: false });
|
|
19668
|
+
if (!stat) return;
|
|
19669
|
+
assertDatabaseFile(file, "runtime registry");
|
|
19670
|
+
const database = new DatabaseSync(file);
|
|
19671
|
+
let transaction = false;
|
|
19672
|
+
try {
|
|
19673
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
19674
|
+
const current = database.prepare("PRAGMA user_version").get();
|
|
19675
|
+
if ((current?.user_version ?? 0) !== 1) return;
|
|
19676
|
+
database.exec("BEGIN IMMEDIATE");
|
|
19677
|
+
transaction = true;
|
|
19678
|
+
const locked = database.prepare("PRAGMA user_version").get();
|
|
19679
|
+
if ((locked?.user_version ?? 0) !== 1) {
|
|
19680
|
+
database.exec("ROLLBACK");
|
|
19681
|
+
transaction = false;
|
|
19682
|
+
return;
|
|
19683
|
+
}
|
|
19684
|
+
if (!tableColumns(database, "projects").includes("lane_of")) {
|
|
19685
|
+
database.exec(`
|
|
19686
|
+
CREATE TABLE projects_v2 (
|
|
19687
|
+
project_key TEXT PRIMARY KEY,
|
|
19688
|
+
project_id TEXT NOT NULL,
|
|
19689
|
+
project_root TEXT NOT NULL,
|
|
19690
|
+
home_runtime_id TEXT NOT NULL,
|
|
19691
|
+
config_revision TEXT,
|
|
19692
|
+
registered_at TEXT NOT NULL,
|
|
19693
|
+
lane_of TEXT
|
|
19694
|
+
) STRICT;
|
|
19695
|
+
INSERT INTO projects_v2 (project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, lane_of)
|
|
19696
|
+
SELECT project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, NULL FROM projects;
|
|
19697
|
+
DROP TABLE projects;
|
|
19698
|
+
ALTER TABLE projects_v2 RENAME TO projects;
|
|
19699
|
+
CREATE INDEX projects_by_project_id ON projects (project_id);
|
|
19700
|
+
`);
|
|
19701
|
+
}
|
|
19702
|
+
database.exec("PRAGMA user_version = 2");
|
|
19703
|
+
database.exec("COMMIT");
|
|
19704
|
+
transaction = false;
|
|
19705
|
+
} catch (error) {
|
|
19706
|
+
if (transaction) {
|
|
19707
|
+
try {
|
|
19708
|
+
database.exec("ROLLBACK");
|
|
19709
|
+
} catch {
|
|
19710
|
+
}
|
|
19711
|
+
}
|
|
19712
|
+
throw error;
|
|
19713
|
+
} finally {
|
|
19714
|
+
database.close();
|
|
19715
|
+
}
|
|
19716
|
+
}
|
|
19610
19717
|
function readKxmSupervisorRecord(database) {
|
|
19611
19718
|
const row = database.prepare("SELECT runtime_id, pid, port, token_hash, started_at, heartbeat_at, state FROM supervisor WHERE singleton_id = 1").get();
|
|
19612
19719
|
return row ? {
|
|
@@ -19624,6 +19731,7 @@ var KxmRuntimeRegistry = class {
|
|
|
19624
19731
|
database;
|
|
19625
19732
|
constructor(path) {
|
|
19626
19733
|
this.path = resolve6(path);
|
|
19734
|
+
migrateRegistryLanes(this.path);
|
|
19627
19735
|
this.database = openDatabase(this.path, "runtime registry", {
|
|
19628
19736
|
schema: REGISTRY_SCHEMA,
|
|
19629
19737
|
version: KXM_REGISTRY_SCHEMA_VERSION,
|
|
@@ -19702,57 +19810,188 @@ var KxmRuntimeRegistry = class {
|
|
|
19702
19810
|
supervisor() {
|
|
19703
19811
|
return this.readSupervisorRow();
|
|
19704
19812
|
}
|
|
19705
|
-
/**
|
|
19706
|
-
|
|
19707
|
-
*
|
|
19708
|
-
* syncing and presence keeps beating. */
|
|
19813
|
+
/** All projects registered to this Runtime, including lane roots, for restart
|
|
19814
|
+
* recovery: the supervisor reopens their contexts so pending outbox rows
|
|
19815
|
+
* resume syncing and presence keeps beating. */
|
|
19709
19816
|
projectsForRuntime(homeRuntimeId) {
|
|
19710
19817
|
const rows = this.database.prepare(
|
|
19711
19818
|
"SELECT project_root, project_id FROM projects WHERE home_runtime_id = ? ORDER BY registered_at"
|
|
19712
19819
|
).all(homeRuntimeId);
|
|
19713
19820
|
return rows.map((row) => ({ projectRoot: row.project_root, projectId: row.project_id }));
|
|
19714
19821
|
}
|
|
19822
|
+
/**
|
|
19823
|
+
* Register or revalidate a control root. The home runtime of a live row is
|
|
19824
|
+
* immutable. A second root whose git common directory matches a live row
|
|
19825
|
+
* for the same project is a lane, unless the registering root is the primary
|
|
19826
|
+
* worktree and the live home row is not: then the primary becomes the home
|
|
19827
|
+
* row (same home runtime id) and every other live row of that repository
|
|
19828
|
+
* becomes its lane. A primary never becomes a lane. A root whose directory
|
|
19829
|
+
* is gone is replaced. A live root that is a different repository is
|
|
19830
|
+
* `project_home_conflict`.
|
|
19831
|
+
*/
|
|
19715
19832
|
registerProject(registration) {
|
|
19716
19833
|
const projectRoot = resolve6(registration.projectRoot);
|
|
19717
19834
|
const projectKey = projectRuntimeKey(projectRoot);
|
|
19835
|
+
const candidateCommon = gitCommonDirectory(projectRoot);
|
|
19836
|
+
const commonByRoot = /* @__PURE__ */ new Map();
|
|
19837
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
19838
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
19839
|
+
let rootsToMeasure;
|
|
19840
|
+
try {
|
|
19841
|
+
const byKey = this.projectRow("SELECT " + PROJECT_COLUMNS + " FROM projects WHERE project_key = ?", projectKey);
|
|
19842
|
+
if (byKey) {
|
|
19843
|
+
const problems = [];
|
|
19844
|
+
if (byKey.project_id !== registration.projectId) problems.push(`control root is already bound to a different project id ${byKey.project_id}`);
|
|
19845
|
+
if (byKey.home_runtime_id !== registration.homeRuntimeId) problems.push(`project ${registration.projectId} home runtime is immutable and cannot be rebound`);
|
|
19846
|
+
if (problems.length > 0) {
|
|
19847
|
+
this.database.exec("ROLLBACK");
|
|
19848
|
+
throw runtimeError("project_home_conflict", ".kxm/project.yaml", problems.join("; "));
|
|
19849
|
+
}
|
|
19850
|
+
const result = registrationFromRow(byKey);
|
|
19851
|
+
this.database.exec("COMMIT");
|
|
19852
|
+
return result;
|
|
19853
|
+
}
|
|
19854
|
+
const rows = this.database.prepare(
|
|
19855
|
+
`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_id = ? ORDER BY registered_at`
|
|
19856
|
+
).all(registration.projectId);
|
|
19857
|
+
const live = rows.filter((row) => controlRootDirectoryExists(row.project_root));
|
|
19858
|
+
const dead = rows.filter((row) => !controlRootDirectoryExists(row.project_root));
|
|
19859
|
+
const unknown = live.filter((row) => !commonByRoot.has(row.project_root));
|
|
19860
|
+
if (unknown.length > 0) {
|
|
19861
|
+
rootsToMeasure = unknown.map((row) => row.project_root);
|
|
19862
|
+
this.database.exec("ROLLBACK");
|
|
19863
|
+
} else if (live.length > 0) {
|
|
19864
|
+
const anchor = live.find((row) => {
|
|
19865
|
+
const dir = commonByRoot.get(row.project_root);
|
|
19866
|
+
return candidateCommon !== void 0 && dir === candidateCommon;
|
|
19867
|
+
});
|
|
19868
|
+
if (!anchor) {
|
|
19869
|
+
this.database.exec("ROLLBACK");
|
|
19870
|
+
throw runtimeError(
|
|
19871
|
+
"project_home_conflict",
|
|
19872
|
+
".kxm/project.yaml",
|
|
19873
|
+
`project ${registration.projectId} is already bound to a different control root`
|
|
19874
|
+
);
|
|
19875
|
+
}
|
|
19876
|
+
if (live.some((row) => row.home_runtime_id !== registration.homeRuntimeId)) {
|
|
19877
|
+
this.database.exec("ROLLBACK");
|
|
19878
|
+
throw runtimeError(
|
|
19879
|
+
"project_home_conflict",
|
|
19880
|
+
".kxm/project.yaml",
|
|
19881
|
+
`project ${registration.projectId} home runtime is immutable and cannot be rebound`
|
|
19882
|
+
);
|
|
19883
|
+
}
|
|
19884
|
+
const sameRepo = live.filter((row) => commonByRoot.get(row.project_root) === candidateCommon);
|
|
19885
|
+
const home = sameRepo.find((row) => row.lane_of === null);
|
|
19886
|
+
const registeringIsPrimary = isPrimaryWorktree(projectRoot, candidateCommon);
|
|
19887
|
+
const homeIsPrimary = home !== void 0 && isPrimaryWorktree(home.project_root, commonByRoot.get(home.project_root));
|
|
19888
|
+
if (registeringIsPrimary && !homeIsPrimary) {
|
|
19889
|
+
this.insertHome(registration, projectRoot, projectKey);
|
|
19890
|
+
for (const row of sameRepo) {
|
|
19891
|
+
this.database.prepare("UPDATE projects SET lane_of = ? WHERE project_key = ?").run(projectKey, row.project_key);
|
|
19892
|
+
}
|
|
19893
|
+
this.database.exec("COMMIT");
|
|
19894
|
+
registration.logger?.({
|
|
19895
|
+
event: "project_home_promoted",
|
|
19896
|
+
projectId: registration.projectId,
|
|
19897
|
+
projectRoot,
|
|
19898
|
+
homeRuntimeId: registration.homeRuntimeId,
|
|
19899
|
+
previousHomeRoot: home?.project_root ?? null,
|
|
19900
|
+
previousHomeKey: home?.project_key ?? null
|
|
19901
|
+
});
|
|
19902
|
+
return this.insertedRegistration(registration, projectRoot, projectKey);
|
|
19903
|
+
}
|
|
19904
|
+
if (registeringIsPrimary) {
|
|
19905
|
+
this.database.exec("ROLLBACK");
|
|
19906
|
+
throw runtimeError(
|
|
19907
|
+
"project_home_conflict",
|
|
19908
|
+
".kxm/project.yaml",
|
|
19909
|
+
`project ${registration.projectId} primary checkout is already the home row`
|
|
19910
|
+
);
|
|
19911
|
+
}
|
|
19912
|
+
const laneOf = anchor.lane_of ?? anchor.project_key;
|
|
19913
|
+
this.database.prepare(`
|
|
19914
|
+
INSERT INTO projects (project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, lane_of)
|
|
19915
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
19916
|
+
`).run(projectKey, registration.projectId, projectRoot, registration.homeRuntimeId, registration.configRevision ?? null, registration.now, laneOf);
|
|
19917
|
+
const result = this.insertedRegistration(registration, projectRoot, projectKey, laneOf);
|
|
19918
|
+
this.database.exec("COMMIT");
|
|
19919
|
+
return result;
|
|
19920
|
+
} else if (dead.length > 0) {
|
|
19921
|
+
const replaced = dead[0];
|
|
19922
|
+
if (dead.some((row) => row.home_runtime_id !== registration.homeRuntimeId)) {
|
|
19923
|
+
this.database.exec("ROLLBACK");
|
|
19924
|
+
throw runtimeError(
|
|
19925
|
+
"project_home_conflict",
|
|
19926
|
+
".kxm/project.yaml",
|
|
19927
|
+
`project ${registration.projectId} home runtime is immutable and cannot be rebound`
|
|
19928
|
+
);
|
|
19929
|
+
}
|
|
19930
|
+
this.database.prepare("DELETE FROM projects WHERE project_id = ?").run(registration.projectId);
|
|
19931
|
+
this.insertHome(registration, projectRoot, projectKey);
|
|
19932
|
+
this.database.exec("COMMIT");
|
|
19933
|
+
registration.logger?.({
|
|
19934
|
+
event: "project_registration_replaced",
|
|
19935
|
+
projectId: registration.projectId,
|
|
19936
|
+
projectRoot,
|
|
19937
|
+
replacedRoot: replaced.project_root,
|
|
19938
|
+
replacedCount: dead.length,
|
|
19939
|
+
replacedHomeRuntimeId: replaced.home_runtime_id,
|
|
19940
|
+
replacedProjectKey: replaced.project_key
|
|
19941
|
+
});
|
|
19942
|
+
return this.insertedRegistration(registration, projectRoot, projectKey);
|
|
19943
|
+
} else {
|
|
19944
|
+
this.insertHome(registration, projectRoot, projectKey);
|
|
19945
|
+
this.database.exec("COMMIT");
|
|
19946
|
+
return this.insertedRegistration(registration, projectRoot, projectKey);
|
|
19947
|
+
}
|
|
19948
|
+
} catch (error) {
|
|
19949
|
+
try {
|
|
19950
|
+
this.database.exec("ROLLBACK");
|
|
19951
|
+
} catch {
|
|
19952
|
+
}
|
|
19953
|
+
throw error;
|
|
19954
|
+
}
|
|
19955
|
+
for (const root of rootsToMeasure ?? []) {
|
|
19956
|
+
commonByRoot.set(root, gitCommonDirectory(root));
|
|
19957
|
+
}
|
|
19958
|
+
}
|
|
19959
|
+
throw runtimeError(
|
|
19960
|
+
"runtime_registry_busy",
|
|
19961
|
+
projectRoot,
|
|
19962
|
+
`project ${registration.projectId} registration could not be rechecked`
|
|
19963
|
+
);
|
|
19964
|
+
}
|
|
19965
|
+
/** Remove one control root. Returns false when that root was not registered. */
|
|
19966
|
+
unregisterProject(projectRoot) {
|
|
19718
19967
|
this.database.exec("BEGIN IMMEDIATE");
|
|
19719
19968
|
try {
|
|
19720
|
-
const
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
if (existing.home_runtime_id !== registration.homeRuntimeId) problems.push(`project ${registration.projectId} home runtime is immutable and cannot be rebound`);
|
|
19728
|
-
if (problems.length > 0) {
|
|
19729
|
-
this.database.exec("ROLLBACK");
|
|
19730
|
-
throw runtimeError("project_home_conflict", ".kxm/project.yaml", problems.join("; "));
|
|
19731
|
-
}
|
|
19732
|
-
const result = {
|
|
19733
|
-
projectId: existing.project_id,
|
|
19734
|
-
projectRoot: existing.project_root,
|
|
19735
|
-
projectKey: existing.project_key,
|
|
19736
|
-
homeRuntimeId: existing.home_runtime_id,
|
|
19737
|
-
...existing.config_revision !== null ? { configRevision: existing.config_revision } : {},
|
|
19738
|
-
registeredAt: existing.registered_at
|
|
19739
|
-
};
|
|
19740
|
-
this.database.exec("COMMIT");
|
|
19741
|
-
return result;
|
|
19969
|
+
const removed = this.deleteRegisteredRoot(projectRoot);
|
|
19970
|
+
this.database.exec("COMMIT");
|
|
19971
|
+
return removed;
|
|
19972
|
+
} catch (error) {
|
|
19973
|
+
try {
|
|
19974
|
+
this.database.exec("ROLLBACK");
|
|
19975
|
+
} catch {
|
|
19742
19976
|
}
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
|
|
19977
|
+
throw error;
|
|
19978
|
+
}
|
|
19979
|
+
}
|
|
19980
|
+
/**
|
|
19981
|
+
* Delete the row only when `supervisorIsLive` is false for the supervisor
|
|
19982
|
+
* row read inside this write transaction. A live supervisor keeps the row;
|
|
19983
|
+
* the caller uses the supervisor route instead of editing the file.
|
|
19984
|
+
*/
|
|
19985
|
+
unregisterProjectIfIdle(projectRoot, supervisorIsLive) {
|
|
19986
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
19987
|
+
try {
|
|
19988
|
+
if (supervisorIsLive(this.readSupervisorRow())) {
|
|
19989
|
+
this.database.exec("ROLLBACK");
|
|
19990
|
+
return "supervisor_live";
|
|
19991
|
+
}
|
|
19992
|
+
const removed = this.deleteRegisteredRoot(projectRoot);
|
|
19747
19993
|
this.database.exec("COMMIT");
|
|
19748
|
-
return
|
|
19749
|
-
projectId: registration.projectId,
|
|
19750
|
-
projectRoot,
|
|
19751
|
-
projectKey,
|
|
19752
|
-
homeRuntimeId: registration.homeRuntimeId,
|
|
19753
|
-
...registration.configRevision !== void 0 ? { configRevision: registration.configRevision } : {},
|
|
19754
|
-
registeredAt: registration.now
|
|
19755
|
-
};
|
|
19994
|
+
return removed ? "removed" : "absent";
|
|
19756
19995
|
} catch (error) {
|
|
19757
19996
|
try {
|
|
19758
19997
|
this.database.exec("ROLLBACK");
|
|
@@ -19761,28 +20000,65 @@ var KxmRuntimeRegistry = class {
|
|
|
19761
20000
|
throw error;
|
|
19762
20001
|
}
|
|
19763
20002
|
}
|
|
20003
|
+
/** How many other rows share this home root's project. Zero when this root is a lane or missing. */
|
|
20004
|
+
homeLaneCount(projectRoot) {
|
|
20005
|
+
const row = this.projectRow(`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_key = ?`, projectRuntimeKey(resolve6(projectRoot)));
|
|
20006
|
+
if (!row || row.lane_of !== null) return 0;
|
|
20007
|
+
const count = this.database.prepare(
|
|
20008
|
+
"SELECT COUNT(*) AS total FROM projects WHERE project_id = ? AND project_key != ?"
|
|
20009
|
+
).get(row.project_id, row.project_key);
|
|
20010
|
+
return Number(count?.total ?? 0);
|
|
20011
|
+
}
|
|
20012
|
+
/** Caller holds the write transaction. Does not commit. */
|
|
20013
|
+
deleteRegisteredRoot(projectRoot) {
|
|
20014
|
+
const projectKey = projectRuntimeKey(resolve6(projectRoot));
|
|
20015
|
+
const row = this.projectRow(`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_key = ?`, projectKey);
|
|
20016
|
+
if (!row) return false;
|
|
20017
|
+
this.database.prepare("DELETE FROM projects WHERE project_key = ?").run(projectKey);
|
|
20018
|
+
if (row.lane_of === null) {
|
|
20019
|
+
const successor = this.database.prepare(
|
|
20020
|
+
"SELECT project_key FROM projects WHERE project_id = ? ORDER BY registered_at LIMIT 1"
|
|
20021
|
+
).get(row.project_id);
|
|
20022
|
+
if (successor) {
|
|
20023
|
+
this.database.prepare("UPDATE projects SET lane_of = NULL WHERE project_key = ?").run(successor.project_key);
|
|
20024
|
+
this.database.prepare(
|
|
20025
|
+
"UPDATE projects SET lane_of = ? WHERE project_id = ? AND project_key != ? AND (lane_of IS NULL OR lane_of = ?)"
|
|
20026
|
+
).run(successor.project_key, row.project_id, successor.project_key, projectKey);
|
|
20027
|
+
}
|
|
20028
|
+
}
|
|
20029
|
+
return true;
|
|
20030
|
+
}
|
|
20031
|
+
/** The home row for a project id, or the earliest row when every row is a lane. */
|
|
19764
20032
|
project(projectId) {
|
|
19765
|
-
const row = this.
|
|
19766
|
-
|
|
19767
|
-
projectId
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
homeRuntimeId: row.home_runtime_id,
|
|
19771
|
-
...row.config_revision !== null ? { configRevision: row.config_revision } : {},
|
|
19772
|
-
registeredAt: row.registered_at
|
|
19773
|
-
} : void 0;
|
|
20033
|
+
const row = this.projectRow(
|
|
20034
|
+
`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_id = ? ORDER BY CASE WHEN lane_of IS NULL THEN 0 ELSE 1 END, registered_at LIMIT 1`,
|
|
20035
|
+
projectId
|
|
20036
|
+
);
|
|
20037
|
+
return row ? registrationFromRow(row) : void 0;
|
|
19774
20038
|
}
|
|
19775
20039
|
projectByRoot(projectRoot) {
|
|
19776
|
-
const
|
|
19777
|
-
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
|
|
19785
|
-
|
|
20040
|
+
const row = this.projectRow(`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_key = ?`, projectRuntimeKey(projectRoot));
|
|
20041
|
+
return row ? registrationFromRow(row) : void 0;
|
|
20042
|
+
}
|
|
20043
|
+
projectRow(sql, parameter) {
|
|
20044
|
+
return this.database.prepare(sql).get(parameter);
|
|
20045
|
+
}
|
|
20046
|
+
insertHome(registration, projectRoot, projectKey) {
|
|
20047
|
+
this.database.prepare(`
|
|
20048
|
+
INSERT INTO projects (project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, lane_of)
|
|
20049
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)
|
|
20050
|
+
`).run(projectKey, registration.projectId, projectRoot, registration.homeRuntimeId, registration.configRevision ?? null, registration.now);
|
|
20051
|
+
}
|
|
20052
|
+
insertedRegistration(registration, projectRoot, projectKey, laneOf) {
|
|
20053
|
+
return {
|
|
20054
|
+
projectId: registration.projectId,
|
|
20055
|
+
projectRoot,
|
|
20056
|
+
projectKey,
|
|
20057
|
+
homeRuntimeId: registration.homeRuntimeId,
|
|
20058
|
+
...registration.configRevision !== void 0 ? { configRevision: registration.configRevision } : {},
|
|
20059
|
+
...laneOf !== void 0 ? { laneOf } : {},
|
|
20060
|
+
registeredAt: registration.now
|
|
20061
|
+
};
|
|
19786
20062
|
}
|
|
19787
20063
|
};
|
|
19788
20064
|
var KXM_RUN_EVENT_SCHEMA = "kxm.run-event.v1";
|
|
@@ -24228,7 +24504,8 @@ function openKxmRuntimeContext(projectRoot, options) {
|
|
|
24228
24504
|
projectRoot: bundle.projectRoot,
|
|
24229
24505
|
homeRuntimeId: options.homeRuntimeId,
|
|
24230
24506
|
configRevision: bundle.configRevision,
|
|
24231
|
-
now: options.now ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
24507
|
+
now: options.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
24508
|
+
...options.logger ? { logger: options.logger } : {}
|
|
24232
24509
|
});
|
|
24233
24510
|
const eventStore = new KxmRunEventStore(join7(paths.projectsDir, registration.projectKey, "run-events.db"));
|
|
24234
24511
|
registerKxmRuntimeHandle(eventStore.path);
|
|
@@ -24582,7 +24859,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
24582
24859
|
import { createHash as createHash16, createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
24583
24860
|
import { chmodSync as chmodSync2, existsSync as existsSync16, lstatSync as lstatSync6, mkdirSync as mkdirSync9, readFileSync as readFileSync13, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
|
|
24584
24861
|
import { createServer } from "node:http";
|
|
24585
|
-
import { dirname as dirname11, isAbsolute as
|
|
24862
|
+
import { dirname as dirname11, isAbsolute as isAbsolute8, join as join18 } from "node:path";
|
|
24586
24863
|
|
|
24587
24864
|
// plugins/kxm/src/oneshot-evidence.ts
|
|
24588
24865
|
import { createHash as createHash10, randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -24737,7 +25014,7 @@ async function beginOneShotEvidence(root, intent, sensitive) {
|
|
|
24737
25014
|
// plugins/kxm/src/prices.ts
|
|
24738
25015
|
var import_yaml3 = __toESM(require_dist(), 1);
|
|
24739
25016
|
import { createHash as createHash11 } from "node:crypto";
|
|
24740
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
25017
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
24741
25018
|
import { join as join9 } from "node:path";
|
|
24742
25019
|
|
|
24743
25020
|
// plugins/kxm/src/price-calc.ts
|
|
@@ -24890,7 +25167,7 @@ function parsePriceCatalog(text) {
|
|
|
24890
25167
|
return catalog;
|
|
24891
25168
|
}
|
|
24892
25169
|
function loadPriceCatalog(rootOrPath) {
|
|
24893
|
-
const candidatePath = existsSync8(join9(rootOrPath, ".kxm", "prices.yaml")) ? join9(rootOrPath, ".kxm", "prices.yaml") : existsSync8(join9(rootOrPath, "prices.yaml")) ? join9(rootOrPath, "prices.yaml") : existsSync8(rootOrPath) &&
|
|
25170
|
+
const candidatePath = existsSync8(join9(rootOrPath, ".kxm", "prices.yaml")) ? join9(rootOrPath, ".kxm", "prices.yaml") : existsSync8(join9(rootOrPath, "prices.yaml")) ? join9(rootOrPath, "prices.yaml") : existsSync8(rootOrPath) && statSync2(rootOrPath).isFile() ? rootOrPath : void 0;
|
|
24894
25171
|
if (!candidatePath || !existsSync8(candidatePath)) {
|
|
24895
25172
|
return void 0;
|
|
24896
25173
|
}
|
|
@@ -24955,9 +25232,9 @@ function isRouteAdmitted(root, modelId) {
|
|
|
24955
25232
|
}
|
|
24956
25233
|
|
|
24957
25234
|
// plugins/kxm/src/worktree-witness.ts
|
|
24958
|
-
import { spawnSync as
|
|
25235
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
24959
25236
|
function gitText(cwd, args) {
|
|
24960
|
-
const result =
|
|
25237
|
+
const result = spawnSync4("git", ["-C", cwd, ...args], {
|
|
24961
25238
|
encoding: "utf8",
|
|
24962
25239
|
windowsHide: true,
|
|
24963
25240
|
timeout: 15e3
|
|
@@ -25270,7 +25547,7 @@ function pruneContextPacket(packet, tokenBudget) {
|
|
|
25270
25547
|
}
|
|
25271
25548
|
|
|
25272
25549
|
// plugins/kxm/src/dispatch-context.ts
|
|
25273
|
-
import { spawnSync as
|
|
25550
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
25274
25551
|
import { readdirSync as readdirSync8 } from "node:fs";
|
|
25275
25552
|
import { extname as extname3, join as join13 } from "node:path";
|
|
25276
25553
|
|
|
@@ -26033,7 +26310,7 @@ function loadAuthoredMemory(repoRoot) {
|
|
|
26033
26310
|
// plugins/kxm/src/skills.ts
|
|
26034
26311
|
var import_yaml8 = __toESM(require_dist(), 1);
|
|
26035
26312
|
import { createHash as createHash12 } from "node:crypto";
|
|
26036
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, readFileSync as readFileSync9, renameSync, rmSync, statSync as
|
|
26313
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, readFileSync as readFileSync9, renameSync, rmSync, statSync as statSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
26037
26314
|
import { dirname as dirname7, join as join12 } from "node:path";
|
|
26038
26315
|
var SKILL_CANDIDATE_SCHEMA = "kxm.skill-candidate.v1";
|
|
26039
26316
|
var SKILL_EVALUATION_SCHEMA = "kxm.skill-evaluation.v1";
|
|
@@ -26413,7 +26690,7 @@ function boundedList(value, field) {
|
|
|
26413
26690
|
return [...new Set(refs)];
|
|
26414
26691
|
}
|
|
26415
26692
|
function readdirSorted(dir) {
|
|
26416
|
-
return readdirSync7(dir).filter((entry) =>
|
|
26693
|
+
return readdirSync7(dir).filter((entry) => statSync3(join12(dir, entry)).isDirectory()).sort();
|
|
26417
26694
|
}
|
|
26418
26695
|
|
|
26419
26696
|
// plugins/kxm/src/dispatch-context.ts
|
|
@@ -26474,7 +26751,7 @@ function gitEnvironment2() {
|
|
|
26474
26751
|
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
|
|
26475
26752
|
}
|
|
26476
26753
|
function committedGateGap(projectRoot) {
|
|
26477
|
-
const result =
|
|
26754
|
+
const result = spawnSync5("git", [
|
|
26478
26755
|
"--no-optional-locks",
|
|
26479
26756
|
"-C",
|
|
26480
26757
|
projectRoot,
|
|
@@ -26616,11 +26893,11 @@ function assembleDispatchContext(sources, input) {
|
|
|
26616
26893
|
import { join as join14 } from "node:path";
|
|
26617
26894
|
|
|
26618
26895
|
// plugins/kxm/src/artifacts-exist.ts
|
|
26619
|
-
import { lstatSync as lstatSync5, realpathSync as
|
|
26620
|
-
import { isAbsolute as
|
|
26896
|
+
import { lstatSync as lstatSync5, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
|
|
26897
|
+
import { isAbsolute as isAbsolute5, relative as relative4, resolve as resolve8 } from "node:path";
|
|
26621
26898
|
function staysUnder(root, candidate) {
|
|
26622
26899
|
const child = relative4(root, candidate);
|
|
26623
|
-
return child === "" || !
|
|
26900
|
+
return child === "" || !isAbsolute5(child) && child !== ".." && !child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
|
|
26624
26901
|
}
|
|
26625
26902
|
function verifyArtifactExists(rootInput, pathInput) {
|
|
26626
26903
|
const lexicalRoot = resolve8(rootInput);
|
|
@@ -26631,12 +26908,12 @@ function verifyArtifactExists(rootInput, pathInput) {
|
|
|
26631
26908
|
let realRoot;
|
|
26632
26909
|
let realPath;
|
|
26633
26910
|
try {
|
|
26634
|
-
realRoot =
|
|
26911
|
+
realRoot = realpathSync4(lexicalRoot);
|
|
26635
26912
|
} catch {
|
|
26636
26913
|
return { ok: false, error: "artifact_unreadable", path: lexicalPath };
|
|
26637
26914
|
}
|
|
26638
26915
|
try {
|
|
26639
|
-
realPath =
|
|
26916
|
+
realPath = realpathSync4(lexicalPath);
|
|
26640
26917
|
} catch {
|
|
26641
26918
|
return { ok: false, error: "artifact_missing", path: lexicalPath };
|
|
26642
26919
|
}
|
|
@@ -26648,7 +26925,7 @@ function verifyArtifactExists(rootInput, pathInput) {
|
|
|
26648
26925
|
if (link.isSymbolicLink()) {
|
|
26649
26926
|
return { ok: false, error: "artifact_outside_workspace_assets", path: lexicalPath };
|
|
26650
26927
|
}
|
|
26651
|
-
const artifact =
|
|
26928
|
+
const artifact = statSync4(realPath);
|
|
26652
26929
|
if (!artifact.isFile()) return { ok: false, error: "artifact_not_file", path: lexicalPath };
|
|
26653
26930
|
if (artifact.size < 1) return { ok: false, error: "artifact_empty", path: lexicalPath };
|
|
26654
26931
|
return { ok: true, path: realPath, bytes: artifact.size };
|
|
@@ -30789,7 +31066,7 @@ var RuntimeHubClient = class {
|
|
|
30789
31066
|
// plugins/kxm/src/hub-binding.ts
|
|
30790
31067
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30791
31068
|
import { homedir as homedir2 } from "node:os";
|
|
30792
|
-
import { dirname as dirname8, isAbsolute as
|
|
31069
|
+
import { dirname as dirname8, isAbsolute as isAbsolute6, join as join16, resolve as resolve9 } from "node:path";
|
|
30793
31070
|
var HUB_BINDING_SCHEMA = "kxm.hub-binding.v1";
|
|
30794
31071
|
var HubBindingError = class extends Error {
|
|
30795
31072
|
constructor(message) {
|
|
@@ -30800,17 +31077,17 @@ var HubBindingError = class extends Error {
|
|
|
30800
31077
|
function resolveUserStateRoot(env) {
|
|
30801
31078
|
const explicit = env.KXM_STATE_HOME?.trim();
|
|
30802
31079
|
if (explicit) {
|
|
30803
|
-
if (!
|
|
31080
|
+
if (!isAbsolute6(explicit)) throw new HubBindingError("local_state_root_not_absolute");
|
|
30804
31081
|
return resolve9(explicit);
|
|
30805
31082
|
}
|
|
30806
31083
|
if (process.platform === "win32") {
|
|
30807
31084
|
const localAppData = env.LOCALAPPDATA?.trim();
|
|
30808
|
-
const base2 = localAppData &&
|
|
31085
|
+
const base2 = localAppData && isAbsolute6(localAppData) ? localAppData : join16(homedir2(), "AppData", "Local");
|
|
30809
31086
|
return resolve9(base2, "KXM");
|
|
30810
31087
|
}
|
|
30811
31088
|
if (process.platform === "darwin") return resolve9(homedir2(), "Library", "Application Support", "KXM");
|
|
30812
31089
|
const xdgState = env.XDG_STATE_HOME?.trim();
|
|
30813
|
-
const base = xdgState &&
|
|
31090
|
+
const base = xdgState && isAbsolute6(xdgState) ? xdgState : join16(homedir2(), ".local", "state");
|
|
30814
31091
|
return resolve9(base, "kxm");
|
|
30815
31092
|
}
|
|
30816
31093
|
function hubBindingFile(env = process.env) {
|
|
@@ -30861,7 +31138,7 @@ function readHubBinding(env = process.env) {
|
|
|
30861
31138
|
// plugins/kxm/src/hub-env.ts
|
|
30862
31139
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync12, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
30863
31140
|
import { homedir as homedir3 } from "node:os";
|
|
30864
|
-
import { dirname as dirname9, isAbsolute as
|
|
31141
|
+
import { dirname as dirname9, isAbsolute as isAbsolute7, join as join17, resolve as resolve10 } from "node:path";
|
|
30865
31142
|
var HUB_ENV_SCHEMA = "kxm.hub-env.v1";
|
|
30866
31143
|
var HubEnvError = class extends Error {
|
|
30867
31144
|
constructor(message) {
|
|
@@ -30872,17 +31149,17 @@ var HubEnvError = class extends Error {
|
|
|
30872
31149
|
function resolveUserStateRoot2(env) {
|
|
30873
31150
|
const explicit = env.KXM_STATE_HOME?.trim();
|
|
30874
31151
|
if (explicit) {
|
|
30875
|
-
if (!
|
|
31152
|
+
if (!isAbsolute7(explicit)) throw new HubEnvError("local_state_root_not_absolute");
|
|
30876
31153
|
return resolve10(explicit);
|
|
30877
31154
|
}
|
|
30878
31155
|
if (process.platform === "win32") {
|
|
30879
31156
|
const localAppData = env.LOCALAPPDATA?.trim();
|
|
30880
|
-
const base2 = localAppData &&
|
|
31157
|
+
const base2 = localAppData && isAbsolute7(localAppData) ? localAppData : join17(homedir3(), "AppData", "Local");
|
|
30881
31158
|
return resolve10(base2, "KXM");
|
|
30882
31159
|
}
|
|
30883
31160
|
if (process.platform === "darwin") return resolve10(homedir3(), "Library", "Application Support", "KXM");
|
|
30884
31161
|
const xdgState = env.XDG_STATE_HOME?.trim();
|
|
30885
|
-
const base = xdgState &&
|
|
31162
|
+
const base = xdgState && isAbsolute7(xdgState) ? xdgState : join17(homedir3(), ".local", "state");
|
|
30886
31163
|
return resolve10(base, "kxm");
|
|
30887
31164
|
}
|
|
30888
31165
|
function hubEnvFile(env = process.env) {
|
|
@@ -30933,7 +31210,7 @@ function resolveClientHubAuthToken(env, project) {
|
|
|
30933
31210
|
}
|
|
30934
31211
|
|
|
30935
31212
|
// plugins/kxm/src/logger.ts
|
|
30936
|
-
import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync as renameSync4, statSync as
|
|
31213
|
+
import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync as renameSync4, statSync as statSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
30937
31214
|
import { dirname as dirname10 } from "node:path";
|
|
30938
31215
|
var LOG_LEVEL_PRIORITY = {
|
|
30939
31216
|
debug: 10,
|
|
@@ -31004,7 +31281,7 @@ function createLogger(options) {
|
|
|
31004
31281
|
let currentSize = 0;
|
|
31005
31282
|
if (filePath && existsSync15(filePath)) {
|
|
31006
31283
|
try {
|
|
31007
|
-
currentSize =
|
|
31284
|
+
currentSize = statSync5(filePath).size;
|
|
31008
31285
|
} catch {
|
|
31009
31286
|
currentSize = 0;
|
|
31010
31287
|
}
|
|
@@ -31151,6 +31428,24 @@ function readRecentSupervisorError(paths) {
|
|
|
31151
31428
|
return void 0;
|
|
31152
31429
|
}
|
|
31153
31430
|
}
|
|
31431
|
+
var SETTLED_RUN_STATUS = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
31432
|
+
function kxmSupervisorRecordIsLive(record2) {
|
|
31433
|
+
return supervisorStatusOf(record2).running;
|
|
31434
|
+
}
|
|
31435
|
+
function unsettledRunIds(context) {
|
|
31436
|
+
const ids = [];
|
|
31437
|
+
for (const stored of context.eventStore.runsForProject(context.projectId, 1e4)) {
|
|
31438
|
+
let status = stored.status;
|
|
31439
|
+
try {
|
|
31440
|
+
status = projectKxmRunReadOnly(context, stored.runId).status;
|
|
31441
|
+
} catch {
|
|
31442
|
+
ids.push(stored.runId);
|
|
31443
|
+
continue;
|
|
31444
|
+
}
|
|
31445
|
+
if (!SETTLED_RUN_STATUS.has(status)) ids.push(stored.runId);
|
|
31446
|
+
}
|
|
31447
|
+
return ids;
|
|
31448
|
+
}
|
|
31154
31449
|
function supervisorStatusOf(record2) {
|
|
31155
31450
|
if (!record2) return { running: false };
|
|
31156
31451
|
const heartbeatAgeMs = Date.now() - Date.parse(record2.heartbeatAt);
|
|
@@ -31499,7 +31794,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
|
|
|
31499
31794
|
}
|
|
31500
31795
|
};
|
|
31501
31796
|
const contextFor = (projectRoot) => {
|
|
31502
|
-
if (!
|
|
31797
|
+
if (!isAbsolute8(projectRoot)) {
|
|
31503
31798
|
throw runtimeError("runtime_request_invalid", "projectRoot", "projectRoot must be an absolute path");
|
|
31504
31799
|
}
|
|
31505
31800
|
const key = projectRuntimeKey(projectRoot);
|
|
@@ -31565,6 +31860,62 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
|
|
|
31565
31860
|
sendJson(response, 200, { ok: true, projectId: context.projectId, retried, outbox: context.eventStore.outboxStatus() });
|
|
31566
31861
|
return;
|
|
31567
31862
|
}
|
|
31863
|
+
if (request.method === "POST" && url.pathname === "/v1/projects/unregister") {
|
|
31864
|
+
const body = await readJsonBody(request);
|
|
31865
|
+
const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
|
|
31866
|
+
const force = body.force === true;
|
|
31867
|
+
if (!projectRoot || !isAbsolute8(projectRoot)) {
|
|
31868
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot must be an absolute path" });
|
|
31869
|
+
return;
|
|
31870
|
+
}
|
|
31871
|
+
if (!force) {
|
|
31872
|
+
const key2 = projectRuntimeKey(projectRoot);
|
|
31873
|
+
const existing2 = contexts.get(key2);
|
|
31874
|
+
let openRunIds = [];
|
|
31875
|
+
if (existing2) {
|
|
31876
|
+
openRunIds = unsettledRunIds(existing2);
|
|
31877
|
+
} else {
|
|
31878
|
+
const registration = registry.projectByRoot(projectRoot);
|
|
31879
|
+
const eventsPath = join18(paths.projectsDir, registration?.projectKey ?? projectRuntimeKey(projectRoot), "run-events.db");
|
|
31880
|
+
if (registration && existsSync16(eventsPath)) {
|
|
31881
|
+
const store = new KxmRunEventStore(eventsPath);
|
|
31882
|
+
try {
|
|
31883
|
+
openRunIds = store.runsForProject(registration.projectId, 1e4).filter((run) => !SETTLED_RUN_STATUS.has(run.status)).map((run) => run.runId);
|
|
31884
|
+
} finally {
|
|
31885
|
+
store.close();
|
|
31886
|
+
}
|
|
31887
|
+
}
|
|
31888
|
+
}
|
|
31889
|
+
if (openRunIds.length > 0) {
|
|
31890
|
+
sendJson(response, 409, {
|
|
31891
|
+
ok: false,
|
|
31892
|
+
error: "runtime_project_busy",
|
|
31893
|
+
message: `unsettled run ${openRunIds.join(", ")}`,
|
|
31894
|
+
runIds: openRunIds
|
|
31895
|
+
});
|
|
31896
|
+
return;
|
|
31897
|
+
}
|
|
31898
|
+
const laneCount = registry.homeLaneCount(projectRoot);
|
|
31899
|
+
if (laneCount > 0) {
|
|
31900
|
+
sendJson(response, 409, {
|
|
31901
|
+
ok: false,
|
|
31902
|
+
error: "runtime_project_has_lanes",
|
|
31903
|
+
message: `home root still has ${laneCount} lane(s)`
|
|
31904
|
+
});
|
|
31905
|
+
return;
|
|
31906
|
+
}
|
|
31907
|
+
}
|
|
31908
|
+
const key = projectRuntimeKey(projectRoot);
|
|
31909
|
+
const existing = contexts.get(key);
|
|
31910
|
+
const unregistered = registry.unregisterProject(projectRoot);
|
|
31911
|
+
if (existing) {
|
|
31912
|
+
contexts.delete(key);
|
|
31913
|
+
syncStatuses.delete(key);
|
|
31914
|
+
closeKxmRuntimeContext(existing);
|
|
31915
|
+
}
|
|
31916
|
+
sendJson(response, 200, { ok: true, unregistered });
|
|
31917
|
+
return;
|
|
31918
|
+
}
|
|
31568
31919
|
if (request.method === "POST" && url.pathname === "/v1/runs") {
|
|
31569
31920
|
const body = await readJsonBody(request);
|
|
31570
31921
|
const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
|
|
@@ -31788,7 +32139,10 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
|
|
|
31788
32139
|
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: `project ${requestedProjectId} is not the bound project ${context.projectId}` });
|
|
31789
32140
|
return;
|
|
31790
32141
|
}
|
|
31791
|
-
const
|
|
32142
|
+
const limitText = url.searchParams.get("limit");
|
|
32143
|
+
const parsedLimit = limitText === null ? 50 : Number(limitText);
|
|
32144
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 1e4) : 50;
|
|
32145
|
+
const runs = context.eventStore.runsForProject(requestedProjectId, limit).map((stored) => {
|
|
31792
32146
|
try {
|
|
31793
32147
|
return projectKxmRunReadOnly(context, stored.runId);
|
|
31794
32148
|
} catch (error) {
|
|
@@ -33679,8 +34033,8 @@ ${divider}
|
|
|
33679
34033
|
}
|
|
33680
34034
|
|
|
33681
34035
|
// plugins/kxm/src/ssh-remote.ts
|
|
33682
|
-
import { spawnSync as
|
|
33683
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync16, readdirSync as readdirSync9, rmSync as rmSync5, statSync as
|
|
34036
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
34037
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync16, readdirSync as readdirSync9, rmSync as rmSync5, statSync as statSync6 } from "node:fs";
|
|
33684
34038
|
import { homedir as homedir4 } from "node:os";
|
|
33685
34039
|
import { join as join22, resolve as resolve12 } from "node:path";
|
|
33686
34040
|
var MAX_SSH_OUTPUT_BYTES = 50 * 1024;
|
|
@@ -33755,7 +34109,7 @@ function parseSshConfig(configPath) {
|
|
|
33755
34109
|
return [];
|
|
33756
34110
|
}
|
|
33757
34111
|
}
|
|
33758
|
-
function resolveSshHostG(host, execFn =
|
|
34112
|
+
function resolveSshHostG(host, execFn = spawnSync6) {
|
|
33759
34113
|
try {
|
|
33760
34114
|
const result = execFn("ssh", ["-G", host], { encoding: "utf-8" });
|
|
33761
34115
|
if (result.status !== 0 || !result.stdout) {
|
|
@@ -33821,7 +34175,7 @@ function buildSshArgs(options) {
|
|
|
33821
34175
|
args.push(options.host);
|
|
33822
34176
|
return args;
|
|
33823
34177
|
}
|
|
33824
|
-
function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn =
|
|
34178
|
+
function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync6) {
|
|
33825
34179
|
const resolvedDir = ensureSocketDir(socketDir);
|
|
33826
34180
|
const controlPath = join22(resolvedDir, "%C");
|
|
33827
34181
|
try {
|
|
@@ -33833,7 +34187,7 @@ function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawn
|
|
|
33833
34187
|
return false;
|
|
33834
34188
|
}
|
|
33835
34189
|
}
|
|
33836
|
-
function closeControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn =
|
|
34190
|
+
function closeControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync6) {
|
|
33837
34191
|
const resolvedDir = ensureSocketDir(socketDir);
|
|
33838
34192
|
const controlPath = join22(resolvedDir, "%C");
|
|
33839
34193
|
try {
|
|
@@ -33853,7 +34207,7 @@ function pruneSocketDir(socketDir = DEFAULT_SOCKET_DIR) {
|
|
|
33853
34207
|
for (const entry of entries) {
|
|
33854
34208
|
const fullPath = join22(resolvedDir, entry);
|
|
33855
34209
|
try {
|
|
33856
|
-
const stat =
|
|
34210
|
+
const stat = statSync6(fullPath);
|
|
33857
34211
|
if (stat.isSocket()) {
|
|
33858
34212
|
rmSync5(fullPath, { force: true });
|
|
33859
34213
|
removed++;
|
|
@@ -33867,7 +34221,7 @@ function pruneSocketDir(socketDir = DEFAULT_SOCKET_DIR) {
|
|
|
33867
34221
|
}
|
|
33868
34222
|
function executeSshRun(params) {
|
|
33869
34223
|
const startTime = Date.now();
|
|
33870
|
-
const execSyncFn = params.execFn ??
|
|
34224
|
+
const execSyncFn = params.execFn ?? spawnSync6;
|
|
33871
34225
|
if (params.action === "info") {
|
|
33872
34226
|
if (params.host) {
|
|
33873
34227
|
const hostInfo = resolveSshHostG(params.host, execSyncFn);
|
|
@@ -34314,6 +34668,7 @@ export {
|
|
|
34314
34668
|
acceptKxmRun,
|
|
34315
34669
|
applyRestorePlan,
|
|
34316
34670
|
assertClosedGateObservation,
|
|
34671
|
+
assertDatabaseFile,
|
|
34317
34672
|
assertKxmConfigError,
|
|
34318
34673
|
attachKxmSupervisor,
|
|
34319
34674
|
backupDatabaseFile,
|
|
@@ -34381,6 +34736,7 @@ export {
|
|
|
34381
34736
|
kxmRunRevisionDrift,
|
|
34382
34737
|
kxmRuntimePaths,
|
|
34383
34738
|
kxmRuntimeRequest,
|
|
34739
|
+
kxmSupervisorRecordIsLive,
|
|
34384
34740
|
kxmSupervisorStatus,
|
|
34385
34741
|
kxmSupervisorTokenFile,
|
|
34386
34742
|
kxmToolPolicyRevision,
|