sphica 0.6.20 → 0.6.21
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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/db/migrations/0008.check.sql +33 -0
- package/db/migrations/0008.sql +53 -0
- package/db/schema.sql +16 -1
- package/dist/capture.js +57 -22
- package/dist/cli.js +636 -596
- package/dist/deliver.js +55 -22
- package/dist/mcp-record.js +55 -22
- package/dist/mcp.js +18 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2966,8 +2966,8 @@ async function run(app, inputs, context) {
|
|
|
2966
2966
|
}
|
|
2967
2967
|
|
|
2968
2968
|
// server/src/admin.ts
|
|
2969
|
-
import
|
|
2970
|
-
import
|
|
2969
|
+
import fs8 from "node:fs";
|
|
2970
|
+
import path9 from "node:path";
|
|
2971
2971
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
2972
2972
|
|
|
2973
2973
|
// server/src/assets.ts
|
|
@@ -3626,7 +3626,7 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
3626
3626
|
|
|
3627
3627
|
// server/src/sqlite.ts
|
|
3628
3628
|
var SCHEMA_GENERATION = 2;
|
|
3629
|
-
var SCHEMA_REVISION =
|
|
3629
|
+
var SCHEMA_REVISION = 8;
|
|
3630
3630
|
var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os2.homedir(), ".sphica");
|
|
3631
3631
|
var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
|
|
3632
3632
|
function requireRuntime() {
|
|
@@ -3726,117 +3726,10 @@ function backups(file = dbFile()) {
|
|
|
3726
3726
|
return fs4.readdirSync(dir).filter((f) => named.test(f)).sort((a2, b) => (named.exec(b)?.[1] ?? "").localeCompare(named.exec(a2)?.[1] ?? "") || b.localeCompare(a2)).map((f) => path4.join(dir, f));
|
|
3727
3727
|
}
|
|
3728
3728
|
|
|
3729
|
-
// server/src/
|
|
3730
|
-
import {
|
|
3731
|
-
import
|
|
3732
|
-
|
|
3733
|
-
var columns = () => process.stdout.isTTY ? Math.max(10, process.stdout.columns || 100) : 1e4;
|
|
3734
|
-
function capture(draw) {
|
|
3735
|
-
let text = "";
|
|
3736
|
-
const output = new Writable({
|
|
3737
|
-
write(chunk, _encoding, done) {
|
|
3738
|
-
text += String(chunk);
|
|
3739
|
-
done();
|
|
3740
|
-
}
|
|
3741
|
-
});
|
|
3742
|
-
Object.assign(output, { columns: columns(), isTTY: true });
|
|
3743
|
-
draw(output);
|
|
3744
|
-
return text.replace(/\n$/, "");
|
|
3745
|
-
}
|
|
3746
|
-
var SGR = /(\u001b\[[0-9;]*m)/;
|
|
3747
|
-
var clean2 = (text) => text.split(SGR).map((piece, i2) => i2 % 2 ? piece : plain(piece)).join("");
|
|
3748
|
-
var oneLine = (text) => clean2(text).replace(/[\n\t]+/g, " ");
|
|
3749
|
-
var bold = (text) => styleText3("bold", text);
|
|
3750
|
-
var dim = (text) => styleText3("dim", text);
|
|
3751
|
-
var plainLine = (text) => wrapAnsi(oneLine(text), columns() - 2, { hard: true, trim: true }).split(`
|
|
3752
|
-
`).join(`
|
|
3753
|
-
`);
|
|
3754
|
-
var GUIDE = 3;
|
|
3755
|
-
var MARKED = /^(?:\u001b\[[0-9;]*m)*[✓△✗○](?:\u001b\[[0-9;]*m)* /;
|
|
3756
|
-
function wrapLine(line, room) {
|
|
3757
|
-
const body = line.trimStart();
|
|
3758
|
-
if (body === "")
|
|
3759
|
-
return [""];
|
|
3760
|
-
const lead = line.length - body.length;
|
|
3761
|
-
const cells = MARKED.test(body) ? /^(.*\S\s{2,})(\S.*)$/.exec(body) : null;
|
|
3762
|
-
const label = cells?.[1] ?? "";
|
|
3763
|
-
const hang = lead + dist_default2(label);
|
|
3764
|
-
const [head2, text, at] = cells?.[2] && room - hang >= 10 ? [label, cells[2], hang] : ["", body, lead];
|
|
3765
|
-
return wrapAnsi(text, Math.max(1, room - at), { hard: true, trim: true }).split(`
|
|
3766
|
-
`).map((part, i2) => i2 === 0 ? `${" ".repeat(lead)}${head2}${part}` : `${" ".repeat(at)}${part}`);
|
|
3767
|
-
}
|
|
3768
|
-
function content(lines, spacing = 0) {
|
|
3769
|
-
const cleaned = lines.flatMap((l) => clean2(l).split(`
|
|
3770
|
-
`));
|
|
3771
|
-
if (!colored2()) {
|
|
3772
|
-
const wrapped2 = cleaned.flatMap((l) => wrapLine(l, columns() - 2));
|
|
3773
|
-
return wrapped2.map((l) => l ? ` ${l}` : "").join(`
|
|
3774
|
-
`);
|
|
3775
|
-
}
|
|
3776
|
-
const wrapped = cleaned.flatMap((l) => wrapLine(l, columns() - GUIDE));
|
|
3777
|
-
return capture((output) => log.message(wrapped, { output, spacing }));
|
|
3778
|
-
}
|
|
3779
|
-
function title2(text, meta) {
|
|
3780
|
-
if (!colored2())
|
|
3781
|
-
return plainLine(text);
|
|
3782
|
-
const t2 = cut(oneLine(text), columns() - GUIDE - 2);
|
|
3783
|
-
const room = columns() - GUIDE - dist_default2(t2) - 2;
|
|
3784
|
-
const extra = meta && room > 8 ? ` ${cut(oneLine(meta), room - 2)}` : "";
|
|
3785
|
-
return capture((output) => intro(`${styleText3(["inverse", "bold"], ` ${t2} `)}${extra ? dim(extra) : ""}`, { output }));
|
|
3786
|
-
}
|
|
3787
|
-
function section(text, gap = false) {
|
|
3788
|
-
const t2 = oneLine(text);
|
|
3789
|
-
if (!colored2())
|
|
3790
|
-
return `${gap ? `
|
|
3791
|
-
` : ""} ${t2}`;
|
|
3792
|
-
return capture((output) => log.step(bold(t2), { output }));
|
|
3793
|
-
}
|
|
3794
|
-
function indent(text) {
|
|
3795
|
-
return content(text.split(`
|
|
3796
|
-
`));
|
|
3797
|
-
}
|
|
3798
|
-
function closing(text) {
|
|
3799
|
-
if (!colored2())
|
|
3800
|
-
return plainLine(text);
|
|
3801
|
-
const t2 = oneLine(text);
|
|
3802
|
-
const lines = wrapAnsi(t2, columns() - GUIDE, { hard: true, trim: true }).split(`
|
|
3803
|
-
`);
|
|
3804
|
-
return capture((output) => outro(bold(lines.join(`
|
|
3805
|
-
${" ".repeat(GUIDE)}`)), { output }));
|
|
3806
|
-
}
|
|
3807
|
-
function failure(head2, reason2) {
|
|
3808
|
-
return [title2(head2), stopped(reason2)].join(`
|
|
3809
|
-
`);
|
|
3810
|
-
}
|
|
3811
|
-
function stopped(reason2) {
|
|
3812
|
-
if (!colored2())
|
|
3813
|
-
return [indent(reason2), closing(`${mark("fail")} Stopped`)].join(`
|
|
3814
|
-
`);
|
|
3815
|
-
const lines = clean2(reason2).split(`
|
|
3816
|
-
`);
|
|
3817
|
-
const [first = "", ...rest] = lines.flatMap((l) => wrapLine(l, columns() - GUIDE));
|
|
3818
|
-
return capture((output) => {
|
|
3819
|
-
log.error(first, { output });
|
|
3820
|
-
if (rest.length)
|
|
3821
|
-
log.message(rest, { output, spacing: 0 });
|
|
3822
|
-
cancel("Stopped", { output });
|
|
3823
|
-
}).replace(/\n+$/, "");
|
|
3824
|
-
}
|
|
3825
|
-
var cut = (text, room) => dist_default2(text) <= room ? text : `${wrapAnsi(text, Math.max(1, room - 1), { hard: true }).split(`
|
|
3826
|
-
`)[0]}…`;
|
|
3827
|
-
function steps(heading, items, note) {
|
|
3828
|
-
const room = columns() - 11;
|
|
3829
|
-
if (!colored2() || items.some((x) => dist_default2(x.command) > room))
|
|
3830
|
-
return [
|
|
3831
|
-
` ${oneLine(heading)}:`,
|
|
3832
|
-
...items.map((x) => ` ${x.who}: ${x.command}${x.after ? `, then ${x.after}` : ""}`),
|
|
3833
|
-
` ${oneLine(note)}`
|
|
3834
|
-
].join(`
|
|
3835
|
-
`);
|
|
3836
|
-
const body = items.flatMap((x) => [bold(x.who), ` ${x.command}`, ...x.after ? [dim(` Then: ${x.after}`)] : [], ""]).concat(dim(oneLine(note))).join(`
|
|
3837
|
-
`);
|
|
3838
|
-
return capture((output) => box(body, oneLine(heading), { output, rounded: true, width: "auto" }));
|
|
3839
|
-
}
|
|
3729
|
+
// server/src/capture.ts
|
|
3730
|
+
import { spawn } from "node:child_process";
|
|
3731
|
+
import fs7 from "node:fs";
|
|
3732
|
+
import path8 from "node:path";
|
|
3840
3733
|
|
|
3841
3734
|
// server/node_modules/kysely/dist/util/object-utils.js
|
|
3842
3735
|
function isUndefined(obj) {
|
|
@@ -3961,10 +3854,10 @@ var CreateIndexNode = freeze({
|
|
|
3961
3854
|
...props
|
|
3962
3855
|
});
|
|
3963
3856
|
},
|
|
3964
|
-
cloneWithColumns(node,
|
|
3857
|
+
cloneWithColumns(node, columns) {
|
|
3965
3858
|
return freeze({
|
|
3966
3859
|
...node,
|
|
3967
|
-
columns: [...node.columns || [], ...
|
|
3860
|
+
columns: [...node.columns || [], ...columns]
|
|
3968
3861
|
});
|
|
3969
3862
|
}
|
|
3970
3863
|
});
|
|
@@ -5615,33 +5508,33 @@ function parseInsertExpression(arg) {
|
|
|
5615
5508
|
return parseInsertColumnsAndValues(list);
|
|
5616
5509
|
}
|
|
5617
5510
|
function parseInsertColumnsAndValues(rows) {
|
|
5618
|
-
const
|
|
5511
|
+
const columns = parseColumnNamesAndIndexes(rows);
|
|
5619
5512
|
return [
|
|
5620
|
-
freeze([...
|
|
5621
|
-
ValuesNode.create(rows.map((row) => parseRowValues(row,
|
|
5513
|
+
freeze([...columns.keys()].map(ColumnNode.create)),
|
|
5514
|
+
ValuesNode.create(rows.map((row) => parseRowValues(row, columns)))
|
|
5622
5515
|
];
|
|
5623
5516
|
}
|
|
5624
5517
|
function parseColumnNamesAndIndexes(rows) {
|
|
5625
|
-
const
|
|
5518
|
+
const columns = new Map;
|
|
5626
5519
|
for (const row of rows) {
|
|
5627
5520
|
const cols = Object.keys(row);
|
|
5628
5521
|
for (const col of cols) {
|
|
5629
|
-
if (!
|
|
5630
|
-
|
|
5522
|
+
if (!columns.has(col) && row[col] !== undefined) {
|
|
5523
|
+
columns.set(col, columns.size);
|
|
5631
5524
|
}
|
|
5632
5525
|
}
|
|
5633
5526
|
}
|
|
5634
|
-
return
|
|
5527
|
+
return columns;
|
|
5635
5528
|
}
|
|
5636
|
-
function parseRowValues(row,
|
|
5529
|
+
function parseRowValues(row, columns) {
|
|
5637
5530
|
const rowColumns = Object.keys(row);
|
|
5638
5531
|
const rowValues = Array.from({
|
|
5639
|
-
length:
|
|
5532
|
+
length: columns.size
|
|
5640
5533
|
});
|
|
5641
5534
|
let hasUndefinedOrComplexColumns = false;
|
|
5642
5535
|
let indexedRowColumns = rowColumns.length;
|
|
5643
5536
|
for (const col of rowColumns) {
|
|
5644
|
-
const columnIdx =
|
|
5537
|
+
const columnIdx = columns.get(col);
|
|
5645
5538
|
if (isUndefined(columnIdx)) {
|
|
5646
5539
|
indexedRowColumns--;
|
|
5647
5540
|
continue;
|
|
@@ -5652,7 +5545,7 @@ function parseRowValues(row, columns2) {
|
|
|
5652
5545
|
}
|
|
5653
5546
|
rowValues[columnIdx] = value;
|
|
5654
5547
|
}
|
|
5655
|
-
const hasMissingColumns = indexedRowColumns <
|
|
5548
|
+
const hasMissingColumns = indexedRowColumns < columns.size;
|
|
5656
5549
|
if (hasMissingColumns || hasUndefinedOrComplexColumns) {
|
|
5657
5550
|
const defaultValue = DefaultInsertValueNode.create();
|
|
5658
5551
|
return ValueListNode.create(rowValues.map((it) => isUndefined(it) ? defaultValue : parseValueExpression(it)));
|
|
@@ -5794,8 +5687,8 @@ class OnConflictBuilder {
|
|
|
5794
5687
|
})
|
|
5795
5688
|
});
|
|
5796
5689
|
}
|
|
5797
|
-
columns(
|
|
5798
|
-
const columnNodes =
|
|
5690
|
+
columns(columns) {
|
|
5691
|
+
const columnNodes = columns.map(ColumnNode.create);
|
|
5799
5692
|
return new OnConflictBuilder({
|
|
5800
5693
|
...this.#props,
|
|
5801
5694
|
onConflictNode: OnConflictNode.cloneWith(this.#props.onConflictNode, {
|
|
@@ -5947,20 +5840,20 @@ class InsertQueryBuilder {
|
|
|
5947
5840
|
this.#props = freeze(props);
|
|
5948
5841
|
}
|
|
5949
5842
|
values(insert) {
|
|
5950
|
-
const [
|
|
5843
|
+
const [columns, values] = parseInsertExpression(insert);
|
|
5951
5844
|
return new InsertQueryBuilder({
|
|
5952
5845
|
...this.#props,
|
|
5953
5846
|
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
|
|
5954
|
-
columns
|
|
5847
|
+
columns,
|
|
5955
5848
|
values
|
|
5956
5849
|
})
|
|
5957
5850
|
});
|
|
5958
5851
|
}
|
|
5959
|
-
columns(
|
|
5852
|
+
columns(columns) {
|
|
5960
5853
|
return new InsertQueryBuilder({
|
|
5961
5854
|
...this.#props,
|
|
5962
5855
|
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
|
|
5963
|
-
columns: freeze(
|
|
5856
|
+
columns: freeze(columns.map(ColumnNode.create))
|
|
5964
5857
|
})
|
|
5965
5858
|
});
|
|
5966
5859
|
}
|
|
@@ -6680,8 +6573,8 @@ function parseCommonTableExpressionName(name) {
|
|
|
6680
6573
|
if (name.includes("(")) {
|
|
6681
6574
|
const parts = name.split(/[\(\)]/);
|
|
6682
6575
|
const table = parts[0];
|
|
6683
|
-
const
|
|
6684
|
-
return CommonTableExpressionNameNode.create(table,
|
|
6576
|
+
const columns = parts[1].split(",").map((it) => it.trim());
|
|
6577
|
+
return CommonTableExpressionNameNode.create(table, columns);
|
|
6685
6578
|
} else {
|
|
6686
6579
|
return CommonTableExpressionNameNode.create(name);
|
|
6687
6580
|
}
|
|
@@ -8467,11 +8360,11 @@ class NotMatchedThenableMergeQueryBuilder {
|
|
|
8467
8360
|
});
|
|
8468
8361
|
}
|
|
8469
8362
|
thenInsertValues(insert) {
|
|
8470
|
-
const [
|
|
8363
|
+
const [columns, values] = parseInsertExpression(insert);
|
|
8471
8364
|
return new WheneableMergeQueryBuilder({
|
|
8472
8365
|
...this.#props,
|
|
8473
8366
|
queryNode: MergeQueryNode.cloneWithThen(this.#props.queryNode, parseMergeThen(InsertQueryNode.cloneWith(InsertQueryNode.createWithoutInto(), {
|
|
8474
|
-
columns
|
|
8367
|
+
columns,
|
|
8475
8368
|
values
|
|
8476
8369
|
})))
|
|
8477
8370
|
});
|
|
@@ -9984,11 +9877,11 @@ var ReferencesNode = freeze({
|
|
|
9984
9877
|
is(node) {
|
|
9985
9878
|
return node.kind === "ReferencesNode";
|
|
9986
9879
|
},
|
|
9987
|
-
create(table,
|
|
9880
|
+
create(table, columns) {
|
|
9988
9881
|
return freeze({
|
|
9989
9882
|
kind: "ReferencesNode",
|
|
9990
9883
|
table,
|
|
9991
|
-
columns: freeze([...
|
|
9884
|
+
columns: freeze([...columns])
|
|
9992
9885
|
});
|
|
9993
9886
|
},
|
|
9994
9887
|
cloneWithOnDelete(references, onDelete) {
|
|
@@ -10258,14 +10151,14 @@ var UniqueConstraintNode = freeze({
|
|
|
10258
10151
|
is(node) {
|
|
10259
10152
|
return node.kind === "UniqueConstraintNode";
|
|
10260
10153
|
},
|
|
10261
|
-
create(
|
|
10262
|
-
if (isString(
|
|
10154
|
+
create(columns, constraintName, nullsNotDistinct) {
|
|
10155
|
+
if (isString(columns.at(0))) {
|
|
10263
10156
|
logOnce("`UniqueConstraintNode.create(columns: string[], ...)` is deprecated - pass `ColumnNode[]` instead.");
|
|
10264
|
-
|
|
10157
|
+
columns = columns.map(ColumnNode.create);
|
|
10265
10158
|
}
|
|
10266
10159
|
return freeze({
|
|
10267
10160
|
kind: "UniqueConstraintNode",
|
|
10268
|
-
columns: freeze(
|
|
10161
|
+
columns: freeze(columns),
|
|
10269
10162
|
name: constraintName ? IdentifierNode.create(constraintName) : undefined,
|
|
10270
10163
|
nullsNotDistinct
|
|
10271
10164
|
});
|
|
@@ -10474,10 +10367,10 @@ var PrimaryKeyConstraintNode = freeze({
|
|
|
10474
10367
|
is(node) {
|
|
10475
10368
|
return node.kind === "PrimaryKeyConstraintNode";
|
|
10476
10369
|
},
|
|
10477
|
-
create(
|
|
10370
|
+
create(columns, constraintName) {
|
|
10478
10371
|
return freeze({
|
|
10479
10372
|
kind: "PrimaryKeyConstraintNode",
|
|
10480
|
-
columns: freeze(
|
|
10373
|
+
columns: freeze(columns.map(ColumnNode.create)),
|
|
10481
10374
|
name: constraintName ? IdentifierNode.create(constraintName) : undefined
|
|
10482
10375
|
});
|
|
10483
10376
|
},
|
|
@@ -10503,10 +10396,10 @@ var AddIndexNode = freeze({
|
|
|
10503
10396
|
...props
|
|
10504
10397
|
});
|
|
10505
10398
|
},
|
|
10506
|
-
cloneWithColumns(node,
|
|
10399
|
+
cloneWithColumns(node, columns) {
|
|
10507
10400
|
return freeze({
|
|
10508
10401
|
...node,
|
|
10509
|
-
columns: [...node.columns || [], ...
|
|
10402
|
+
columns: [...node.columns || [], ...columns]
|
|
10510
10403
|
});
|
|
10511
10404
|
}
|
|
10512
10405
|
});
|
|
@@ -10537,11 +10430,11 @@ class AlterTableAddIndexBuilder {
|
|
|
10537
10430
|
})
|
|
10538
10431
|
});
|
|
10539
10432
|
}
|
|
10540
|
-
columns(
|
|
10433
|
+
columns(columns) {
|
|
10541
10434
|
return new AlterTableAddIndexBuilder({
|
|
10542
10435
|
...this.#props,
|
|
10543
10436
|
node: AlterTableNode.cloneWithTableProps(this.#props.node, {
|
|
10544
|
-
addIndex: AddIndexNode.cloneWithColumns(this.#props.node.addIndex,
|
|
10437
|
+
addIndex: AddIndexNode.cloneWithColumns(this.#props.node.addIndex, columns.map((item) => isString(item) ? parseOrderedColumnName(item) : item.toOperationNode()))
|
|
10545
10438
|
})
|
|
10546
10439
|
});
|
|
10547
10440
|
}
|
|
@@ -10743,8 +10636,8 @@ class AlterTableBuilder {
|
|
|
10743
10636
|
node: AlterTableNode.cloneWithColumnAlteration(this.#props.node, ModifyColumnNode.create(builder.toOperationNode()))
|
|
10744
10637
|
});
|
|
10745
10638
|
}
|
|
10746
|
-
addUniqueConstraint(constraintName,
|
|
10747
|
-
const uniqueConstraintBuilder = build(new UniqueConstraintNodeBuilder(UniqueConstraintNode.create(
|
|
10639
|
+
addUniqueConstraint(constraintName, columns, build = noop) {
|
|
10640
|
+
const uniqueConstraintBuilder = build(new UniqueConstraintNodeBuilder(UniqueConstraintNode.create(columns.map((column) => isString(column) ? ColumnNode.create(column) : parseExpression(column)), constraintName)));
|
|
10748
10641
|
return new AlterTableExecutor({
|
|
10749
10642
|
...this.#props,
|
|
10750
10643
|
node: AlterTableNode.cloneWithTableProps(this.#props.node, {
|
|
@@ -10761,15 +10654,15 @@ class AlterTableBuilder {
|
|
|
10761
10654
|
})
|
|
10762
10655
|
});
|
|
10763
10656
|
}
|
|
10764
|
-
addForeignKeyConstraint(constraintName,
|
|
10765
|
-
const constraintBuilder = build(new ForeignKeyConstraintBuilder(ForeignKeyConstraintNode.create(
|
|
10657
|
+
addForeignKeyConstraint(constraintName, columns, targetTable, targetColumns, build = noop) {
|
|
10658
|
+
const constraintBuilder = build(new ForeignKeyConstraintBuilder(ForeignKeyConstraintNode.create(columns.map(ColumnNode.create), parseTable(targetTable), targetColumns.map(ColumnNode.create), constraintName)));
|
|
10766
10659
|
return new AlterTableAddForeignKeyConstraintBuilder({
|
|
10767
10660
|
...this.#props,
|
|
10768
10661
|
constraintBuilder
|
|
10769
10662
|
});
|
|
10770
10663
|
}
|
|
10771
|
-
addPrimaryKeyConstraint(constraintName,
|
|
10772
|
-
const constraintBuilder = build(new PrimaryKeyConstraintBuilder(PrimaryKeyConstraintNode.create(
|
|
10664
|
+
addPrimaryKeyConstraint(constraintName, columns, build = noop) {
|
|
10665
|
+
const constraintBuilder = build(new PrimaryKeyConstraintBuilder(PrimaryKeyConstraintNode.create(columns, constraintName)));
|
|
10773
10666
|
return new AlterTableExecutor({
|
|
10774
10667
|
...this.#props,
|
|
10775
10668
|
node: AlterTableNode.cloneWithTableProps(this.#props.node, {
|
|
@@ -10920,10 +10813,10 @@ class CreateIndexBuilder {
|
|
|
10920
10813
|
])
|
|
10921
10814
|
});
|
|
10922
10815
|
}
|
|
10923
|
-
columns(
|
|
10816
|
+
columns(columns) {
|
|
10924
10817
|
return new CreateIndexBuilder({
|
|
10925
10818
|
...this.#props,
|
|
10926
|
-
node: CreateIndexNode.cloneWithColumns(this.#props.node,
|
|
10819
|
+
node: CreateIndexNode.cloneWithColumns(this.#props.node, columns.map((item) => isString(item) ? parseOrderedColumnName(item) : item.toOperationNode()))
|
|
10927
10820
|
});
|
|
10928
10821
|
}
|
|
10929
10822
|
expression(expression) {
|
|
@@ -11053,22 +10946,22 @@ class CreateTableBuilder {
|
|
|
11053
10946
|
node: CreateTableNode.cloneWithColumn(this.#props.node, columnBuilder.toOperationNode())
|
|
11054
10947
|
});
|
|
11055
10948
|
}
|
|
11056
|
-
addPrimaryKeyConstraint(constraintName,
|
|
11057
|
-
const constraintBuilder = build(new PrimaryKeyConstraintBuilder(PrimaryKeyConstraintNode.create(
|
|
10949
|
+
addPrimaryKeyConstraint(constraintName, columns, build = noop) {
|
|
10950
|
+
const constraintBuilder = build(new PrimaryKeyConstraintBuilder(PrimaryKeyConstraintNode.create(columns, constraintName)));
|
|
11058
10951
|
return new CreateTableBuilder({
|
|
11059
10952
|
...this.#props,
|
|
11060
10953
|
node: CreateTableNode.cloneWithConstraint(this.#props.node, constraintBuilder.toOperationNode())
|
|
11061
10954
|
});
|
|
11062
10955
|
}
|
|
11063
|
-
addUniqueConstraint(constraintName,
|
|
11064
|
-
const uniqueConstraintBuilder = build(new UniqueConstraintNodeBuilder(UniqueConstraintNode.create(
|
|
10956
|
+
addUniqueConstraint(constraintName, columns, build = noop) {
|
|
10957
|
+
const uniqueConstraintBuilder = build(new UniqueConstraintNodeBuilder(UniqueConstraintNode.create(columns.map((column) => isString(column) ? ColumnNode.create(column) : parseExpression(column)), constraintName)));
|
|
11065
10958
|
return new CreateTableBuilder({
|
|
11066
10959
|
...this.#props,
|
|
11067
10960
|
node: CreateTableNode.cloneWithConstraint(this.#props.node, uniqueConstraintBuilder.toOperationNode())
|
|
11068
10961
|
});
|
|
11069
10962
|
}
|
|
11070
|
-
addIndex(indexName,
|
|
11071
|
-
const addIndexBuilder = build(new CreateTableAddIndexBuilder(AddIndexNode.cloneWithColumns(AddIndexNode.create(indexName),
|
|
10963
|
+
addIndex(indexName, columns, build = noop) {
|
|
10964
|
+
const addIndexBuilder = build(new CreateTableAddIndexBuilder(AddIndexNode.cloneWithColumns(AddIndexNode.create(indexName), columns.map((column) => isString(column) ? ColumnNode.create(column) : parseExpression(column)))));
|
|
11072
10965
|
return new CreateTableBuilder({
|
|
11073
10966
|
...this.#props,
|
|
11074
10967
|
node: CreateTableNode.cloneWithIndex(this.#props.node, addIndexBuilder.toOperationNode())
|
|
@@ -11081,8 +10974,8 @@ class CreateTableBuilder {
|
|
|
11081
10974
|
node: CreateTableNode.cloneWithConstraint(this.#props.node, constraintBuilder.toOperationNode())
|
|
11082
10975
|
});
|
|
11083
10976
|
}
|
|
11084
|
-
addForeignKeyConstraint(constraintName,
|
|
11085
|
-
const builder = build(new ForeignKeyConstraintBuilder(ForeignKeyConstraintNode.create(
|
|
10977
|
+
addForeignKeyConstraint(constraintName, columns, targetTable, targetColumns, build = noop) {
|
|
10978
|
+
const builder = build(new ForeignKeyConstraintBuilder(ForeignKeyConstraintNode.create(columns.map(ColumnNode.create), parseTable(targetTable), targetColumns.map(ColumnNode.create), constraintName)));
|
|
11086
10979
|
return new CreateTableBuilder({
|
|
11087
10980
|
...this.#props,
|
|
11088
10981
|
node: CreateTableNode.cloneWithConstraint(this.#props.node, builder.toOperationNode())
|
|
@@ -11314,11 +11207,11 @@ class CreateViewBuilder {
|
|
|
11314
11207
|
})
|
|
11315
11208
|
});
|
|
11316
11209
|
}
|
|
11317
|
-
columns(
|
|
11210
|
+
columns(columns) {
|
|
11318
11211
|
return new CreateViewBuilder({
|
|
11319
11212
|
...this.#props,
|
|
11320
11213
|
node: CreateViewNode.cloneWith(this.#props.node, {
|
|
11321
|
-
columns:
|
|
11214
|
+
columns: columns.map(parseColumnName)
|
|
11322
11215
|
})
|
|
11323
11216
|
});
|
|
11324
11217
|
}
|
|
@@ -14469,9 +14362,9 @@ class SqliteIntrospector {
|
|
|
14469
14362
|
}
|
|
14470
14363
|
return tablesResult.map(({ name, sql: sql2, type }) => {
|
|
14471
14364
|
let autoIncrementCol = sql2?.split(/[\(\),]/)?.find((it) => it.toLowerCase().includes("autoincrement"))?.trimStart()?.split(/\s+/)?.[0]?.replace(/["`]/g, "");
|
|
14472
|
-
const
|
|
14365
|
+
const columns = columnsByTable[name] ?? [];
|
|
14473
14366
|
if (!autoIncrementCol) {
|
|
14474
|
-
const pkCols =
|
|
14367
|
+
const pkCols = columns.filter((r2) => r2.pk > 0);
|
|
14475
14368
|
if (pkCols.length === 1 && pkCols[0].type.toLowerCase() === "integer") {
|
|
14476
14369
|
autoIncrementCol = pkCols[0].name;
|
|
14477
14370
|
}
|
|
@@ -14480,7 +14373,7 @@ class SqliteIntrospector {
|
|
|
14480
14373
|
name,
|
|
14481
14374
|
isView: type === "view",
|
|
14482
14375
|
isForeign: false,
|
|
14483
|
-
columns:
|
|
14376
|
+
columns: columns.map((col) => ({
|
|
14484
14377
|
name: col.name,
|
|
14485
14378
|
dataType: col.type,
|
|
14486
14379
|
isNullable: !col.notnull,
|
|
@@ -14921,364 +14814,104 @@ function openWriter(role, file = dbFile(), busyMs) {
|
|
|
14921
14814
|
return kyselyOn(() => connectWriter(role, file, false, busyMs));
|
|
14922
14815
|
}
|
|
14923
14816
|
|
|
14924
|
-
// server/src/
|
|
14925
|
-
var
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
look.close();
|
|
14949
|
-
}
|
|
14950
|
-
fs5.renameSync(partial, done);
|
|
14951
|
-
} catch (e) {
|
|
14952
|
-
for (const f of [partial, `${partial}-wal`, `${partial}-shm`])
|
|
14953
|
-
fs5.rmSync(f, { force: true });
|
|
14954
|
-
throw new Error(`Could not back up ${file} before migrating it, so nothing was migrated (${e.message}). Check free space in ${backupDir(file)}.`);
|
|
14955
|
-
}
|
|
14956
|
-
return done;
|
|
14957
|
-
}
|
|
14958
|
-
function prune(file, kept) {
|
|
14959
|
-
let all;
|
|
14817
|
+
// server/src/knowledge.ts
|
|
14818
|
+
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
14819
|
+
|
|
14820
|
+
// server/src/project.ts
|
|
14821
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14822
|
+
import fs5 from "node:fs";
|
|
14823
|
+
import os3 from "node:os";
|
|
14824
|
+
import path6 from "node:path";
|
|
14825
|
+
|
|
14826
|
+
// server/src/anchors.ts
|
|
14827
|
+
import path5 from "node:path";
|
|
14828
|
+
var MAX_BYTES = 2 * 1024 * 1024;
|
|
14829
|
+
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path5.sep}`) || path5.isAbsolute(rel);
|
|
14830
|
+
|
|
14831
|
+
// server/src/project.ts
|
|
14832
|
+
var localFile = () => path6.join(sphicaHome(), "projects.json");
|
|
14833
|
+
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
14834
|
+
function legacyRemote(url) {
|
|
14835
|
+
const raw = String(url ?? "").trim();
|
|
14836
|
+
if (!raw)
|
|
14837
|
+
return null;
|
|
14838
|
+
const scp = raw.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+?)(?:\.git)?$/);
|
|
14839
|
+
if (scp)
|
|
14840
|
+
return `${scp[1]}/${scp[2]}`;
|
|
14960
14841
|
try {
|
|
14961
|
-
|
|
14842
|
+
const u3 = new URL(raw);
|
|
14843
|
+
if (!u3.hostname)
|
|
14844
|
+
return null;
|
|
14845
|
+
const p = u3.pathname.replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
14846
|
+
return p ? `${u3.hostname}/${p}` : u3.hostname;
|
|
14962
14847
|
} catch {
|
|
14963
|
-
return;
|
|
14964
|
-
}
|
|
14965
|
-
for (const old of all.filter((b) => b !== kept).slice(KEEP - 1))
|
|
14966
|
-
try {
|
|
14967
|
-
fs5.rmSync(old, { force: true });
|
|
14968
|
-
} catch {}
|
|
14969
|
-
}
|
|
14970
|
-
var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
|
|
14971
|
-
var SQLITE_BUSY = 5;
|
|
14972
|
-
function taken(raw, table) {
|
|
14973
|
-
if (!raw.prepare("select 1 from temp.sqlite_schema where type = 'table' and name = ?").get(table))
|
|
14974
|
-
return [];
|
|
14975
|
-
const rows = raw.prepare(`select * from temp.${table} order by rule, rowid`).all();
|
|
14976
|
-
raw.exec(`drop table temp.${table}`);
|
|
14977
|
-
return rows;
|
|
14978
|
-
}
|
|
14979
|
-
function listed(rows) {
|
|
14980
|
-
const rules = new Map;
|
|
14981
|
-
for (const r2 of rows) {
|
|
14982
|
-
const of = rules.get(r2.rule);
|
|
14983
|
-
if (of)
|
|
14984
|
-
of.push(r2);
|
|
14985
|
-
else
|
|
14986
|
-
rules.set(r2.rule, [r2]);
|
|
14848
|
+
return null;
|
|
14987
14849
|
}
|
|
14988
|
-
return [...rules].flatMap(([rule2, of]) => [
|
|
14989
|
-
`${inline(String(rule2))}: ${plural(of.length, "row")}`,
|
|
14990
|
-
...of.map((r2) => ` ${inline(String(r2.item))}${r2.action ? ` → ${inline(String(r2.action))}` : ""}`)
|
|
14991
|
-
]).join(`
|
|
14992
|
-
`);
|
|
14993
14850
|
}
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
14851
|
+
var fold = (s) => s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
|
|
14852
|
+
function normalizeKey(key) {
|
|
14853
|
+
if (!key.startsWith("git:"))
|
|
14854
|
+
return key;
|
|
14855
|
+
const rest = key.slice(4);
|
|
14856
|
+
const cut = rest.indexOf("/");
|
|
14857
|
+
const host = fold(cut < 0 ? rest : rest.slice(0, cut));
|
|
14858
|
+
return host === "github.com" ? fold(key) : `git:${host}${cut < 0 ? "" : rest.slice(cut)}`;
|
|
15001
14859
|
}
|
|
15002
|
-
|
|
15003
|
-
if (raw.isTransaction)
|
|
15004
|
-
return null;
|
|
14860
|
+
var git = (dir, ...args) => {
|
|
15005
14861
|
try {
|
|
15006
|
-
return
|
|
14862
|
+
return execFileSync2("git", ["-C", dir, ...args], {
|
|
14863
|
+
encoding: "utf8",
|
|
14864
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
14865
|
+
timeout: 5000
|
|
14866
|
+
}).trim();
|
|
15007
14867
|
} catch {
|
|
15008
14868
|
return null;
|
|
15009
14869
|
}
|
|
15010
|
-
}
|
|
15011
|
-
function
|
|
15012
|
-
raw
|
|
14870
|
+
};
|
|
14871
|
+
function localMap() {
|
|
14872
|
+
let raw;
|
|
15013
14873
|
try {
|
|
15014
|
-
|
|
15015
|
-
raw.exec("commit");
|
|
15016
|
-
return out;
|
|
14874
|
+
raw = fs5.readFileSync(localFile(), "utf8");
|
|
15017
14875
|
} catch (e) {
|
|
15018
|
-
|
|
14876
|
+
if (e.code === "ENOENT")
|
|
14877
|
+
return {};
|
|
15019
14878
|
throw e;
|
|
15020
14879
|
}
|
|
15021
|
-
|
|
15022
|
-
function optimize(raw) {
|
|
14880
|
+
let m;
|
|
15023
14881
|
try {
|
|
15024
|
-
|
|
15025
|
-
} catch {
|
|
14882
|
+
m = JSON.parse(raw);
|
|
14883
|
+
} catch {
|
|
14884
|
+
m = null;
|
|
14885
|
+
}
|
|
14886
|
+
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !path6.isAbsolute(root) || typeof name !== "string" || !LOCAL_KEY.test(name)))
|
|
14887
|
+
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
14888
|
+
return m;
|
|
15026
14889
|
}
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
}
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
const
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
if (!fs5.existsSync(script))
|
|
15046
|
-
throw new Error(`This Sphica has no migration script for revision ${r2} (${script} is missing). Reinstall sphica (\`npm i -g sphica@${packageVersionAt(ROOT) ?? "latest"}\`), then run \`sphica init\` again.`);
|
|
15047
|
-
raw.exec("pragma foreign_keys = off");
|
|
15048
|
-
try {
|
|
15049
|
-
const notes = immediate(raw, () => {
|
|
15050
|
-
if (versionOf(raw) !== r2 - 1)
|
|
15051
|
-
return [];
|
|
15052
|
-
if (fs5.existsSync(CHECK(dir, r2))) {
|
|
15053
|
-
raw.exec(fs5.readFileSync(CHECK(dir, r2), "utf8"));
|
|
15054
|
-
const stops = taken(raw, STOP2);
|
|
15055
|
-
if (stops.length)
|
|
15056
|
-
throw new Stop(r2, stops);
|
|
15057
|
-
}
|
|
15058
|
-
raw.exec(fs5.readFileSync(script, "utf8"));
|
|
15059
|
-
const broken = raw.prepare("pragma foreign_key_check").all().length;
|
|
15060
|
-
if (broken)
|
|
15061
|
-
throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);
|
|
15062
|
-
if (versionOf(raw) !== r2)
|
|
15063
|
-
throw new Error(`${path5.basename(script)} did not set revision ${r2}`);
|
|
15064
|
-
return taken(raw, NOTE);
|
|
15065
|
-
});
|
|
15066
|
-
if (notes.length)
|
|
15067
|
-
say(`Changed while migrating to revision ${r2}: ${plural(notes.length, "row")} (the backup keeps them as they were)
|
|
15068
|
-
${listed(notes)}`);
|
|
15069
|
-
} finally {
|
|
15070
|
-
raw.exec("pragma foreign_keys = on");
|
|
15071
|
-
}
|
|
15072
|
-
}
|
|
15073
|
-
} catch (e) {
|
|
15074
|
-
const said = e.message.replace(/\.$/, "");
|
|
15075
|
-
const list = e instanceof Stop ? `
|
|
15076
|
-
${e.list}` : "";
|
|
15077
|
-
const now = committed(raw);
|
|
15078
|
-
if (now === from) {
|
|
15079
|
-
let left = "the backup made for this run was removed";
|
|
15080
|
-
try {
|
|
15081
|
-
fs5.rmSync(backup, { force: true });
|
|
15082
|
-
} catch {
|
|
15083
|
-
left = `the backup made for this run is still at ${backup}; delete it yourself`;
|
|
15084
|
-
}
|
|
15085
|
-
const retry = sqliteCode(e) === SQLITE_BUSY ? " Another process is writing to the database. Run `sphica init` again when it has finished." : "";
|
|
15086
|
-
throw new Error(`${said}. No migration step was committed: the database is still at revision ${from}, and there is no need to replace it with a backup (${left}).${retry}${list}`);
|
|
15087
|
-
}
|
|
15088
|
-
const base = path5.basename(file);
|
|
15089
|
-
const state = now === null ? "The committed revision could not be confirmed after the failure" : `The database is now at revision ${now} (this run, or another \`sphica init\` running at the same time, migrated it that far)`;
|
|
15090
|
-
throw new Error(`${said}. ${state}. The database before migrating is at ${backup} (anything recorded after it was made is not in it). To go back to it, close every session using Sphica, move ${base}, ${base}-wal, and ${base}-shm in ${path5.dirname(file)} aside, then copy the backup to ${file}.${list}`);
|
|
15091
|
-
}
|
|
15092
|
-
optimize(raw);
|
|
15093
|
-
prune(file, backup);
|
|
15094
|
-
return from;
|
|
15095
|
-
});
|
|
15096
|
-
}
|
|
15097
|
-
function dbInit(file = dbFile()) {
|
|
15098
|
-
if (fs5.existsSync(file)) {
|
|
15099
|
-
const look = new DatabaseSync3(file, { readOnly: true });
|
|
15100
|
-
let generation;
|
|
15101
|
-
try {
|
|
15102
|
-
generation = generationOf(look);
|
|
15103
|
-
} finally {
|
|
15104
|
-
look.close();
|
|
15105
|
-
}
|
|
15106
|
-
if (generation === null)
|
|
15107
|
-
throw new Error(`${file} is not a Sphica database (no schema). Move it to another name, then run this again.`);
|
|
15108
|
-
const got = withOwner(file, versionOf);
|
|
15109
|
-
if (got === SCHEMA_REVISION)
|
|
15110
|
-
say(`Already exists: ${file} (revision ${got})`);
|
|
15111
|
-
else if (got > SCHEMA_REVISION)
|
|
15112
|
-
say(`Already exists: ${file} (revision ${got}, made by a newer Sphica; this one expects ${SCHEMA_REVISION}. Update sphica.)`);
|
|
15113
|
-
else {
|
|
15114
|
-
migrate(file);
|
|
15115
|
-
say(`Migrated: ${file} (revision ${got} → ${SCHEMA_REVISION})`);
|
|
15116
|
-
}
|
|
15117
|
-
return;
|
|
15118
|
-
}
|
|
15119
|
-
fs5.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
15120
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
15121
|
-
fs5.rmSync(tmp, { force: true });
|
|
15122
|
-
try {
|
|
15123
|
-
withOwner(tmp, (raw) => {
|
|
15124
|
-
raw.exec("pragma journal_mode = wal");
|
|
15125
|
-
raw.exec(fs5.readFileSync(SCHEMA(), "utf8"));
|
|
15126
|
-
if (versionOf(raw) !== SCHEMA_REVISION)
|
|
15127
|
-
throw new Error(`db/schema.sql has user_version ${versionOf(raw)}, but the code expects ${SCHEMA_REVISION}`);
|
|
15128
|
-
optimize(raw);
|
|
15129
|
-
}, true);
|
|
15130
|
-
try {
|
|
15131
|
-
fs5.linkSync(tmp, file);
|
|
15132
|
-
} catch (e) {
|
|
15133
|
-
if (e.code === "EEXIST" || fs5.existsSync(file))
|
|
15134
|
-
throw new Error(`${file} already exists (another sphica init created it first). Run this again to check it.`);
|
|
15135
|
-
fs5.renameSync(tmp, file);
|
|
15136
|
-
}
|
|
15137
|
-
} finally {
|
|
15138
|
-
for (const f of [tmp, `${tmp}-wal`, `${tmp}-shm`])
|
|
15139
|
-
fs5.rmSync(f, { force: true });
|
|
15140
|
-
}
|
|
15141
|
-
say(`Created: ${file} (revision ${SCHEMA_REVISION})`);
|
|
15142
|
-
}
|
|
15143
|
-
function bindOwner(user, file = dbFile()) {
|
|
15144
|
-
return withOwner(file, (raw) => immediate(raw, () => {
|
|
15145
|
-
const revision = versionOf(raw);
|
|
15146
|
-
if (revision !== SCHEMA_REVISION)
|
|
15147
|
-
return { kind: "skipped", revision };
|
|
15148
|
-
const bound = raw.prepare("select external_id, login from owner_identity where provider = 'github' order by external_id = ? desc, bound_at, external_id limit 1").get(String(user.id));
|
|
15149
|
-
if (!bound) {
|
|
15150
|
-
raw.prepare("insert into owner_identity (provider, external_id, login, bound_at) values ('github', ?, ?, ?)").run(String(user.id), user.login, iso(Date.now()));
|
|
15151
|
-
return { kind: "bound" };
|
|
15152
|
-
}
|
|
15153
|
-
return bound.external_id === String(user.id) ? { kind: "already" } : { kind: "other", id: bound.external_id, login: bound.login };
|
|
15154
|
-
}));
|
|
15155
|
-
}
|
|
15156
|
-
function reindex(file = dbFile()) {
|
|
15157
|
-
const counts = withOwner(file, (raw) => {
|
|
15158
|
-
const n2 = immediate(raw, () => {
|
|
15159
|
-
raw.exec("insert into unit_fts (unit_fts) values ('delete-all')");
|
|
15160
|
-
raw.exec("insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text");
|
|
15161
|
-
raw.exec("insert into source_fts (source_fts) values ('delete-all')");
|
|
15162
|
-
raw.exec("insert into source_fts (rowid, lexemes) select id, sphica_terms(text) from source where indexed = 1");
|
|
15163
|
-
const n3 = (sql2) => raw.prepare(sql2).get().n;
|
|
15164
|
-
return {
|
|
15165
|
-
units: n3("select count(*) as n from unit"),
|
|
15166
|
-
sources: n3("select count(*) as n from source where indexed = 1")
|
|
15167
|
-
};
|
|
15168
|
-
});
|
|
15169
|
-
optimize(raw);
|
|
15170
|
-
return n2;
|
|
15171
|
-
});
|
|
15172
|
-
say(`Rebuilt the index: ${plural(counts.units, "unit")}, ${plural(counts.sources, "source")}`);
|
|
15173
|
-
}
|
|
15174
|
-
function inspect(file = dbFile()) {
|
|
15175
|
-
const size = (f) => fs5.existsSync(f) ? fs5.statSync(f).size : 0;
|
|
15176
|
-
return withOwner(file, (raw) => {
|
|
15177
|
-
const check = (table) => {
|
|
15178
|
-
try {
|
|
15179
|
-
raw.exec(`insert into ${table} (${table}, rank) values ('integrity-check', 1)`);
|
|
15180
|
-
return null;
|
|
15181
|
-
} catch (e) {
|
|
15182
|
-
return e.message;
|
|
15183
|
-
}
|
|
15184
|
-
};
|
|
15185
|
-
return {
|
|
15186
|
-
revision: versionOf(raw),
|
|
15187
|
-
bytes: size(file) + size(`${file}-wal`),
|
|
15188
|
-
fts: { unit: check("unit_fts"), source: check("source_fts") }
|
|
15189
|
-
};
|
|
15190
|
-
});
|
|
15191
|
-
}
|
|
15192
|
-
|
|
15193
|
-
// server/src/anchors.ts
|
|
15194
|
-
import path6 from "node:path";
|
|
15195
|
-
var MAX_BYTES = 2 * 1024 * 1024;
|
|
15196
|
-
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path6.sep}`) || path6.isAbsolute(rel);
|
|
15197
|
-
|
|
15198
|
-
// server/src/capture.ts
|
|
15199
|
-
import { spawn } from "node:child_process";
|
|
15200
|
-
import fs8 from "node:fs";
|
|
15201
|
-
import path9 from "node:path";
|
|
15202
|
-
|
|
15203
|
-
// server/src/knowledge.ts
|
|
15204
|
-
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
15205
|
-
|
|
15206
|
-
// server/src/project.ts
|
|
15207
|
-
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
15208
|
-
import fs6 from "node:fs";
|
|
15209
|
-
import os3 from "node:os";
|
|
15210
|
-
import path7 from "node:path";
|
|
15211
|
-
var localFile = () => path7.join(sphicaHome(), "projects.json");
|
|
15212
|
-
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
15213
|
-
function normalizeRemote(url) {
|
|
15214
|
-
const raw = String(url ?? "").trim();
|
|
15215
|
-
if (!raw)
|
|
15216
|
-
return null;
|
|
15217
|
-
const scp = raw.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+?)(?:\.git)?$/);
|
|
15218
|
-
if (scp)
|
|
15219
|
-
return `${scp[1]}/${scp[2]}`;
|
|
15220
|
-
try {
|
|
15221
|
-
const u3 = new URL(raw);
|
|
15222
|
-
if (!u3.hostname)
|
|
15223
|
-
return null;
|
|
15224
|
-
const p = u3.pathname.replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
15225
|
-
return p ? `${u3.hostname}/${p}` : u3.hostname;
|
|
15226
|
-
} catch {
|
|
15227
|
-
return null;
|
|
15228
|
-
}
|
|
15229
|
-
}
|
|
15230
|
-
var git = (dir, ...args) => {
|
|
15231
|
-
try {
|
|
15232
|
-
return execFileSync2("git", ["-C", dir, ...args], {
|
|
15233
|
-
encoding: "utf8",
|
|
15234
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
15235
|
-
timeout: 5000
|
|
15236
|
-
}).trim();
|
|
15237
|
-
} catch {
|
|
15238
|
-
return null;
|
|
15239
|
-
}
|
|
15240
|
-
};
|
|
15241
|
-
function localMap() {
|
|
15242
|
-
let raw;
|
|
15243
|
-
try {
|
|
15244
|
-
raw = fs6.readFileSync(localFile(), "utf8");
|
|
15245
|
-
} catch (e) {
|
|
15246
|
-
if (e.code === "ENOENT")
|
|
15247
|
-
return {};
|
|
15248
|
-
throw e;
|
|
15249
|
-
}
|
|
15250
|
-
let m;
|
|
15251
|
-
try {
|
|
15252
|
-
m = JSON.parse(raw);
|
|
15253
|
-
} catch {
|
|
15254
|
-
m = null;
|
|
15255
|
-
}
|
|
15256
|
-
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !path7.isAbsolute(root) || typeof name !== "string" || !LOCAL_KEY.test(name)))
|
|
15257
|
-
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
15258
|
-
return m;
|
|
15259
|
-
}
|
|
15260
|
-
var rootOf = (dir) => git(path7.resolve(dir), "rev-parse", "--show-toplevel") || path7.resolve(dir);
|
|
15261
|
-
function identify(dir) {
|
|
15262
|
-
const given = path7.resolve(dir);
|
|
15263
|
-
const top = git(given, "rev-parse", "--show-toplevel");
|
|
15264
|
-
const root = top || given;
|
|
15265
|
-
const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
15266
|
-
if (remote)
|
|
15267
|
-
return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
15268
|
-
const map = localMap();
|
|
15269
|
-
for (let d = root;; d = path7.dirname(d)) {
|
|
15270
|
-
const local = map[d];
|
|
15271
|
-
if (local && LOCAL_KEY.test(local))
|
|
15272
|
-
return { key: `local:${local}`, root: d, name: local };
|
|
15273
|
-
if (top || path7.dirname(d) === d)
|
|
15274
|
-
return null;
|
|
14890
|
+
var rootOf = (dir) => git(path6.resolve(dir), "rev-parse", "--show-toplevel") || path6.resolve(dir);
|
|
14891
|
+
function identify(dir) {
|
|
14892
|
+
const given = path6.resolve(dir);
|
|
14893
|
+
const top = git(given, "rev-parse", "--show-toplevel");
|
|
14894
|
+
const root = top || given;
|
|
14895
|
+
const legacy = top ? legacyRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
14896
|
+
if (legacy) {
|
|
14897
|
+
const key = normalizeKey(`git:${legacy}`);
|
|
14898
|
+
const remote = key.slice(4);
|
|
14899
|
+
return { key, legacyKey: `git:${legacy}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
14900
|
+
}
|
|
14901
|
+
const map = localMap();
|
|
14902
|
+
for (let d = root;; d = path6.dirname(d)) {
|
|
14903
|
+
const local = map[d];
|
|
14904
|
+
if (local && LOCAL_KEY.test(local))
|
|
14905
|
+
return { key: `local:${local}`, legacyKey: `local:${local}`, root: d, name: local };
|
|
14906
|
+
if (top || path6.dirname(d) === d)
|
|
14907
|
+
return null;
|
|
15275
14908
|
}
|
|
15276
14909
|
}
|
|
15277
14910
|
function checkLocalName(name) {
|
|
15278
14911
|
if (!LOCAL_KEY.test(name))
|
|
15279
14912
|
throw new Error(`Use only lowercase letters, digits, and . _ - in the name: ${name}`);
|
|
15280
14913
|
}
|
|
15281
|
-
var repositoryRoot = (dir) => git(
|
|
14914
|
+
var repositoryRoot = (dir) => git(path6.resolve(dir), "rev-parse", "--show-toplevel") || null;
|
|
15282
14915
|
function nameLocal(dir, name) {
|
|
15283
14916
|
checkLocalName(name);
|
|
15284
14917
|
const place = identify(dir);
|
|
@@ -15287,12 +14920,12 @@ function nameLocal(dir, name) {
|
|
|
15287
14920
|
const root = rootOf(dir);
|
|
15288
14921
|
const m = localMap();
|
|
15289
14922
|
m[root] = name;
|
|
15290
|
-
|
|
15291
|
-
|
|
14923
|
+
fs5.mkdirSync(path6.dirname(localFile()), { recursive: true, mode: 448 });
|
|
14924
|
+
fs5.writeFileSync(localFile(), `${JSON.stringify(m, null, 2)}
|
|
15292
14925
|
`);
|
|
15293
14926
|
return { key: `local:${name}`, root, name };
|
|
15294
14927
|
}
|
|
15295
|
-
function localRoots(roots = [
|
|
14928
|
+
function localRoots(roots = [path6.join(os3.homedir(), "Projects")]) {
|
|
15296
14929
|
const seen = new Map;
|
|
15297
14930
|
const add = (p) => {
|
|
15298
14931
|
if (p)
|
|
@@ -15301,16 +14934,16 @@ function localRoots(roots = [path7.join(os3.homedir(), "Projects")]) {
|
|
|
15301
14934
|
for (const r2 of roots) {
|
|
15302
14935
|
let entries = [];
|
|
15303
14936
|
try {
|
|
15304
|
-
entries =
|
|
14937
|
+
entries = fs5.readdirSync(r2, { withFileTypes: true });
|
|
15305
14938
|
} catch {
|
|
15306
14939
|
continue;
|
|
15307
14940
|
}
|
|
15308
14941
|
for (const e of entries)
|
|
15309
14942
|
if (e.isDirectory() && !e.name.startsWith("."))
|
|
15310
|
-
add(identify(
|
|
14943
|
+
add(identify(path6.join(r2, e.name)));
|
|
15311
14944
|
}
|
|
15312
14945
|
for (const [root, name] of Object.entries(localMap())) {
|
|
15313
|
-
if (LOCAL_KEY.test(name) &&
|
|
14946
|
+
if (LOCAL_KEY.test(name) && fs5.existsSync(root))
|
|
15314
14947
|
add({ key: `local:${name}`, root, name });
|
|
15315
14948
|
}
|
|
15316
14949
|
const found = new Map;
|
|
@@ -15324,11 +14957,11 @@ function localRoots(roots = [path7.join(os3.homedir(), "Projects")]) {
|
|
|
15324
14957
|
return { found, ambiguous };
|
|
15325
14958
|
}
|
|
15326
14959
|
function relativeTo(root, file, cwd = root) {
|
|
15327
|
-
const abs =
|
|
15328
|
-
const rel =
|
|
14960
|
+
const abs = path6.resolve(cwd, file);
|
|
14961
|
+
const rel = path6.relative(root, abs);
|
|
15329
14962
|
if (!rel || leaves(rel) || /\p{Cc}/u.test(rel))
|
|
15330
14963
|
return null;
|
|
15331
|
-
return rel.split(
|
|
14964
|
+
return rel.split(path6.sep).join("/");
|
|
15332
14965
|
}
|
|
15333
14966
|
function patchPaths(patch) {
|
|
15334
14967
|
const out = [];
|
|
@@ -15343,8 +14976,8 @@ function patchPaths(patch) {
|
|
|
15343
14976
|
|
|
15344
14977
|
// server/src/worktree.ts
|
|
15345
14978
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
15346
|
-
import
|
|
15347
|
-
import
|
|
14979
|
+
import fs6 from "node:fs";
|
|
14980
|
+
import path7 from "node:path";
|
|
15348
14981
|
var MAX_PATHS = 200;
|
|
15349
14982
|
var git2 = (root, args) => {
|
|
15350
14983
|
try {
|
|
@@ -15378,7 +15011,7 @@ function snapshot(root) {
|
|
|
15378
15011
|
continue;
|
|
15379
15012
|
let st;
|
|
15380
15013
|
try {
|
|
15381
|
-
st =
|
|
15014
|
+
st = fs6.lstatSync(path7.join(root, p), { throwIfNoEntry: false });
|
|
15382
15015
|
} catch {
|
|
15383
15016
|
st = undefined;
|
|
15384
15017
|
}
|
|
@@ -15401,15 +15034,17 @@ function changed(root, before, after) {
|
|
|
15401
15034
|
}
|
|
15402
15035
|
|
|
15403
15036
|
// server/src/capture.ts
|
|
15404
|
-
var spoolDir = () =>
|
|
15405
|
-
var stateFile = () =>
|
|
15406
|
-
var rejectedDir = () =>
|
|
15407
|
-
var unregisteredDir = () =>
|
|
15037
|
+
var spoolDir = () => path8.join(sphicaHome(), "spool");
|
|
15038
|
+
var stateFile = () => path8.join(sphicaHome(), "capture.json");
|
|
15039
|
+
var rejectedDir = () => path8.join(spoolDir(), "rejected");
|
|
15040
|
+
var unregisteredDir = () => path8.join(spoolDir(), "unregistered");
|
|
15408
15041
|
var HOLD_DAYS = 30;
|
|
15409
15042
|
var HOLD_MAX = 1000;
|
|
15410
|
-
var baselineDir = () =>
|
|
15043
|
+
var baselineDir = () => path8.join(sphicaHome(), "worktree");
|
|
15411
15044
|
function current(raw) {
|
|
15412
15045
|
const r2 = raw;
|
|
15046
|
+
if (typeof r2.project !== "string")
|
|
15047
|
+
throw new Error("queue record without a project key");
|
|
15413
15048
|
if (r2.v === 2)
|
|
15414
15049
|
return raw;
|
|
15415
15050
|
if (r2.v !== 1)
|
|
@@ -15428,22 +15063,22 @@ function current(raw) {
|
|
|
15428
15063
|
return { ...old, v: 2, kind: "edit", event: null, path: old.path, via: "tool" };
|
|
15429
15064
|
}
|
|
15430
15065
|
var MAX_MESSAGE = 128 * 1024;
|
|
15431
|
-
var
|
|
15066
|
+
var KEEP = 8 * 1024;
|
|
15432
15067
|
function fit(body) {
|
|
15433
15068
|
const all = bytes(body);
|
|
15434
15069
|
if (all <= MAX_MESSAGE) {
|
|
15435
15070
|
const kept = mask(body);
|
|
15436
15071
|
return { body: kept, truncated: false, redacted: kept !== body, originalBytes: all };
|
|
15437
15072
|
}
|
|
15438
|
-
const start = head(body,
|
|
15439
|
-
const end = tail(body,
|
|
15440
|
-
const a2 = head(mask(start),
|
|
15441
|
-
const z = tail(mask(end),
|
|
15442
|
-
const
|
|
15073
|
+
const start = head(body, KEEP * 2);
|
|
15074
|
+
const end = tail(body, KEEP * 2);
|
|
15075
|
+
const a2 = head(mask(start), KEEP);
|
|
15076
|
+
const z = tail(mask(end), KEEP);
|
|
15077
|
+
const cut = all - bytes(a2) - bytes(z);
|
|
15443
15078
|
return {
|
|
15444
15079
|
body: `${a2}
|
|
15445
15080
|
|
|
15446
|
-
[${
|
|
15081
|
+
[${cut.toLocaleString("en-US")} bytes in the middle not saved]
|
|
15447
15082
|
|
|
15448
15083
|
${z}`,
|
|
15449
15084
|
truncated: true,
|
|
@@ -15451,10 +15086,10 @@ ${z}`,
|
|
|
15451
15086
|
originalBytes: all
|
|
15452
15087
|
};
|
|
15453
15088
|
}
|
|
15454
|
-
function
|
|
15089
|
+
function prune(held) {
|
|
15455
15090
|
let files;
|
|
15456
15091
|
try {
|
|
15457
|
-
files =
|
|
15092
|
+
files = fs7.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
|
|
15458
15093
|
} catch {
|
|
15459
15094
|
return;
|
|
15460
15095
|
}
|
|
@@ -15462,21 +15097,21 @@ function prune2(held) {
|
|
|
15462
15097
|
const stale = files.filter((f) => Number(f.split("-")[0]) < cutoff);
|
|
15463
15098
|
const over = files.slice(0, Math.max(0, files.length - HOLD_MAX));
|
|
15464
15099
|
for (const f of new Set([...stale, ...over]))
|
|
15465
|
-
|
|
15100
|
+
fs7.rmSync(path8.join(held, f), { force: true });
|
|
15466
15101
|
}
|
|
15467
15102
|
function spool(record) {
|
|
15468
15103
|
const dir = spoolDir();
|
|
15469
|
-
|
|
15104
|
+
fs7.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
15470
15105
|
const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}.json`;
|
|
15471
|
-
const tmp =
|
|
15472
|
-
|
|
15473
|
-
|
|
15106
|
+
const tmp = path8.join(dir, `.${name}`);
|
|
15107
|
+
fs7.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
|
|
15108
|
+
fs7.renameSync(tmp, path8.join(dir, name));
|
|
15474
15109
|
}
|
|
15475
15110
|
var branchOf = (root) => {
|
|
15476
15111
|
try {
|
|
15477
|
-
const dotgit =
|
|
15478
|
-
const gitdir =
|
|
15479
|
-
const h2 =
|
|
15112
|
+
const dotgit = path8.join(root, ".git");
|
|
15113
|
+
const gitdir = fs7.statSync(dotgit).isFile() ? path8.resolve(root, fs7.readFileSync(dotgit, "utf8").match(/^gitdir: (.+)$/m)?.[1]?.trim() ?? "") : dotgit;
|
|
15114
|
+
const h2 = fs7.readFileSync(path8.join(gitdir, "HEAD"), "utf8").trim();
|
|
15480
15115
|
return h2.startsWith("ref: refs/heads/") ? h2.slice("ref: refs/heads/".length) : null;
|
|
15481
15116
|
} catch {
|
|
15482
15117
|
return null;
|
|
@@ -15520,7 +15155,7 @@ Notes: ${notes.trim()}` : "";
|
|
|
15520
15155
|
};
|
|
15521
15156
|
}
|
|
15522
15157
|
function captureNotice(file = dbFile()) {
|
|
15523
|
-
if (!
|
|
15158
|
+
if (!fs7.existsSync(file))
|
|
15524
15159
|
return panel("sphica: no database, so conversations are not recorded", [file], "Create it with sphica init");
|
|
15525
15160
|
const s = readState();
|
|
15526
15161
|
if (s.stuck)
|
|
@@ -15529,32 +15164,32 @@ function captureNotice(file = dbFile()) {
|
|
|
15529
15164
|
return panel(`sphica: the database rejected ${plural(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
|
|
15530
15165
|
return null;
|
|
15531
15166
|
}
|
|
15532
|
-
var baselineFile = (host, session) =>
|
|
15167
|
+
var baselineFile = (host, session) => path8.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
|
|
15533
15168
|
function readBaseline(file) {
|
|
15534
15169
|
try {
|
|
15535
|
-
return JSON.parse(
|
|
15170
|
+
return JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
15536
15171
|
} catch {
|
|
15537
15172
|
return null;
|
|
15538
15173
|
}
|
|
15539
15174
|
}
|
|
15540
15175
|
function writeBaseline(file, b) {
|
|
15541
|
-
|
|
15176
|
+
fs7.mkdirSync(path8.dirname(file), { recursive: true, mode: 448 });
|
|
15542
15177
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
15543
|
-
|
|
15544
|
-
|
|
15178
|
+
fs7.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
|
|
15179
|
+
fs7.renameSync(tmp, file);
|
|
15545
15180
|
}
|
|
15546
15181
|
function pruneBaselines() {
|
|
15547
15182
|
const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
|
|
15548
15183
|
let files;
|
|
15549
15184
|
try {
|
|
15550
|
-
files =
|
|
15185
|
+
files = fs7.readdirSync(baselineDir());
|
|
15551
15186
|
} catch {
|
|
15552
15187
|
return;
|
|
15553
15188
|
}
|
|
15554
15189
|
for (const f of files) {
|
|
15555
|
-
const file =
|
|
15556
|
-
if ((
|
|
15557
|
-
|
|
15190
|
+
const file = path8.join(baselineDir(), f);
|
|
15191
|
+
if ((fs7.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
|
|
15192
|
+
fs7.rmSync(file, { force: true });
|
|
15558
15193
|
}
|
|
15559
15194
|
}
|
|
15560
15195
|
function onHook(host, input) {
|
|
@@ -15565,7 +15200,7 @@ function onHook(host, input) {
|
|
|
15565
15200
|
return { flush: false };
|
|
15566
15201
|
const file = process.env.CLAUDE_ENV_FILE;
|
|
15567
15202
|
if (file && input.session_id && /^[A-Za-z0-9_-]+$/.test(input.session_id)) {
|
|
15568
|
-
|
|
15203
|
+
fs7.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
|
|
15569
15204
|
`);
|
|
15570
15205
|
}
|
|
15571
15206
|
const place2 = identify(input.cwd ?? process.cwd());
|
|
@@ -15590,12 +15225,12 @@ function onHook(host, input) {
|
|
|
15590
15225
|
v: 2,
|
|
15591
15226
|
host,
|
|
15592
15227
|
session: String(input.session_id),
|
|
15593
|
-
project: place.
|
|
15228
|
+
project: place.legacyKey,
|
|
15594
15229
|
branch: branchOf(place.root),
|
|
15595
15230
|
turn,
|
|
15596
15231
|
at
|
|
15597
15232
|
};
|
|
15598
|
-
const
|
|
15233
|
+
const say = (key, speaker, raw, when = at) => {
|
|
15599
15234
|
const kept = fit(clean(raw).trim());
|
|
15600
15235
|
if (!kept.body.trim())
|
|
15601
15236
|
return;
|
|
@@ -15613,11 +15248,11 @@ function onHook(host, input) {
|
|
|
15613
15248
|
if (event === "UserPromptSubmit" && input.prompt) {
|
|
15614
15249
|
const prompt = input.prompt.trimStart();
|
|
15615
15250
|
if (!INJECTED.some((r2) => r2.test(prompt)))
|
|
15616
|
-
|
|
15251
|
+
say(`${turn}:owner`, "owner", prompt);
|
|
15617
15252
|
}
|
|
15618
15253
|
if (event === "Stop") {
|
|
15619
15254
|
if (input.last_assistant_message)
|
|
15620
|
-
|
|
15255
|
+
say(`${turn}:assistant`, "assistant", input.last_assistant_message);
|
|
15621
15256
|
const now = snapshot(place.root);
|
|
15622
15257
|
if (now) {
|
|
15623
15258
|
const before = readBaseline(baseline);
|
|
@@ -15635,8 +15270,8 @@ function onHook(host, input) {
|
|
|
15635
15270
|
const said = answersOf(input);
|
|
15636
15271
|
if (said) {
|
|
15637
15272
|
const id = `${turn}:ask:${input.tool_use_id ?? at}`;
|
|
15638
|
-
|
|
15639
|
-
|
|
15273
|
+
say(`${id}:q`, "assistant", said.questions, new Date(Date.parse(at) - 1).toISOString());
|
|
15274
|
+
say(id, "owner", said.answers);
|
|
15640
15275
|
}
|
|
15641
15276
|
return { flush: false };
|
|
15642
15277
|
}
|
|
@@ -15651,13 +15286,13 @@ function onHook(host, input) {
|
|
|
15651
15286
|
}
|
|
15652
15287
|
function writeState(s) {
|
|
15653
15288
|
try {
|
|
15654
|
-
|
|
15289
|
+
fs7.writeFileSync(stateFile(), JSON.stringify(s));
|
|
15655
15290
|
} catch {}
|
|
15656
15291
|
}
|
|
15657
15292
|
function readState() {
|
|
15658
15293
|
const count = (dir) => {
|
|
15659
15294
|
try {
|
|
15660
|
-
return
|
|
15295
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
|
|
15661
15296
|
} catch {
|
|
15662
15297
|
return 0;
|
|
15663
15298
|
}
|
|
@@ -15669,7 +15304,7 @@ function readState() {
|
|
|
15669
15304
|
};
|
|
15670
15305
|
let raw = {};
|
|
15671
15306
|
try {
|
|
15672
|
-
const parsed = JSON.parse(
|
|
15307
|
+
const parsed = JSON.parse(fs7.readFileSync(stateFile(), "utf8"));
|
|
15673
15308
|
if (parsed && typeof parsed === "object")
|
|
15674
15309
|
raw = parsed;
|
|
15675
15310
|
} catch {}
|
|
@@ -15683,25 +15318,25 @@ function readState() {
|
|
|
15683
15318
|
};
|
|
15684
15319
|
}
|
|
15685
15320
|
function lock() {
|
|
15686
|
-
const file =
|
|
15687
|
-
|
|
15321
|
+
const file = path8.join(spoolDir(), ".lock");
|
|
15322
|
+
fs7.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
|
|
15688
15323
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
15689
15324
|
try {
|
|
15690
|
-
|
|
15325
|
+
fs7.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
|
|
15691
15326
|
return () => {
|
|
15692
15327
|
try {
|
|
15693
|
-
if (
|
|
15694
|
-
|
|
15328
|
+
if (fs7.readFileSync(file, "utf8") === String(process.pid))
|
|
15329
|
+
fs7.rmSync(file, { force: true });
|
|
15695
15330
|
} catch {}
|
|
15696
15331
|
};
|
|
15697
15332
|
} catch (e) {
|
|
15698
15333
|
if (e.code !== "EEXIST")
|
|
15699
15334
|
throw e;
|
|
15700
15335
|
}
|
|
15701
|
-
const st =
|
|
15336
|
+
const st = fs7.statSync(file, { throwIfNoEntry: false });
|
|
15702
15337
|
if (!st)
|
|
15703
15338
|
continue;
|
|
15704
|
-
const holder = Number(
|
|
15339
|
+
const holder = Number(fs7.readFileSync(file, "utf8") || 0);
|
|
15705
15340
|
const fresh = Date.now() - st.mtimeMs < 5 * 60000;
|
|
15706
15341
|
const alive = (() => {
|
|
15707
15342
|
if (holder <= 0)
|
|
@@ -15714,7 +15349,7 @@ function lock() {
|
|
|
15714
15349
|
})();
|
|
15715
15350
|
if (alive && fresh)
|
|
15716
15351
|
return null;
|
|
15717
|
-
|
|
15352
|
+
fs7.rmSync(file, { force: true });
|
|
15718
15353
|
}
|
|
15719
15354
|
return null;
|
|
15720
15355
|
}
|
|
@@ -15722,17 +15357,33 @@ var BATCH = 500;
|
|
|
15722
15357
|
var FLUSH_BUDGET_MS = 30000;
|
|
15723
15358
|
var ROWS = 1000;
|
|
15724
15359
|
var chunks = (xs, n2 = ROWS) => Array.from({ length: Math.ceil(xs.length / n2) }, (_, i2) => xs.slice(i2 * n2, (i2 + 1) * n2));
|
|
15725
|
-
async function
|
|
15360
|
+
async function projectsOf(trx, keys) {
|
|
15361
|
+
if (!keys.length)
|
|
15362
|
+
return new Map;
|
|
15363
|
+
const all = await trx.selectFrom("project").select(["id", "key"]).execute();
|
|
15364
|
+
const exact = new Map(all.map((p) => [p.key, p.id]));
|
|
15365
|
+
const normal = Map.groupBy(all, (p) => normalizeKey(p.key));
|
|
15366
|
+
const found = new Map;
|
|
15367
|
+
for (const k of keys) {
|
|
15368
|
+
const same = normal.get(normalizeKey(k));
|
|
15369
|
+
const id = exact.get(k) ?? (same?.length === 1 ? same[0]?.id : undefined);
|
|
15370
|
+
if (id !== undefined)
|
|
15371
|
+
found.set(k, id);
|
|
15372
|
+
}
|
|
15373
|
+
return found;
|
|
15374
|
+
}
|
|
15375
|
+
async function write(db, batch) {
|
|
15726
15376
|
return inTransaction(db, async (trx) => {
|
|
15377
|
+
const projects = await projectsOf(trx, batch.map((r2) => r2.project));
|
|
15727
15378
|
const sessions = new Map;
|
|
15728
15379
|
for (const r2 of batch) {
|
|
15729
15380
|
const p = projects.get(r2.project);
|
|
15730
|
-
if (
|
|
15381
|
+
if (p === undefined)
|
|
15731
15382
|
continue;
|
|
15732
|
-
const id = sessionId(p
|
|
15383
|
+
const id = sessionId(p, r2.host, r2.session);
|
|
15733
15384
|
const prev = sessions.get(id);
|
|
15734
15385
|
if (!prev || Date.parse(r2.at) < Date.parse(prev.at))
|
|
15735
|
-
sessions.set(id, { project: p
|
|
15386
|
+
sessions.set(id, { project: p, host: r2.host, session: r2.session, branch: r2.branch, at: r2.at });
|
|
15736
15387
|
}
|
|
15737
15388
|
for (const part of chunks([...sessions]))
|
|
15738
15389
|
await trx.insertInto("capture_session").values(part.map(([id, v]) => ({
|
|
@@ -15745,9 +15396,9 @@ async function write(db, batch, projects) {
|
|
|
15745
15396
|
}))).execute();
|
|
15746
15397
|
const messages = batch.flatMap((m) => {
|
|
15747
15398
|
const p = m.kind === "message" ? projects.get(m.project) : undefined;
|
|
15748
|
-
if (m.kind !== "message" ||
|
|
15399
|
+
if (m.kind !== "message" || p === undefined)
|
|
15749
15400
|
return [];
|
|
15750
|
-
return [{ m, session: sessionId(p
|
|
15401
|
+
return [{ m, session: sessionId(p, m.host, m.session) }];
|
|
15751
15402
|
});
|
|
15752
15403
|
const present = async () => {
|
|
15753
15404
|
let n2 = 0;
|
|
@@ -15775,11 +15426,11 @@ async function write(db, batch, projects) {
|
|
|
15775
15426
|
const after = await present();
|
|
15776
15427
|
const edits = batch.flatMap((r2) => {
|
|
15777
15428
|
const p = r2.kind === "edit" ? projects.get(r2.project) : undefined;
|
|
15778
|
-
if (r2.kind !== "edit" ||
|
|
15429
|
+
if (r2.kind !== "edit" || p === undefined)
|
|
15779
15430
|
return [];
|
|
15780
15431
|
return [
|
|
15781
15432
|
{
|
|
15782
|
-
session_id: sessionId(p
|
|
15433
|
+
session_id: sessionId(p, r2.host, r2.session),
|
|
15783
15434
|
turn_id: r2.turn,
|
|
15784
15435
|
tool_event_id: r2.event,
|
|
15785
15436
|
path: r2.path,
|
|
@@ -15790,14 +15441,14 @@ async function write(db, batch, projects) {
|
|
|
15790
15441
|
});
|
|
15791
15442
|
for (const part of chunks(edits))
|
|
15792
15443
|
await trx.insertInto("capture_edit").values(part).execute();
|
|
15793
|
-
return after - before;
|
|
15444
|
+
return { sent: after - before, strayed: new Set(batch.filter((r2) => !projects.has(r2.project))) };
|
|
15794
15445
|
});
|
|
15795
15446
|
}
|
|
15796
15447
|
var REJECTED = new Set([18, 19, 20, 25]);
|
|
15797
15448
|
var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
|
|
15798
15449
|
var queued = (from) => {
|
|
15799
15450
|
try {
|
|
15800
|
-
return
|
|
15451
|
+
return fs7.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
|
|
15801
15452
|
} catch {
|
|
15802
15453
|
return [];
|
|
15803
15454
|
}
|
|
@@ -15808,30 +15459,33 @@ async function sendBatch(db, names) {
|
|
|
15808
15459
|
for (const { name, from } of names) {
|
|
15809
15460
|
let r2;
|
|
15810
15461
|
try {
|
|
15811
|
-
r2 = current(JSON.parse(
|
|
15462
|
+
r2 = current(JSON.parse(fs7.readFileSync(path8.join(from, name), "utf8")));
|
|
15812
15463
|
} catch {
|
|
15813
|
-
|
|
15814
|
-
|
|
15464
|
+
fs7.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
15465
|
+
fs7.renameSync(path8.join(from, name), path8.join(rejectedDir(), name));
|
|
15815
15466
|
continue;
|
|
15816
15467
|
}
|
|
15817
15468
|
if (r2)
|
|
15818
15469
|
records.push({ name, from, r: r2 });
|
|
15819
15470
|
else
|
|
15820
|
-
|
|
15471
|
+
fs7.rmSync(path8.join(from, name), { force: true });
|
|
15821
15472
|
}
|
|
15822
|
-
const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
|
|
15823
|
-
const known = records.filter((x) => projects.has(x.r.project));
|
|
15824
|
-
const strayed = records.filter((x) => !projects.has(x.r.project));
|
|
15825
15473
|
let sent = 0;
|
|
15474
|
+
let strayed = [];
|
|
15826
15475
|
const bad = [];
|
|
15827
15476
|
try {
|
|
15828
|
-
|
|
15477
|
+
const out = await write(db, records.map((x) => x.r));
|
|
15478
|
+
sent = out.sent;
|
|
15479
|
+
strayed = records.filter((x) => out.strayed.has(x.r));
|
|
15829
15480
|
} catch (e) {
|
|
15830
15481
|
if (!rejected(e))
|
|
15831
15482
|
throw e;
|
|
15832
|
-
for (const x of
|
|
15483
|
+
for (const x of records) {
|
|
15833
15484
|
try {
|
|
15834
|
-
|
|
15485
|
+
const out = await write(db, [x.r]);
|
|
15486
|
+
sent += out.sent;
|
|
15487
|
+
if (out.strayed.size)
|
|
15488
|
+
strayed.push(x);
|
|
15835
15489
|
} catch (e2) {
|
|
15836
15490
|
if (!rejected(e2))
|
|
15837
15491
|
throw e2;
|
|
@@ -15840,10 +15494,10 @@ async function sendBatch(db, names) {
|
|
|
15840
15494
|
}
|
|
15841
15495
|
}
|
|
15842
15496
|
if (bad.length) {
|
|
15843
|
-
|
|
15497
|
+
fs7.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
|
|
15844
15498
|
for (const x of bad) {
|
|
15845
15499
|
try {
|
|
15846
|
-
|
|
15500
|
+
fs7.renameSync(path8.join(x.from, x.name), path8.join(rejectedDir(), x.name));
|
|
15847
15501
|
} catch (e) {
|
|
15848
15502
|
if (e.code !== "ENOENT")
|
|
15849
15503
|
throw e;
|
|
@@ -15851,12 +15505,12 @@ async function sendBatch(db, names) {
|
|
|
15851
15505
|
}
|
|
15852
15506
|
}
|
|
15853
15507
|
if (strayed.length) {
|
|
15854
|
-
|
|
15508
|
+
fs7.mkdirSync(held, { recursive: true, mode: 448 });
|
|
15855
15509
|
for (const x of strayed) {
|
|
15856
15510
|
if (x.from === held)
|
|
15857
15511
|
continue;
|
|
15858
15512
|
try {
|
|
15859
|
-
|
|
15513
|
+
fs7.renameSync(path8.join(x.from, x.name), path8.join(held, x.name));
|
|
15860
15514
|
} catch (e) {
|
|
15861
15515
|
if (e.code !== "ENOENT")
|
|
15862
15516
|
throw e;
|
|
@@ -15866,7 +15520,7 @@ async function sendBatch(db, names) {
|
|
|
15866
15520
|
const moved = new Set([...bad, ...strayed].map((x) => x.name));
|
|
15867
15521
|
for (const x of records)
|
|
15868
15522
|
if (!moved.has(x.name))
|
|
15869
|
-
|
|
15523
|
+
fs7.rmSync(path8.join(x.from, x.name), { force: true });
|
|
15870
15524
|
return { sent, rejected: bad.length };
|
|
15871
15525
|
}
|
|
15872
15526
|
async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
@@ -15897,7 +15551,7 @@ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
|
15897
15551
|
const sentBefore = batches;
|
|
15898
15552
|
try {
|
|
15899
15553
|
if (first) {
|
|
15900
|
-
|
|
15554
|
+
prune(held);
|
|
15901
15555
|
for (const part of chunks(queued(held), BATCH)) {
|
|
15902
15556
|
if (late())
|
|
15903
15557
|
break;
|
|
@@ -15911,7 +15565,7 @@ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
|
|
|
15911
15565
|
await send(part);
|
|
15912
15566
|
queueBatches++;
|
|
15913
15567
|
}
|
|
15914
|
-
|
|
15568
|
+
prune(held);
|
|
15915
15569
|
total.deferred = queued(held).length;
|
|
15916
15570
|
if (batches > sentBefore)
|
|
15917
15571
|
writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
|
|
@@ -15952,6 +15606,392 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
|
|
|
15952
15606
|
main().catch(() => {});
|
|
15953
15607
|
}
|
|
15954
15608
|
|
|
15609
|
+
// server/src/cli/view.ts
|
|
15610
|
+
import { Writable } from "node:stream";
|
|
15611
|
+
import { styleText as styleText3 } from "node:util";
|
|
15612
|
+
var colored2 = () => Boolean(process.stdout.isTTY && process.stderr.isTTY) && !process.env.NO_COLOR;
|
|
15613
|
+
var columns = () => process.stdout.isTTY ? Math.max(10, process.stdout.columns || 100) : 1e4;
|
|
15614
|
+
function capture(draw) {
|
|
15615
|
+
let text = "";
|
|
15616
|
+
const output = new Writable({
|
|
15617
|
+
write(chunk, _encoding, done) {
|
|
15618
|
+
text += String(chunk);
|
|
15619
|
+
done();
|
|
15620
|
+
}
|
|
15621
|
+
});
|
|
15622
|
+
Object.assign(output, { columns: columns(), isTTY: true });
|
|
15623
|
+
draw(output);
|
|
15624
|
+
return text.replace(/\n$/, "");
|
|
15625
|
+
}
|
|
15626
|
+
var SGR = /(\u001b\[[0-9;]*m)/;
|
|
15627
|
+
var clean2 = (text) => text.split(SGR).map((piece, i2) => i2 % 2 ? piece : plain(piece)).join("");
|
|
15628
|
+
var oneLine = (text) => clean2(text).replace(/[\n\t]+/g, " ");
|
|
15629
|
+
var bold = (text) => styleText3("bold", text);
|
|
15630
|
+
var dim = (text) => styleText3("dim", text);
|
|
15631
|
+
var plainLine = (text) => wrapAnsi(oneLine(text), columns() - 2, { hard: true, trim: true }).split(`
|
|
15632
|
+
`).join(`
|
|
15633
|
+
`);
|
|
15634
|
+
var GUIDE = 3;
|
|
15635
|
+
var MARKED = /^(?:\u001b\[[0-9;]*m)*[✓△✗○](?:\u001b\[[0-9;]*m)* /;
|
|
15636
|
+
function wrapLine(line, room) {
|
|
15637
|
+
const body = line.trimStart();
|
|
15638
|
+
if (body === "")
|
|
15639
|
+
return [""];
|
|
15640
|
+
const lead = line.length - body.length;
|
|
15641
|
+
const cells = MARKED.test(body) ? /^(.*\S\s{2,})(\S.*)$/.exec(body) : null;
|
|
15642
|
+
const label = cells?.[1] ?? "";
|
|
15643
|
+
const hang = lead + dist_default2(label);
|
|
15644
|
+
const [head2, text, at] = cells?.[2] && room - hang >= 10 ? [label, cells[2], hang] : ["", body, lead];
|
|
15645
|
+
return wrapAnsi(text, Math.max(1, room - at), { hard: true, trim: true }).split(`
|
|
15646
|
+
`).map((part, i2) => i2 === 0 ? `${" ".repeat(lead)}${head2}${part}` : `${" ".repeat(at)}${part}`);
|
|
15647
|
+
}
|
|
15648
|
+
function content(lines, spacing = 0) {
|
|
15649
|
+
const cleaned = lines.flatMap((l) => clean2(l).split(`
|
|
15650
|
+
`));
|
|
15651
|
+
if (!colored2()) {
|
|
15652
|
+
const wrapped2 = cleaned.flatMap((l) => wrapLine(l, columns() - 2));
|
|
15653
|
+
return wrapped2.map((l) => l ? ` ${l}` : "").join(`
|
|
15654
|
+
`);
|
|
15655
|
+
}
|
|
15656
|
+
const wrapped = cleaned.flatMap((l) => wrapLine(l, columns() - GUIDE));
|
|
15657
|
+
return capture((output) => log.message(wrapped, { output, spacing }));
|
|
15658
|
+
}
|
|
15659
|
+
function title2(text, meta) {
|
|
15660
|
+
if (!colored2())
|
|
15661
|
+
return plainLine(text);
|
|
15662
|
+
const t2 = cut(oneLine(text), columns() - GUIDE - 2);
|
|
15663
|
+
const room = columns() - GUIDE - dist_default2(t2) - 2;
|
|
15664
|
+
const extra = meta && room > 8 ? ` ${cut(oneLine(meta), room - 2)}` : "";
|
|
15665
|
+
return capture((output) => intro(`${styleText3(["inverse", "bold"], ` ${t2} `)}${extra ? dim(extra) : ""}`, { output }));
|
|
15666
|
+
}
|
|
15667
|
+
function section(text, gap = false) {
|
|
15668
|
+
const t2 = oneLine(text);
|
|
15669
|
+
if (!colored2())
|
|
15670
|
+
return `${gap ? `
|
|
15671
|
+
` : ""} ${t2}`;
|
|
15672
|
+
return capture((output) => log.step(bold(t2), { output }));
|
|
15673
|
+
}
|
|
15674
|
+
function indent(text) {
|
|
15675
|
+
return content(text.split(`
|
|
15676
|
+
`));
|
|
15677
|
+
}
|
|
15678
|
+
function closing(text) {
|
|
15679
|
+
if (!colored2())
|
|
15680
|
+
return plainLine(text);
|
|
15681
|
+
const t2 = oneLine(text);
|
|
15682
|
+
const lines = wrapAnsi(t2, columns() - GUIDE, { hard: true, trim: true }).split(`
|
|
15683
|
+
`);
|
|
15684
|
+
return capture((output) => outro(bold(lines.join(`
|
|
15685
|
+
${" ".repeat(GUIDE)}`)), { output }));
|
|
15686
|
+
}
|
|
15687
|
+
function failure(head2, reason2) {
|
|
15688
|
+
return [title2(head2), stopped(reason2)].join(`
|
|
15689
|
+
`);
|
|
15690
|
+
}
|
|
15691
|
+
function stopped(reason2) {
|
|
15692
|
+
if (!colored2())
|
|
15693
|
+
return [indent(reason2), closing(`${mark("fail")} Stopped`)].join(`
|
|
15694
|
+
`);
|
|
15695
|
+
const lines = clean2(reason2).split(`
|
|
15696
|
+
`);
|
|
15697
|
+
const [first = "", ...rest] = lines.flatMap((l) => wrapLine(l, columns() - GUIDE));
|
|
15698
|
+
return capture((output) => {
|
|
15699
|
+
log.error(first, { output });
|
|
15700
|
+
if (rest.length)
|
|
15701
|
+
log.message(rest, { output, spacing: 0 });
|
|
15702
|
+
cancel("Stopped", { output });
|
|
15703
|
+
}).replace(/\n+$/, "");
|
|
15704
|
+
}
|
|
15705
|
+
var cut = (text, room) => dist_default2(text) <= room ? text : `${wrapAnsi(text, Math.max(1, room - 1), { hard: true }).split(`
|
|
15706
|
+
`)[0]}…`;
|
|
15707
|
+
function steps(heading, items, note) {
|
|
15708
|
+
const room = columns() - 11;
|
|
15709
|
+
if (!colored2() || items.some((x) => dist_default2(x.command) > room))
|
|
15710
|
+
return [
|
|
15711
|
+
` ${oneLine(heading)}:`,
|
|
15712
|
+
...items.map((x) => ` ${x.who}: ${x.command}${x.after ? `, then ${x.after}` : ""}`),
|
|
15713
|
+
` ${oneLine(note)}`
|
|
15714
|
+
].join(`
|
|
15715
|
+
`);
|
|
15716
|
+
const body = items.flatMap((x) => [bold(x.who), ` ${x.command}`, ...x.after ? [dim(` Then: ${x.after}`)] : [], ""]).concat(dim(oneLine(note))).join(`
|
|
15717
|
+
`);
|
|
15718
|
+
return capture((output) => box(body, oneLine(heading), { output, rounded: true, width: "auto" }));
|
|
15719
|
+
}
|
|
15720
|
+
|
|
15721
|
+
// server/src/admin.ts
|
|
15722
|
+
var say = (text) => console.log(indent(text));
|
|
15723
|
+
var SCHEMA = () => path9.join(dbDir(), "schema.sql");
|
|
15724
|
+
var MIGRATIONS = () => path9.join(dbDir(), "migrations");
|
|
15725
|
+
var MIGRATION = (dir, revision) => path9.join(dir, `${String(revision).padStart(4, "0")}.sql`);
|
|
15726
|
+
var CHECK = (dir, revision) => path9.join(dir, `${String(revision).padStart(4, "0")}.check.sql`);
|
|
15727
|
+
var STOP2 = "sphica_migration_stop";
|
|
15728
|
+
var NOTE = "sphica_migration_note";
|
|
15729
|
+
var KEEP2 = 3;
|
|
15730
|
+
function backUp(raw, file, from) {
|
|
15731
|
+
fs8.mkdirSync(backupDir(file), { recursive: true, mode: 448 });
|
|
15732
|
+
const done = backupPath(file, from);
|
|
15733
|
+
const partial = `${done}.partial`;
|
|
15734
|
+
try {
|
|
15735
|
+
raw.prepare("vacuum into ?").run(partial);
|
|
15736
|
+
const look = new DatabaseSync3(partial);
|
|
15737
|
+
try {
|
|
15738
|
+
look.exec("pragma journal_mode = wal");
|
|
15739
|
+
const check = look.prepare("pragma quick_check").all();
|
|
15740
|
+
if (check.length !== 1 || check[0]?.quick_check !== "ok")
|
|
15741
|
+
throw new Error(`the copy failed its check (${check.map((c) => c.quick_check).join("; ")})`);
|
|
15742
|
+
if (versionOf(look) !== from)
|
|
15743
|
+
throw new Error(`the copy is at revision ${versionOf(look)}, not ${from}`);
|
|
15744
|
+
} finally {
|
|
15745
|
+
look.close();
|
|
15746
|
+
}
|
|
15747
|
+
fs8.renameSync(partial, done);
|
|
15748
|
+
} catch (e) {
|
|
15749
|
+
for (const f of [partial, `${partial}-wal`, `${partial}-shm`])
|
|
15750
|
+
fs8.rmSync(f, { force: true });
|
|
15751
|
+
throw new Error(`Could not back up ${file} before migrating it, so nothing was migrated (${e.message}). Check free space in ${backupDir(file)}.`);
|
|
15752
|
+
}
|
|
15753
|
+
return done;
|
|
15754
|
+
}
|
|
15755
|
+
function prune2(file, kept) {
|
|
15756
|
+
let all;
|
|
15757
|
+
try {
|
|
15758
|
+
all = backups(file);
|
|
15759
|
+
} catch {
|
|
15760
|
+
return;
|
|
15761
|
+
}
|
|
15762
|
+
for (const old of all.filter((b) => b !== kept).slice(KEEP2 - 1))
|
|
15763
|
+
try {
|
|
15764
|
+
fs8.rmSync(old, { force: true });
|
|
15765
|
+
} catch {}
|
|
15766
|
+
}
|
|
15767
|
+
var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
|
|
15768
|
+
var SQLITE_BUSY = 5;
|
|
15769
|
+
function taken(raw, table) {
|
|
15770
|
+
if (!raw.prepare("select 1 from temp.sqlite_schema where type = 'table' and name = ?").get(table))
|
|
15771
|
+
return [];
|
|
15772
|
+
const rows = raw.prepare(`select * from temp.${table} order by rule, rowid`).all();
|
|
15773
|
+
raw.exec(`drop table temp.${table}`);
|
|
15774
|
+
return rows;
|
|
15775
|
+
}
|
|
15776
|
+
function listed(rows) {
|
|
15777
|
+
const rules = new Map;
|
|
15778
|
+
for (const r2 of rows) {
|
|
15779
|
+
const of = rules.get(r2.rule);
|
|
15780
|
+
if (of)
|
|
15781
|
+
of.push(r2);
|
|
15782
|
+
else
|
|
15783
|
+
rules.set(r2.rule, [r2]);
|
|
15784
|
+
}
|
|
15785
|
+
return [...rules].flatMap(([rule2, of]) => [
|
|
15786
|
+
`${inline(String(rule2))}: ${plural(of.length, "row")}`,
|
|
15787
|
+
...of.map((r2) => ` ${inline(String(r2.item))}${r2.action ? ` → ${inline(String(r2.action))}` : ""}`)
|
|
15788
|
+
]).join(`
|
|
15789
|
+
`);
|
|
15790
|
+
}
|
|
15791
|
+
var STOP_ADVICE = {
|
|
15792
|
+
"projects with records whose keys become one once normalized": (revision, rows) => `Revision ${revision} was not applied: ${plural(rows.length, "project")} with records have keys that become one once normalized (listed below), and this Sphica cannot merge them. Search, read, and the record tools stay unavailable until a Sphica that can merge them migrates this database. Meanwhile capture keeps recording a session whose remote is written as one of the listed keys; a session whose remote is written otherwise is held, and held records are dropped after ${HOLD_DAYS} days or past ${HOLD_MAX} of them. Report the list below at https://github.com/iroha924/sphica/issues. Forgetting sources does not resolve it`
|
|
15793
|
+
};
|
|
15794
|
+
|
|
15795
|
+
class Stop extends Error {
|
|
15796
|
+
list;
|
|
15797
|
+
constructor(revision, rows) {
|
|
15798
|
+
const rules = new Set(rows.map((r2) => r2.rule));
|
|
15799
|
+
const advice = rules.size === 1 ? STOP_ADVICE[String(rows[0]?.rule)] : undefined;
|
|
15800
|
+
super(advice ? advice(revision, rows) : `The database has ${plural(rows.length, "row")} that revision ${revision} cannot take, and Sphica writes no such row (listed below). Fix the rows, or forget a listed source with /sphica:forget on the Sphica version that still opens this database, then run \`sphica init\` again`);
|
|
15801
|
+
this.list = listed(rows);
|
|
15802
|
+
}
|
|
15803
|
+
}
|
|
15804
|
+
function committed(raw) {
|
|
15805
|
+
if (raw.isTransaction)
|
|
15806
|
+
return null;
|
|
15807
|
+
try {
|
|
15808
|
+
return versionOf(raw);
|
|
15809
|
+
} catch {
|
|
15810
|
+
return null;
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15813
|
+
function immediate(raw, fn) {
|
|
15814
|
+
raw.exec("begin immediate");
|
|
15815
|
+
try {
|
|
15816
|
+
const out = fn();
|
|
15817
|
+
raw.exec("commit");
|
|
15818
|
+
return out;
|
|
15819
|
+
} catch (e) {
|
|
15820
|
+
raw.exec("rollback");
|
|
15821
|
+
throw e;
|
|
15822
|
+
}
|
|
15823
|
+
}
|
|
15824
|
+
function optimize(raw) {
|
|
15825
|
+
try {
|
|
15826
|
+
raw.exec("pragma optimize");
|
|
15827
|
+
} catch {}
|
|
15828
|
+
}
|
|
15829
|
+
function withOwner(file, fn, create = false) {
|
|
15830
|
+
const raw = connectWriter("owner", file, create);
|
|
15831
|
+
try {
|
|
15832
|
+
return fn(raw);
|
|
15833
|
+
} finally {
|
|
15834
|
+
raw.close();
|
|
15835
|
+
}
|
|
15836
|
+
}
|
|
15837
|
+
function migrate(file = dbFile(), dir = MIGRATIONS()) {
|
|
15838
|
+
return withOwner(file, (raw) => {
|
|
15839
|
+
const from = versionOf(raw);
|
|
15840
|
+
if (from >= SCHEMA_REVISION)
|
|
15841
|
+
return from;
|
|
15842
|
+
const backup = backUp(raw, file, from);
|
|
15843
|
+
say(`Backed up: ${backup} (revision ${from})`);
|
|
15844
|
+
try {
|
|
15845
|
+
for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
|
|
15846
|
+
const script = MIGRATION(dir, r2);
|
|
15847
|
+
if (!fs8.existsSync(script))
|
|
15848
|
+
throw new Error(`This Sphica has no migration script for revision ${r2} (${script} is missing). Reinstall sphica (\`npm i -g sphica@${packageVersionAt(ROOT) ?? "latest"}\`), then run \`sphica init\` again.`);
|
|
15849
|
+
raw.exec("pragma foreign_keys = off");
|
|
15850
|
+
try {
|
|
15851
|
+
const notes = immediate(raw, () => {
|
|
15852
|
+
if (versionOf(raw) !== r2 - 1)
|
|
15853
|
+
return [];
|
|
15854
|
+
if (fs8.existsSync(CHECK(dir, r2))) {
|
|
15855
|
+
raw.exec(fs8.readFileSync(CHECK(dir, r2), "utf8"));
|
|
15856
|
+
const stops = taken(raw, STOP2);
|
|
15857
|
+
if (stops.length)
|
|
15858
|
+
throw new Stop(r2, stops);
|
|
15859
|
+
}
|
|
15860
|
+
raw.exec(fs8.readFileSync(script, "utf8"));
|
|
15861
|
+
const broken = raw.prepare("pragma foreign_key_check").all().length;
|
|
15862
|
+
if (broken)
|
|
15863
|
+
throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);
|
|
15864
|
+
if (versionOf(raw) !== r2)
|
|
15865
|
+
throw new Error(`${path9.basename(script)} did not set revision ${r2}`);
|
|
15866
|
+
return taken(raw, NOTE);
|
|
15867
|
+
});
|
|
15868
|
+
if (notes.length)
|
|
15869
|
+
say(`Changed while migrating to revision ${r2}: ${plural(notes.length, "row")} (the backup keeps them as they were)
|
|
15870
|
+
${listed(notes)}`);
|
|
15871
|
+
} finally {
|
|
15872
|
+
raw.exec("pragma foreign_keys = on");
|
|
15873
|
+
}
|
|
15874
|
+
}
|
|
15875
|
+
} catch (e) {
|
|
15876
|
+
const said = e.message.replace(/\.$/, "");
|
|
15877
|
+
const list = e instanceof Stop ? `
|
|
15878
|
+
${e.list}` : "";
|
|
15879
|
+
const now = committed(raw);
|
|
15880
|
+
if (now === from) {
|
|
15881
|
+
let left = "the backup made for this run was removed";
|
|
15882
|
+
try {
|
|
15883
|
+
fs8.rmSync(backup, { force: true });
|
|
15884
|
+
} catch {
|
|
15885
|
+
left = `the backup made for this run is still at ${backup}; delete it yourself`;
|
|
15886
|
+
}
|
|
15887
|
+
const retry = sqliteCode(e) === SQLITE_BUSY ? " Another process is writing to the database. Run `sphica init` again when it has finished." : "";
|
|
15888
|
+
throw new Error(`${said}. No migration step was committed: the database is still at revision ${from}, and there is no need to replace it with a backup (${left}).${retry}${list}`);
|
|
15889
|
+
}
|
|
15890
|
+
const base = path9.basename(file);
|
|
15891
|
+
const state = now === null ? "The committed revision could not be confirmed after the failure" : `The database is now at revision ${now} (this run, or another \`sphica init\` running at the same time, migrated it that far)`;
|
|
15892
|
+
throw new Error(`${said}. ${state}. The database before migrating is at ${backup} (anything recorded after it was made is not in it). To go back to it, close every session using Sphica, move ${base}, ${base}-wal, and ${base}-shm in ${path9.dirname(file)} aside, then copy the backup to ${file}.${list}`);
|
|
15893
|
+
}
|
|
15894
|
+
optimize(raw);
|
|
15895
|
+
prune2(file, backup);
|
|
15896
|
+
return from;
|
|
15897
|
+
});
|
|
15898
|
+
}
|
|
15899
|
+
function dbInit(file = dbFile()) {
|
|
15900
|
+
if (fs8.existsSync(file)) {
|
|
15901
|
+
const look = new DatabaseSync3(file, { readOnly: true });
|
|
15902
|
+
let generation;
|
|
15903
|
+
try {
|
|
15904
|
+
generation = generationOf(look);
|
|
15905
|
+
} finally {
|
|
15906
|
+
look.close();
|
|
15907
|
+
}
|
|
15908
|
+
if (generation === null)
|
|
15909
|
+
throw new Error(`${file} is not a Sphica database (no schema). Move it to another name, then run this again.`);
|
|
15910
|
+
const got = withOwner(file, versionOf);
|
|
15911
|
+
if (got === SCHEMA_REVISION)
|
|
15912
|
+
say(`Already exists: ${file} (revision ${got})`);
|
|
15913
|
+
else if (got > SCHEMA_REVISION)
|
|
15914
|
+
say(`Already exists: ${file} (revision ${got}, made by a newer Sphica; this one expects ${SCHEMA_REVISION}. Update sphica.)`);
|
|
15915
|
+
else {
|
|
15916
|
+
migrate(file);
|
|
15917
|
+
say(`Migrated: ${file} (revision ${got} → ${SCHEMA_REVISION})`);
|
|
15918
|
+
}
|
|
15919
|
+
return;
|
|
15920
|
+
}
|
|
15921
|
+
fs8.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
|
|
15922
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
15923
|
+
fs8.rmSync(tmp, { force: true });
|
|
15924
|
+
try {
|
|
15925
|
+
withOwner(tmp, (raw) => {
|
|
15926
|
+
raw.exec("pragma journal_mode = wal");
|
|
15927
|
+
raw.exec(fs8.readFileSync(SCHEMA(), "utf8"));
|
|
15928
|
+
if (versionOf(raw) !== SCHEMA_REVISION)
|
|
15929
|
+
throw new Error(`db/schema.sql has user_version ${versionOf(raw)}, but the code expects ${SCHEMA_REVISION}`);
|
|
15930
|
+
optimize(raw);
|
|
15931
|
+
}, true);
|
|
15932
|
+
try {
|
|
15933
|
+
fs8.linkSync(tmp, file);
|
|
15934
|
+
} catch (e) {
|
|
15935
|
+
if (e.code === "EEXIST" || fs8.existsSync(file))
|
|
15936
|
+
throw new Error(`${file} already exists (another sphica init created it first). Run this again to check it.`);
|
|
15937
|
+
fs8.renameSync(tmp, file);
|
|
15938
|
+
}
|
|
15939
|
+
} finally {
|
|
15940
|
+
for (const f of [tmp, `${tmp}-wal`, `${tmp}-shm`])
|
|
15941
|
+
fs8.rmSync(f, { force: true });
|
|
15942
|
+
}
|
|
15943
|
+
say(`Created: ${file} (revision ${SCHEMA_REVISION})`);
|
|
15944
|
+
}
|
|
15945
|
+
function bindOwner(user, file = dbFile()) {
|
|
15946
|
+
return withOwner(file, (raw) => immediate(raw, () => {
|
|
15947
|
+
const revision = versionOf(raw);
|
|
15948
|
+
if (revision !== SCHEMA_REVISION)
|
|
15949
|
+
return { kind: "skipped", revision };
|
|
15950
|
+
const bound = raw.prepare("select external_id, login from owner_identity where provider = 'github' order by external_id = ? desc, bound_at, external_id limit 1").get(String(user.id));
|
|
15951
|
+
if (!bound) {
|
|
15952
|
+
raw.prepare("insert into owner_identity (provider, external_id, login, bound_at) values ('github', ?, ?, ?)").run(String(user.id), user.login, iso(Date.now()));
|
|
15953
|
+
return { kind: "bound" };
|
|
15954
|
+
}
|
|
15955
|
+
return bound.external_id === String(user.id) ? { kind: "already" } : { kind: "other", id: bound.external_id, login: bound.login };
|
|
15956
|
+
}));
|
|
15957
|
+
}
|
|
15958
|
+
function reindex(file = dbFile()) {
|
|
15959
|
+
const counts = withOwner(file, (raw) => {
|
|
15960
|
+
const n2 = immediate(raw, () => {
|
|
15961
|
+
raw.exec("insert into unit_fts (unit_fts) values ('delete-all')");
|
|
15962
|
+
raw.exec("insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text");
|
|
15963
|
+
raw.exec("insert into source_fts (source_fts) values ('delete-all')");
|
|
15964
|
+
raw.exec("insert into source_fts (rowid, lexemes) select id, sphica_terms(text) from source where indexed = 1");
|
|
15965
|
+
const n3 = (sql2) => raw.prepare(sql2).get().n;
|
|
15966
|
+
return {
|
|
15967
|
+
units: n3("select count(*) as n from unit"),
|
|
15968
|
+
sources: n3("select count(*) as n from source where indexed = 1")
|
|
15969
|
+
};
|
|
15970
|
+
});
|
|
15971
|
+
optimize(raw);
|
|
15972
|
+
return n2;
|
|
15973
|
+
});
|
|
15974
|
+
say(`Rebuilt the index: ${plural(counts.units, "unit")}, ${plural(counts.sources, "source")}`);
|
|
15975
|
+
}
|
|
15976
|
+
function inspect(file = dbFile()) {
|
|
15977
|
+
const size = (f) => fs8.existsSync(f) ? fs8.statSync(f).size : 0;
|
|
15978
|
+
return withOwner(file, (raw) => {
|
|
15979
|
+
const check = (table) => {
|
|
15980
|
+
try {
|
|
15981
|
+
raw.exec(`insert into ${table} (${table}, rank) values ('integrity-check', 1)`);
|
|
15982
|
+
return null;
|
|
15983
|
+
} catch (e) {
|
|
15984
|
+
return e.message;
|
|
15985
|
+
}
|
|
15986
|
+
};
|
|
15987
|
+
return {
|
|
15988
|
+
revision: versionOf(raw),
|
|
15989
|
+
bytes: size(file) + size(`${file}-wal`),
|
|
15990
|
+
fts: { unit: check("unit_fts"), source: check("source_fts") }
|
|
15991
|
+
};
|
|
15992
|
+
});
|
|
15993
|
+
}
|
|
15994
|
+
|
|
15955
15995
|
// server/src/cli/common.ts
|
|
15956
15996
|
async function withDb(role, fn) {
|
|
15957
15997
|
const db = role === "reader" ? openReader() : openWriter(role);
|