@kontextmind/kxm 0.7.102 → 0.7.104
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 +30 -9
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contributing/test-matrix.md +7 -5
- package/docs/operations/backup-and-restore.md +59 -27
- package/docs/operations/deploy.md +1 -1
- package/docs/reference/cli-reference.md +32 -21
- package/docs/reference/config-reference.md +4 -2
- package/docs/reference/harness-routing.md +5 -5
- package/docs/reference/workflow-catalog.md +1 -1
- package/docs/start/quickstart-claude-code.md +1 -1
- package/docs/templates/runbook.md +2 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +788 -651
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +321 -291
- package/plugins/kxm/dist/runtime.js +424 -348
- package/plugins/kxm/dist/server.js +78 -78
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +14 -7
- package/plugins/kxm/src/cli/project.ts +66 -12
- package/plugins/kxm/src/cli.ts +7 -5
- package/plugins/kxm/src/database.ts +193 -65
- package/plugins/kxm/src/init-guide-setup.ts +9 -4
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-paths.ts +50 -0
- package/plugins/kxm/src/runtime-store.ts +22 -53
- package/plugins/kxm/src/runtime-supervisor.ts +38 -17
- package/schemas/backup-manifest.schema.json +15 -0
|
@@ -3872,49 +3872,49 @@ var require_fast_uri = __commonJS({
|
|
|
3872
3872
|
schemelessOptions.skipEscape = true;
|
|
3873
3873
|
return serialize(resolved, schemelessOptions);
|
|
3874
3874
|
}
|
|
3875
|
-
function resolveComponent(base,
|
|
3875
|
+
function resolveComponent(base, relative3, options, skipNormalization) {
|
|
3876
3876
|
const target = {};
|
|
3877
3877
|
if (!skipNormalization) {
|
|
3878
3878
|
base = parse3(serialize(base, options), options);
|
|
3879
|
-
|
|
3879
|
+
relative3 = parse3(serialize(relative3, options), options);
|
|
3880
3880
|
}
|
|
3881
3881
|
options = options || {};
|
|
3882
|
-
if (!options.tolerant &&
|
|
3883
|
-
target.scheme =
|
|
3884
|
-
target.userinfo =
|
|
3885
|
-
target.host =
|
|
3886
|
-
target.port =
|
|
3887
|
-
target.path = removeDotSegments(
|
|
3888
|
-
target.query =
|
|
3882
|
+
if (!options.tolerant && relative3.scheme) {
|
|
3883
|
+
target.scheme = relative3.scheme;
|
|
3884
|
+
target.userinfo = relative3.userinfo;
|
|
3885
|
+
target.host = relative3.host;
|
|
3886
|
+
target.port = relative3.port;
|
|
3887
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3888
|
+
target.query = relative3.query;
|
|
3889
3889
|
} else {
|
|
3890
|
-
if (
|
|
3891
|
-
target.userinfo =
|
|
3892
|
-
target.host =
|
|
3893
|
-
target.port =
|
|
3894
|
-
target.path = removeDotSegments(
|
|
3895
|
-
target.query =
|
|
3890
|
+
if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
|
|
3891
|
+
target.userinfo = relative3.userinfo;
|
|
3892
|
+
target.host = relative3.host;
|
|
3893
|
+
target.port = relative3.port;
|
|
3894
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3895
|
+
target.query = relative3.query;
|
|
3896
3896
|
} else {
|
|
3897
|
-
if (!
|
|
3897
|
+
if (!relative3.path) {
|
|
3898
3898
|
target.path = base.path;
|
|
3899
|
-
if (
|
|
3900
|
-
target.query =
|
|
3899
|
+
if (relative3.query !== void 0) {
|
|
3900
|
+
target.query = relative3.query;
|
|
3901
3901
|
} else {
|
|
3902
3902
|
target.query = base.query;
|
|
3903
3903
|
}
|
|
3904
3904
|
} else {
|
|
3905
|
-
if (
|
|
3906
|
-
target.path = removeDotSegments(
|
|
3905
|
+
if (relative3.path[0] === "/") {
|
|
3906
|
+
target.path = removeDotSegments(relative3.path);
|
|
3907
3907
|
} else {
|
|
3908
3908
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3909
|
-
target.path = "/" +
|
|
3909
|
+
target.path = "/" + relative3.path;
|
|
3910
3910
|
} else if (!base.path) {
|
|
3911
|
-
target.path =
|
|
3911
|
+
target.path = relative3.path;
|
|
3912
3912
|
} else {
|
|
3913
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3913
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
|
|
3914
3914
|
}
|
|
3915
3915
|
target.path = removeDotSegments(target.path);
|
|
3916
3916
|
}
|
|
3917
|
-
target.query =
|
|
3917
|
+
target.query = relative3.query;
|
|
3918
3918
|
}
|
|
3919
3919
|
target.userinfo = base.userinfo;
|
|
3920
3920
|
target.host = base.host;
|
|
@@ -3922,7 +3922,7 @@ var require_fast_uri = __commonJS({
|
|
|
3922
3922
|
}
|
|
3923
3923
|
target.scheme = base.scheme;
|
|
3924
3924
|
}
|
|
3925
|
-
target.fragment =
|
|
3925
|
+
target.fragment = relative3.fragment;
|
|
3926
3926
|
return target;
|
|
3927
3927
|
}
|
|
3928
3928
|
function equal(uriA, uriB, options) {
|
|
@@ -11411,10 +11411,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
11411
11411
|
let offset = bm.offset;
|
|
11412
11412
|
let commentEnd = null;
|
|
11413
11413
|
for (const collItem of bm.items) {
|
|
11414
|
-
const { start, key, sep:
|
|
11414
|
+
const { start, key, sep: sep3, value } = collItem;
|
|
11415
11415
|
const keyProps = resolveProps.resolveProps(start, {
|
|
11416
11416
|
indicator: "explicit-key-ind",
|
|
11417
|
-
next: key ??
|
|
11417
|
+
next: key ?? sep3?.[0],
|
|
11418
11418
|
offset,
|
|
11419
11419
|
onError,
|
|
11420
11420
|
parentIndent: bm.indent,
|
|
@@ -11428,7 +11428,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
11428
11428
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
11429
11429
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
11430
11430
|
}
|
|
11431
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
11431
|
+
if (!keyProps.anchor && !keyProps.tag && !sep3) {
|
|
11432
11432
|
commentEnd = keyProps.end;
|
|
11433
11433
|
if (keyProps.comment) {
|
|
11434
11434
|
if (map.comment)
|
|
@@ -11452,7 +11452,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
11452
11452
|
ctx.atKey = false;
|
|
11453
11453
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
11454
11454
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
11455
|
-
const valueProps = resolveProps.resolveProps(
|
|
11455
|
+
const valueProps = resolveProps.resolveProps(sep3 ?? [], {
|
|
11456
11456
|
indicator: "map-value-ind",
|
|
11457
11457
|
next: value,
|
|
11458
11458
|
offset: keyNode.range[2],
|
|
@@ -11468,7 +11468,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
11468
11468
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
11469
11469
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
11470
11470
|
}
|
|
11471
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
11471
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep3, null, valueProps, onError);
|
|
11472
11472
|
if (ctx.schema.compat)
|
|
11473
11473
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
11474
11474
|
offset = valueNode.range[2];
|
|
@@ -11559,7 +11559,7 @@ var require_resolve_end = __commonJS({
|
|
|
11559
11559
|
let comment = "";
|
|
11560
11560
|
if (end) {
|
|
11561
11561
|
let hasSpace = false;
|
|
11562
|
-
let
|
|
11562
|
+
let sep3 = "";
|
|
11563
11563
|
for (const token of end) {
|
|
11564
11564
|
const { source, type } = token;
|
|
11565
11565
|
switch (type) {
|
|
@@ -11573,13 +11573,13 @@ var require_resolve_end = __commonJS({
|
|
|
11573
11573
|
if (!comment)
|
|
11574
11574
|
comment = cb;
|
|
11575
11575
|
else
|
|
11576
|
-
comment +=
|
|
11577
|
-
|
|
11576
|
+
comment += sep3 + cb;
|
|
11577
|
+
sep3 = "";
|
|
11578
11578
|
break;
|
|
11579
11579
|
}
|
|
11580
11580
|
case "newline":
|
|
11581
11581
|
if (comment)
|
|
11582
|
-
|
|
11582
|
+
sep3 += source;
|
|
11583
11583
|
hasSpace = true;
|
|
11584
11584
|
break;
|
|
11585
11585
|
default:
|
|
@@ -11622,18 +11622,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11622
11622
|
let offset = fc.offset + fc.start.source.length;
|
|
11623
11623
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
11624
11624
|
const collItem = fc.items[i];
|
|
11625
|
-
const { start, key, sep:
|
|
11625
|
+
const { start, key, sep: sep3, value } = collItem;
|
|
11626
11626
|
const props = resolveProps.resolveProps(start, {
|
|
11627
11627
|
flow: fcName,
|
|
11628
11628
|
indicator: "explicit-key-ind",
|
|
11629
|
-
next: key ??
|
|
11629
|
+
next: key ?? sep3?.[0],
|
|
11630
11630
|
offset,
|
|
11631
11631
|
onError,
|
|
11632
11632
|
parentIndent: fc.indent,
|
|
11633
11633
|
startOnNewline: false
|
|
11634
11634
|
});
|
|
11635
11635
|
if (!props.found) {
|
|
11636
|
-
if (!props.anchor && !props.tag && !
|
|
11636
|
+
if (!props.anchor && !props.tag && !sep3 && !value) {
|
|
11637
11637
|
if (i === 0 && props.comma)
|
|
11638
11638
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
11639
11639
|
else if (i < fc.items.length - 1)
|
|
@@ -11687,8 +11687,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11687
11687
|
}
|
|
11688
11688
|
}
|
|
11689
11689
|
}
|
|
11690
|
-
if (!isMap2 && !
|
|
11691
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
11690
|
+
if (!isMap2 && !sep3 && !props.found) {
|
|
11691
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep3, null, props, onError);
|
|
11692
11692
|
coll.items.push(valueNode);
|
|
11693
11693
|
offset = valueNode.range[2];
|
|
11694
11694
|
if (isBlock(value))
|
|
@@ -11700,7 +11700,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11700
11700
|
if (isBlock(key))
|
|
11701
11701
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
11702
11702
|
ctx.atKey = false;
|
|
11703
|
-
const valueProps = resolveProps.resolveProps(
|
|
11703
|
+
const valueProps = resolveProps.resolveProps(sep3 ?? [], {
|
|
11704
11704
|
flow: fcName,
|
|
11705
11705
|
indicator: "map-value-ind",
|
|
11706
11706
|
next: value,
|
|
@@ -11711,8 +11711,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11711
11711
|
});
|
|
11712
11712
|
if (valueProps.found) {
|
|
11713
11713
|
if (!isMap2 && !props.found && ctx.options.strict) {
|
|
11714
|
-
if (
|
|
11715
|
-
for (const st of
|
|
11714
|
+
if (sep3)
|
|
11715
|
+
for (const st of sep3) {
|
|
11716
11716
|
if (st === valueProps.found)
|
|
11717
11717
|
break;
|
|
11718
11718
|
if (st.type === "newline") {
|
|
@@ -11729,7 +11729,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11729
11729
|
else
|
|
11730
11730
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
11731
11731
|
}
|
|
11732
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
11732
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep3, null, valueProps, onError) : null;
|
|
11733
11733
|
if (valueNode) {
|
|
11734
11734
|
if (isBlock(value))
|
|
11735
11735
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -11909,7 +11909,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
11909
11909
|
chompStart = i + 1;
|
|
11910
11910
|
}
|
|
11911
11911
|
let value = "";
|
|
11912
|
-
let
|
|
11912
|
+
let sep3 = "";
|
|
11913
11913
|
let prevMoreIndented = false;
|
|
11914
11914
|
for (let i = 0; i < contentStart; ++i)
|
|
11915
11915
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -11926,24 +11926,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
11926
11926
|
indent = "";
|
|
11927
11927
|
}
|
|
11928
11928
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
11929
|
-
value +=
|
|
11930
|
-
|
|
11929
|
+
value += sep3 + indent.slice(trimIndent) + content;
|
|
11930
|
+
sep3 = "\n";
|
|
11931
11931
|
} else if (indent.length > trimIndent || content[0] === " ") {
|
|
11932
|
-
if (
|
|
11933
|
-
|
|
11934
|
-
else if (!prevMoreIndented &&
|
|
11935
|
-
|
|
11936
|
-
value +=
|
|
11937
|
-
|
|
11932
|
+
if (sep3 === " ")
|
|
11933
|
+
sep3 = "\n";
|
|
11934
|
+
else if (!prevMoreIndented && sep3 === "\n")
|
|
11935
|
+
sep3 = "\n\n";
|
|
11936
|
+
value += sep3 + indent.slice(trimIndent) + content;
|
|
11937
|
+
sep3 = "\n";
|
|
11938
11938
|
prevMoreIndented = true;
|
|
11939
11939
|
} else if (content === "") {
|
|
11940
|
-
if (
|
|
11940
|
+
if (sep3 === "\n")
|
|
11941
11941
|
value += "\n";
|
|
11942
11942
|
else
|
|
11943
|
-
|
|
11943
|
+
sep3 = "\n";
|
|
11944
11944
|
} else {
|
|
11945
|
-
value +=
|
|
11946
|
-
|
|
11945
|
+
value += sep3 + content;
|
|
11946
|
+
sep3 = " ";
|
|
11947
11947
|
prevMoreIndented = false;
|
|
11948
11948
|
}
|
|
11949
11949
|
}
|
|
@@ -12126,25 +12126,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
12126
12126
|
trimBoth = /^[ \t]+|[ \t]+$/g;
|
|
12127
12127
|
}
|
|
12128
12128
|
let res = match[1].replace(trimEnd, "");
|
|
12129
|
-
let
|
|
12129
|
+
let sep3 = " ";
|
|
12130
12130
|
let pos = line.lastIndex;
|
|
12131
12131
|
while (match = line.exec(source)) {
|
|
12132
12132
|
const lm = match[1].replace(trimBoth, "");
|
|
12133
12133
|
if (lm === "") {
|
|
12134
|
-
if (
|
|
12135
|
-
res +=
|
|
12134
|
+
if (sep3 === "\n")
|
|
12135
|
+
res += sep3;
|
|
12136
12136
|
else
|
|
12137
|
-
|
|
12137
|
+
sep3 = "\n";
|
|
12138
12138
|
} else {
|
|
12139
|
-
res +=
|
|
12140
|
-
|
|
12139
|
+
res += sep3 + lm;
|
|
12140
|
+
sep3 = " ";
|
|
12141
12141
|
}
|
|
12142
12142
|
pos = line.lastIndex;
|
|
12143
12143
|
}
|
|
12144
12144
|
const last = /[ \t]*(.*)/sy;
|
|
12145
12145
|
last.lastIndex = pos;
|
|
12146
12146
|
match = last.exec(source);
|
|
12147
|
-
return res +
|
|
12147
|
+
return res + sep3 + (match?.[1] ?? "");
|
|
12148
12148
|
}
|
|
12149
12149
|
function doubleQuotedValue(source, onError) {
|
|
12150
12150
|
let res = "";
|
|
@@ -12954,14 +12954,14 @@ var require_cst_stringify = __commonJS({
|
|
|
12954
12954
|
}
|
|
12955
12955
|
}
|
|
12956
12956
|
}
|
|
12957
|
-
function stringifyItem({ start, key, sep:
|
|
12957
|
+
function stringifyItem({ start, key, sep: sep3, value }) {
|
|
12958
12958
|
let res = "";
|
|
12959
12959
|
for (const st of start)
|
|
12960
12960
|
res += st.source;
|
|
12961
12961
|
if (key)
|
|
12962
12962
|
res += stringifyToken(key);
|
|
12963
|
-
if (
|
|
12964
|
-
for (const st of
|
|
12963
|
+
if (sep3)
|
|
12964
|
+
for (const st of sep3)
|
|
12965
12965
|
res += st.source;
|
|
12966
12966
|
if (value)
|
|
12967
12967
|
res += stringifyToken(value);
|
|
@@ -14128,18 +14128,18 @@ var require_parser = __commonJS({
|
|
|
14128
14128
|
if (this.type === "map-value-ind") {
|
|
14129
14129
|
const prev = getPrevProps(this.peek(2));
|
|
14130
14130
|
const start = getFirstKeyStartProps(prev);
|
|
14131
|
-
let
|
|
14131
|
+
let sep3;
|
|
14132
14132
|
if (scalar.end) {
|
|
14133
|
-
|
|
14134
|
-
|
|
14133
|
+
sep3 = scalar.end;
|
|
14134
|
+
sep3.push(this.sourceToken);
|
|
14135
14135
|
delete scalar.end;
|
|
14136
14136
|
} else
|
|
14137
|
-
|
|
14137
|
+
sep3 = [this.sourceToken];
|
|
14138
14138
|
const map = {
|
|
14139
14139
|
type: "block-map",
|
|
14140
14140
|
offset: scalar.offset,
|
|
14141
14141
|
indent: scalar.indent,
|
|
14142
|
-
items: [{ start, key: scalar, sep:
|
|
14142
|
+
items: [{ start, key: scalar, sep: sep3 }]
|
|
14143
14143
|
};
|
|
14144
14144
|
this.onKeyLine = true;
|
|
14145
14145
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -14292,15 +14292,15 @@ var require_parser = __commonJS({
|
|
|
14292
14292
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
14293
14293
|
const start2 = getFirstKeyStartProps(it.start);
|
|
14294
14294
|
const key = it.key;
|
|
14295
|
-
const
|
|
14296
|
-
|
|
14295
|
+
const sep3 = it.sep;
|
|
14296
|
+
sep3.push(this.sourceToken);
|
|
14297
14297
|
delete it.key;
|
|
14298
14298
|
delete it.sep;
|
|
14299
14299
|
this.stack.push({
|
|
14300
14300
|
type: "block-map",
|
|
14301
14301
|
offset: this.offset,
|
|
14302
14302
|
indent: this.indent,
|
|
14303
|
-
items: [{ start: start2, key, sep:
|
|
14303
|
+
items: [{ start: start2, key, sep: sep3 }]
|
|
14304
14304
|
});
|
|
14305
14305
|
} else if (start.length > 0) {
|
|
14306
14306
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -14494,13 +14494,13 @@ var require_parser = __commonJS({
|
|
|
14494
14494
|
const prev = getPrevProps(parent);
|
|
14495
14495
|
const start = getFirstKeyStartProps(prev);
|
|
14496
14496
|
fixFlowSeqItems(fc);
|
|
14497
|
-
const
|
|
14498
|
-
|
|
14497
|
+
const sep3 = fc.end.splice(1, fc.end.length);
|
|
14498
|
+
sep3.push(this.sourceToken);
|
|
14499
14499
|
const map = {
|
|
14500
14500
|
type: "block-map",
|
|
14501
14501
|
offset: fc.offset,
|
|
14502
14502
|
indent: fc.indent,
|
|
14503
|
-
items: [{ start, key: fc, sep:
|
|
14503
|
+
items: [{ start, key: fc, sep: sep3 }]
|
|
14504
14504
|
};
|
|
14505
14505
|
this.onKeyLine = true;
|
|
14506
14506
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -19384,7 +19384,7 @@ import {
|
|
|
19384
19384
|
unlinkSync,
|
|
19385
19385
|
writeFileSync as writeFileSync4
|
|
19386
19386
|
} from "node:fs";
|
|
19387
|
-
import { basename as basename3, dirname as dirname5, join as join5, resolve as resolve4 } from "node:path";
|
|
19387
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
|
|
19388
19388
|
|
|
19389
19389
|
// plugins/kxm/src/sqlite.ts
|
|
19390
19390
|
import { createRequire } from "node:module";
|
package/plugins/kxm/package.json
CHANGED
|
@@ -15,7 +15,7 @@ invent restart or status subcommands.
|
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| `kxm hub view` | Check `/health` and `/ready`; exits 1 when the hub is down | `--json` |
|
|
17
17
|
| `kxm tenant status` | Hub metadata and Runtime run state as one labeled view; reads only and never starts the supervisor | `--json` |
|
|
18
|
-
| `kxm backup` | Verified SQLite backup of
|
|
18
|
+
| `kxm backup` | Verified SQLite backup of this project's hub store and Runtime event store with a hashed manifest; exits 1 when the backup is incomplete | `--out <dir>`, `--all-projects`, `--json` |
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
kxm hub view --json
|
|
@@ -30,9 +30,12 @@ runs have separate ID spaces, so a comparison with no shared ID is
|
|
|
30
30
|
`unverified`, never agreement.
|
|
31
31
|
|
|
32
32
|
The durable hub store defaults to `.kxm/state/kxm.db` (`KXM_DATA_PATH`). Do not
|
|
33
|
-
hand-edit it. `kxm backup` also copies
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
hand-edit it. `kxm backup` also copies this project's Runtime event store and
|
|
34
|
+
prompt sidecar from the user state root. The shared Runtime registry and other
|
|
35
|
+
projects' event stores are copied only with `--all-projects`, and only
|
|
36
|
+
`kxm restore --all-projects` restores them, rolling back every project on the
|
|
37
|
+
machine. Do not pass `--all-projects` unless the user asked for a whole-machine
|
|
38
|
+
backup or restore.
|
|
36
39
|
|
|
37
40
|
## Operator steps
|
|
38
41
|
|
|
@@ -45,7 +48,7 @@ the hub admin token or a project token.
|
|
|
45
48
|
| `kxm hub stop` | Request managed hub and worker shutdown | `--wait-ms <ms>` |
|
|
46
49
|
| `kxm hub bind <url>` | Bind this machine to a running hub | http or https URL |
|
|
47
50
|
| `kxm hub unbind` | Remove this machine's hub binding | `--json` |
|
|
48
|
-
| `kxm restore <manifest>` | Replace
|
|
51
|
+
| `kxm restore <manifest>` | Replace this project's live stores from a verified backup manifest | `--all-projects`, `--json` |
|
|
49
52
|
|
|
50
53
|
```bash
|
|
51
54
|
kxm hub start
|
|
@@ -70,5 +73,9 @@ kxm restore .kxm/backups/pre-upgrade/manifest.json
|
|
|
70
73
|
claim is reclaimed automatically, an orphaned server is terminated first,
|
|
71
74
|
and `kxm hub stop` recovers such orphans directly.
|
|
72
75
|
- `kxm restore` replaces the live stores. Preview it with
|
|
73
|
-
`kxm restore <manifest> --dry-run`,
|
|
74
|
-
|
|
76
|
+
`kxm restore <manifest> --dry-run`, and never skip restore verification. It
|
|
77
|
+
refuses before writing while the Runtime supervisor runs
|
|
78
|
+
(`restore_runtime_running`) or a hub holds the hub store
|
|
79
|
+
(`restore_hub_running`), so stop both first (`kxm runtime stop`,
|
|
80
|
+
`kxm hub stop`). A backup that holds the registry or another project's
|
|
81
|
+
event store is refused with `restore_requires_all_projects`.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import { createBackup, planBackup, planRestore,
|
|
6
|
+
import { applyRestorePlan, createBackup, databaseError, planBackup, planRestore, type RestorePlan } from "../database.ts";
|
|
7
|
+
import { readLiveHubClaim } from "../hub-autostart.ts";
|
|
7
8
|
import { refreshModelInventory } from "../model-inventory.ts";
|
|
8
9
|
import { listInventoryModels, listRoleBindings, loadRoutePolicy, setRouteState, updateRouteState } from "../routes.ts";
|
|
9
10
|
import {
|
|
@@ -181,23 +182,30 @@ export async function cmdKxmInit(
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
|
|
185
|
+
/** The checkout backup and restore act for, found as the Runtime finds it, so the
|
|
186
|
+
* project's event store key is the one the Runtime derives. */
|
|
187
|
+
function backupProjectRoot(runtime: Runtime): string {
|
|
188
|
+
return discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined; allProjects?: boolean | undefined }): Promise<number> {
|
|
185
192
|
try {
|
|
186
193
|
const backupOptions = {
|
|
187
|
-
projectRoot: runtime
|
|
194
|
+
projectRoot: backupProjectRoot(runtime),
|
|
188
195
|
env: runtime.env,
|
|
196
|
+
...(options.allProjects === true ? { allProjects: true } : {}),
|
|
189
197
|
...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
|
|
190
198
|
};
|
|
191
199
|
if (runtime.dryRun) {
|
|
192
200
|
const plan = planBackup(backupOptions);
|
|
193
201
|
printPlan(
|
|
194
202
|
runtime,
|
|
195
|
-
{ command: "backup", outDir: plan.outDir, stores: plan.stores, files: plan.files },
|
|
203
|
+
{ command: "backup", scope: plan.scope, outDir: plan.outDir, stores: plan.stores, files: plan.files },
|
|
196
204
|
[
|
|
197
205
|
...[...plan.stores, ...plan.files].map((entry) => ({ action: "write" as const, target: join(plan.outDir, entry.backupFile) })),
|
|
198
206
|
{ action: "write", target: join(plan.outDir, "manifest.json") },
|
|
199
207
|
],
|
|
200
|
-
`back up ${plan.stores.length} store(s) and ${plan.files.length} file(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
|
|
208
|
+
`back up ${plan.stores.length} store(s) and ${plan.files.length} file(s) (${plan.scope} scope) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
|
|
201
209
|
);
|
|
202
210
|
return 0;
|
|
203
211
|
}
|
|
@@ -212,7 +220,7 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
|
|
|
212
220
|
};
|
|
213
221
|
const summary = [
|
|
214
222
|
complete
|
|
215
|
-
? `Created SQLite backup with ${manifest.stores.length} store(s):`
|
|
223
|
+
? `Created SQLite backup with ${manifest.stores.length} store(s) (${manifest.scope ?? "project"} scope):`
|
|
216
224
|
: `Backup is incomplete (${manifest.omitted?.length ?? 0} omitted); not ok:`,
|
|
217
225
|
...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
|
|
218
226
|
...(manifest.omitted ?? []).map((id) => ` - omitted ${id}`),
|
|
@@ -230,16 +238,63 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
|
|
|
230
238
|
}
|
|
231
239
|
}
|
|
232
240
|
|
|
233
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Refuse while anything holds the stores a restore would overwrite. Replacing a
|
|
243
|
+
* SQLite file under an open connection loses the writer's next commit or corrupts
|
|
244
|
+
* the store, and the supervisor holds the registry and every project's event store
|
|
245
|
+
* open. Reads only: the registry is opened read-only and the hub claim is a file,
|
|
246
|
+
* so the check is the same under --dry-run.
|
|
247
|
+
*/
|
|
248
|
+
function assertRestoreTargetsStopped(runtime: Runtime, plan: RestorePlan): void {
|
|
249
|
+
const paths = kxmRuntimePaths({ env: runtime.env });
|
|
250
|
+
let supervisor: ReturnType<typeof kxmSupervisorStatus>;
|
|
234
251
|
try {
|
|
252
|
+
supervisor = kxmSupervisorStatus(paths, { readOnly: true });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// Fail closed, but name the way out: a registry too broken to read is also one a
|
|
255
|
+
// restore may be meant to replace.
|
|
256
|
+
throw databaseError(
|
|
257
|
+
"restore_runtime_unverified",
|
|
258
|
+
paths.registryDb,
|
|
259
|
+
`cannot read the Runtime registry to check whether the supervisor is running (${error instanceof Error ? error.message : String(error)}); stop the Runtime, move the registry aside, and restore again`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (supervisor.running) {
|
|
263
|
+
throw databaseError(
|
|
264
|
+
"restore_runtime_running",
|
|
265
|
+
paths.registryDb,
|
|
266
|
+
`the Runtime supervisor is running (pid ${String(supervisor.pid)}); stop it with \`kxm runtime stop\` and keep it stopped until the restore finishes`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
for (const store of plan.stores) {
|
|
270
|
+
if (store.storeId !== "hub-store") continue;
|
|
271
|
+
const claim = readLiveHubClaim(dirname(store.targetPath));
|
|
272
|
+
if (claim) {
|
|
273
|
+
throw databaseError(
|
|
274
|
+
"restore_hub_running",
|
|
275
|
+
store.targetPath,
|
|
276
|
+
`a hub (pid ${String(claim.pid)}) is running on ${store.targetPath}; stop it with \`kxm hub stop\` before restoring`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function cmdRestore(runtime: Runtime, manifestArg: string, options: { allProjects?: boolean | undefined } = {}): Promise<number> {
|
|
283
|
+
try {
|
|
284
|
+
const plan = planRestore(resolve(runtime.cwd, manifestArg), {
|
|
285
|
+
projectRoot: backupProjectRoot(runtime),
|
|
286
|
+
env: runtime.env,
|
|
287
|
+
...(options.allProjects === true ? { allProjects: true } : {}),
|
|
288
|
+
});
|
|
289
|
+
assertRestoreTargetsStopped(runtime, plan);
|
|
235
290
|
if (runtime.dryRun) {
|
|
236
|
-
const plan = planRestore(resolve(runtime.cwd, manifestArg), { projectRoot: runtime.cwd });
|
|
237
291
|
printPlan(
|
|
238
292
|
runtime,
|
|
239
293
|
{
|
|
240
294
|
command: "restore",
|
|
241
295
|
backupId: plan.backupId,
|
|
242
296
|
manifestPath: plan.manifestPath,
|
|
297
|
+
...(plan.scope !== undefined ? { scope: plan.scope } : {}),
|
|
243
298
|
stores: plan.stores.map(({ storeId, targetPath, schemaVersion }) => ({ storeId, targetPath, schemaVersion })),
|
|
244
299
|
files: plan.files.map(({ id, targetPath }) => ({ id, targetPath })),
|
|
245
300
|
},
|
|
@@ -256,14 +311,13 @@ export async function cmdRestore(runtime: Runtime, manifestArg: string): Promise
|
|
|
256
311
|
);
|
|
257
312
|
return 0;
|
|
258
313
|
}
|
|
259
|
-
const result =
|
|
260
|
-
projectRoot: runtime.cwd,
|
|
261
|
-
});
|
|
314
|
+
const result = applyRestorePlan(plan);
|
|
262
315
|
const payload = {
|
|
263
316
|
ok: true,
|
|
264
317
|
command: "restore",
|
|
265
318
|
backupId: result.backupId,
|
|
266
319
|
manifestPath: result.manifestPath,
|
|
320
|
+
...(plan.scope !== undefined ? { scope: plan.scope } : {}),
|
|
267
321
|
restoredStores: result.restoredStores,
|
|
268
322
|
};
|
|
269
323
|
const summary = [
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -419,15 +419,17 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
419
419
|
});
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
-
addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of
|
|
422
|
+
addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of this project's hub store and Runtime event store, with a hashed manifest"))
|
|
423
423
|
.option("--out <dir>", "Directory to write backup and manifest")
|
|
424
|
-
.
|
|
424
|
+
.option("--all-projects", "Also back up the shared Runtime registry and every project's event store on this machine")
|
|
425
|
+
.action(async function backupAction(this: Command, options: { out?: string; allProjects?: boolean }) {
|
|
425
426
|
result.code = await cmdBackup(runtimeFrom(ctx, this), options);
|
|
426
427
|
});
|
|
427
428
|
|
|
428
|
-
addGlobalOptions(program.command("restore <manifest>").description("Restore SQLite stores from a verified backup manifest"))
|
|
429
|
-
.
|
|
430
|
-
|
|
429
|
+
addGlobalOptions(program.command("restore <manifest>").description("Restore this project's SQLite stores from a verified backup manifest; refuses while the Runtime or hub is running"))
|
|
430
|
+
.option("--all-projects", "Also restore the shared Runtime registry and other projects' event stores the backup holds")
|
|
431
|
+
.action(async function restoreAction(this: Command, manifest: string, options: { allProjects?: boolean }) {
|
|
432
|
+
result.code = await cmdRestore(runtimeFrom(ctx, this), manifest, options);
|
|
431
433
|
});
|
|
432
434
|
|
|
433
435
|
addGlobalOptions(program.command("run").description("Create a KXM run (offline-first; kxm runs drive <runId> --simulated executes it model-free)")
|