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/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 fs5 from "node:fs";
2970
- import path5 from "node:path";
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 = 7;
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/cli/view.ts
3730
- import { Writable } from "node:stream";
3731
- import { styleText as styleText3 } from "node:util";
3732
- var colored2 = () => Boolean(process.stdout.isTTY && process.stderr.isTTY) && !process.env.NO_COLOR;
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, columns2) {
3857
+ cloneWithColumns(node, columns) {
3965
3858
  return freeze({
3966
3859
  ...node,
3967
- columns: [...node.columns || [], ...columns2]
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 columns2 = parseColumnNamesAndIndexes(rows);
5511
+ const columns = parseColumnNamesAndIndexes(rows);
5619
5512
  return [
5620
- freeze([...columns2.keys()].map(ColumnNode.create)),
5621
- ValuesNode.create(rows.map((row) => parseRowValues(row, columns2)))
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 columns2 = new Map;
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 (!columns2.has(col) && row[col] !== undefined) {
5630
- columns2.set(col, columns2.size);
5522
+ if (!columns.has(col) && row[col] !== undefined) {
5523
+ columns.set(col, columns.size);
5631
5524
  }
5632
5525
  }
5633
5526
  }
5634
- return columns2;
5527
+ return columns;
5635
5528
  }
5636
- function parseRowValues(row, columns2) {
5529
+ function parseRowValues(row, columns) {
5637
5530
  const rowColumns = Object.keys(row);
5638
5531
  const rowValues = Array.from({
5639
- length: columns2.size
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 = columns2.get(col);
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 < columns2.size;
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(columns2) {
5798
- const columnNodes = columns2.map(ColumnNode.create);
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 [columns2, values] = parseInsertExpression(insert);
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: columns2,
5847
+ columns,
5955
5848
  values
5956
5849
  })
5957
5850
  });
5958
5851
  }
5959
- columns(columns2) {
5852
+ columns(columns) {
5960
5853
  return new InsertQueryBuilder({
5961
5854
  ...this.#props,
5962
5855
  queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
5963
- columns: freeze(columns2.map(ColumnNode.create))
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 columns2 = parts[1].split(",").map((it) => it.trim());
6684
- return CommonTableExpressionNameNode.create(table, columns2);
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 [columns2, values] = parseInsertExpression(insert);
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: columns2,
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, columns2) {
9880
+ create(table, columns) {
9988
9881
  return freeze({
9989
9882
  kind: "ReferencesNode",
9990
9883
  table,
9991
- columns: freeze([...columns2])
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(columns2, constraintName, nullsNotDistinct) {
10262
- if (isString(columns2.at(0))) {
10154
+ create(columns, constraintName, nullsNotDistinct) {
10155
+ if (isString(columns.at(0))) {
10263
10156
  logOnce("`UniqueConstraintNode.create(columns: string[], ...)` is deprecated - pass `ColumnNode[]` instead.");
10264
- columns2 = columns2.map(ColumnNode.create);
10157
+ columns = columns.map(ColumnNode.create);
10265
10158
  }
10266
10159
  return freeze({
10267
10160
  kind: "UniqueConstraintNode",
10268
- columns: freeze(columns2),
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(columns2, constraintName) {
10370
+ create(columns, constraintName) {
10478
10371
  return freeze({
10479
10372
  kind: "PrimaryKeyConstraintNode",
10480
- columns: freeze(columns2.map(ColumnNode.create)),
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, columns2) {
10399
+ cloneWithColumns(node, columns) {
10507
10400
  return freeze({
10508
10401
  ...node,
10509
- columns: [...node.columns || [], ...columns2]
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(columns2) {
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, columns2.map((item) => isString(item) ? parseOrderedColumnName(item) : item.toOperationNode()))
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, columns2, build = noop) {
10747
- const uniqueConstraintBuilder = build(new UniqueConstraintNodeBuilder(UniqueConstraintNode.create(columns2.map((column) => isString(column) ? ColumnNode.create(column) : parseExpression(column)), constraintName)));
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, columns2, targetTable, targetColumns, build = noop) {
10765
- const constraintBuilder = build(new ForeignKeyConstraintBuilder(ForeignKeyConstraintNode.create(columns2.map(ColumnNode.create), parseTable(targetTable), targetColumns.map(ColumnNode.create), constraintName)));
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, columns2, build = noop) {
10772
- const constraintBuilder = build(new PrimaryKeyConstraintBuilder(PrimaryKeyConstraintNode.create(columns2, constraintName)));
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(columns2) {
10816
+ columns(columns) {
10924
10817
  return new CreateIndexBuilder({
10925
10818
  ...this.#props,
10926
- node: CreateIndexNode.cloneWithColumns(this.#props.node, columns2.map((item) => isString(item) ? parseOrderedColumnName(item) : item.toOperationNode()))
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, columns2, build = noop) {
11057
- const constraintBuilder = build(new PrimaryKeyConstraintBuilder(PrimaryKeyConstraintNode.create(columns2, constraintName)));
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, columns2, build = noop) {
11064
- const uniqueConstraintBuilder = build(new UniqueConstraintNodeBuilder(UniqueConstraintNode.create(columns2.map((column) => isString(column) ? ColumnNode.create(column) : parseExpression(column)), constraintName)));
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, columns2, build = noop) {
11071
- const addIndexBuilder = build(new CreateTableAddIndexBuilder(AddIndexNode.cloneWithColumns(AddIndexNode.create(indexName), columns2.map((column) => isString(column) ? ColumnNode.create(column) : parseExpression(column)))));
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, columns2, targetTable, targetColumns, build = noop) {
11085
- const builder = build(new ForeignKeyConstraintBuilder(ForeignKeyConstraintNode.create(columns2.map(ColumnNode.create), parseTable(targetTable), targetColumns.map(ColumnNode.create), constraintName)));
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(columns2) {
11210
+ columns(columns) {
11318
11211
  return new CreateViewBuilder({
11319
11212
  ...this.#props,
11320
11213
  node: CreateViewNode.cloneWith(this.#props.node, {
11321
- columns: columns2.map(parseColumnName)
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 columns2 = columnsByTable[name] ?? [];
14365
+ const columns = columnsByTable[name] ?? [];
14473
14366
  if (!autoIncrementCol) {
14474
- const pkCols = columns2.filter((r2) => r2.pk > 0);
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: columns2.map((col) => ({
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/admin.ts
14925
- var say = (text) => console.log(indent(text));
14926
- var SCHEMA = () => path5.join(dbDir(), "schema.sql");
14927
- var MIGRATIONS = () => path5.join(dbDir(), "migrations");
14928
- var MIGRATION = (dir, revision) => path5.join(dir, `${String(revision).padStart(4, "0")}.sql`);
14929
- var CHECK = (dir, revision) => path5.join(dir, `${String(revision).padStart(4, "0")}.check.sql`);
14930
- var STOP2 = "sphica_migration_stop";
14931
- var NOTE = "sphica_migration_note";
14932
- var KEEP = 3;
14933
- function backUp(raw, file, from) {
14934
- fs5.mkdirSync(backupDir(file), { recursive: true, mode: 448 });
14935
- const done = backupPath(file, from);
14936
- const partial = `${done}.partial`;
14937
- try {
14938
- raw.prepare("vacuum into ?").run(partial);
14939
- const look = new DatabaseSync3(partial);
14940
- try {
14941
- look.exec("pragma journal_mode = wal");
14942
- const check = look.prepare("pragma quick_check").all();
14943
- if (check.length !== 1 || check[0]?.quick_check !== "ok")
14944
- throw new Error(`the copy failed its check (${check.map((c) => c.quick_check).join("; ")})`);
14945
- if (versionOf(look) !== from)
14946
- throw new Error(`the copy is at revision ${versionOf(look)}, not ${from}`);
14947
- } finally {
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
- all = backups(file);
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
- class Stop extends Error {
14996
- list;
14997
- constructor(revision, rows) {
14998
- super(`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`);
14999
- this.list = listed(rows);
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
- function committed(raw) {
15003
- if (raw.isTransaction)
15004
- return null;
14860
+ var git = (dir, ...args) => {
15005
14861
  try {
15006
- return versionOf(raw);
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 immediate(raw, fn) {
15012
- raw.exec("begin immediate");
14870
+ };
14871
+ function localMap() {
14872
+ let raw;
15013
14873
  try {
15014
- const out = fn();
15015
- raw.exec("commit");
15016
- return out;
14874
+ raw = fs5.readFileSync(localFile(), "utf8");
15017
14875
  } catch (e) {
15018
- raw.exec("rollback");
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
- raw.exec("pragma optimize");
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
- function withOwner(file, fn, create = false) {
15028
- const raw = connectWriter("owner", file, create);
15029
- try {
15030
- return fn(raw);
15031
- } finally {
15032
- raw.close();
15033
- }
15034
- }
15035
- function migrate(file = dbFile(), dir = MIGRATIONS()) {
15036
- return withOwner(file, (raw) => {
15037
- const from = versionOf(raw);
15038
- if (from >= SCHEMA_REVISION)
15039
- return from;
15040
- const backup = backUp(raw, file, from);
15041
- say(`Backed up: ${backup} (revision ${from})`);
15042
- try {
15043
- for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
15044
- const script = MIGRATION(dir, r2);
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(path7.resolve(dir), "rev-parse", "--show-toplevel") || null;
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
- fs6.mkdirSync(path7.dirname(localFile()), { recursive: true, mode: 448 });
15291
- fs6.writeFileSync(localFile(), `${JSON.stringify(m, null, 2)}
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 = [path7.join(os3.homedir(), "Projects")]) {
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 = fs6.readdirSync(r2, { withFileTypes: true });
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(path7.join(r2, e.name)));
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) && fs6.existsSync(root))
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 = path7.resolve(cwd, file);
15328
- const rel = path7.relative(root, abs);
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(path7.sep).join("/");
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 fs7 from "node:fs";
15347
- import path8 from "node:path";
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 = fs7.lstatSync(path8.join(root, p), { throwIfNoEntry: false });
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 = () => path9.join(sphicaHome(), "spool");
15405
- var stateFile = () => path9.join(sphicaHome(), "capture.json");
15406
- var rejectedDir = () => path9.join(spoolDir(), "rejected");
15407
- var unregisteredDir = () => path9.join(spoolDir(), "unregistered");
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 = () => path9.join(sphicaHome(), "worktree");
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 KEEP2 = 8 * 1024;
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, KEEP2 * 2);
15439
- const end = tail(body, KEEP2 * 2);
15440
- const a2 = head(mask(start), KEEP2);
15441
- const z = tail(mask(end), KEEP2);
15442
- const cut2 = all - bytes(a2) - bytes(z);
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
- [${cut2.toLocaleString("en-US")} bytes in the middle not saved]
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 prune2(held) {
15089
+ function prune(held) {
15455
15090
  let files;
15456
15091
  try {
15457
- files = fs8.readdirSync(held).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
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
- fs8.rmSync(path9.join(held, f), { force: true });
15100
+ fs7.rmSync(path8.join(held, f), { force: true });
15466
15101
  }
15467
15102
  function spool(record) {
15468
15103
  const dir = spoolDir();
15469
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
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 = path9.join(dir, `.${name}`);
15472
- fs8.writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
15473
- fs8.renameSync(tmp, path9.join(dir, name));
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 = path9.join(root, ".git");
15478
- const gitdir = fs8.statSync(dotgit).isFile() ? path9.resolve(root, fs8.readFileSync(dotgit, "utf8").match(/^gitdir: (.+)$/m)?.[1]?.trim() ?? "") : dotgit;
15479
- const h2 = fs8.readFileSync(path9.join(gitdir, "HEAD"), "utf8").trim();
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 (!fs8.existsSync(file))
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) => path9.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
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(fs8.readFileSync(file, "utf8"));
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
- fs8.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
15176
+ fs7.mkdirSync(path8.dirname(file), { recursive: true, mode: 448 });
15542
15177
  const tmp = `${file}.${process.pid}.tmp`;
15543
- fs8.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
15544
- fs8.renameSync(tmp, file);
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 = fs8.readdirSync(baselineDir());
15185
+ files = fs7.readdirSync(baselineDir());
15551
15186
  } catch {
15552
15187
  return;
15553
15188
  }
15554
15189
  for (const f of files) {
15555
- const file = path9.join(baselineDir(), f);
15556
- if ((fs8.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
15557
- fs8.rmSync(file, { force: true });
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
- fs8.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
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.key,
15228
+ project: place.legacyKey,
15594
15229
  branch: branchOf(place.root),
15595
15230
  turn,
15596
15231
  at
15597
15232
  };
15598
- const say2 = (key, speaker, raw, when = at) => {
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
- say2(`${turn}:owner`, "owner", prompt);
15251
+ say(`${turn}:owner`, "owner", prompt);
15617
15252
  }
15618
15253
  if (event === "Stop") {
15619
15254
  if (input.last_assistant_message)
15620
- say2(`${turn}:assistant`, "assistant", input.last_assistant_message);
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
- say2(`${id}:q`, "assistant", said.questions, new Date(Date.parse(at) - 1).toISOString());
15639
- say2(id, "owner", said.answers);
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
- fs8.writeFileSync(stateFile(), JSON.stringify(s));
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 fs8.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length;
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(fs8.readFileSync(stateFile(), "utf8"));
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 = path9.join(spoolDir(), ".lock");
15687
- fs8.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
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
- fs8.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
15325
+ fs7.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
15691
15326
  return () => {
15692
15327
  try {
15693
- if (fs8.readFileSync(file, "utf8") === String(process.pid))
15694
- fs8.rmSync(file, { force: true });
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 = fs8.statSync(file, { throwIfNoEntry: false });
15336
+ const st = fs7.statSync(file, { throwIfNoEntry: false });
15702
15337
  if (!st)
15703
15338
  continue;
15704
- const holder = Number(fs8.readFileSync(file, "utf8") || 0);
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
- fs8.rmSync(file, { force: true });
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 write(db, batch, projects) {
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 (!p)
15381
+ if (p === undefined)
15731
15382
  continue;
15732
- const id = sessionId(p.id, r2.host, r2.session);
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.id, host: r2.host, session: r2.session, branch: r2.branch, at: r2.at });
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" || !p)
15399
+ if (m.kind !== "message" || p === undefined)
15749
15400
  return [];
15750
- return [{ m, session: sessionId(p.id, m.host, m.session) }];
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" || !p)
15429
+ if (r2.kind !== "edit" || p === undefined)
15779
15430
  return [];
15780
15431
  return [
15781
15432
  {
15782
- session_id: sessionId(p.id, r2.host, r2.session),
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 fs8.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
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(fs8.readFileSync(path9.join(from, name), "utf8")));
15462
+ r2 = current(JSON.parse(fs7.readFileSync(path8.join(from, name), "utf8")));
15812
15463
  } catch {
15813
- fs8.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15814
- fs8.renameSync(path9.join(from, name), path9.join(rejectedDir(), name));
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
- fs8.rmSync(path9.join(from, name), { force: true });
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
- sent = await write(db, known.map((x) => x.r), projects);
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 known) {
15483
+ for (const x of records) {
15833
15484
  try {
15834
- sent += await write(db, [x.r], projects);
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
- fs8.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15497
+ fs7.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15844
15498
  for (const x of bad) {
15845
15499
  try {
15846
- fs8.renameSync(path9.join(x.from, x.name), path9.join(rejectedDir(), x.name));
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
- fs8.mkdirSync(held, { recursive: true, mode: 448 });
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
- fs8.renameSync(path9.join(x.from, x.name), path9.join(held, x.name));
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
- fs8.rmSync(path9.join(x.from, x.name), { force: true });
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
- prune2(held);
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
- prune2(held);
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);