latticesql 3.4.4 → 3.4.5

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
@@ -632,14 +632,6 @@ function resolveDbPath(raw, configDir2) {
632
632
  }
633
633
  return resolve(configDir2, raw);
634
634
  }
635
- function warnDeprecatedRef(entity, field, target) {
636
- const key = `${entity}.${field}`;
637
- if (warnedDeprecatedRefs.has(key)) return;
638
- warnedDeprecatedRefs.add(key);
639
- console.warn(
640
- `Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
641
- );
642
- }
643
635
  function entityToTableDef(entityName, entity) {
644
636
  const rawFields = entity.fields;
645
637
  if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
@@ -666,7 +658,6 @@ function entityToTableDef(entityName, entity) {
666
658
  table: field.ref,
667
659
  foreignKey: fieldName
668
660
  };
669
- warnDeprecatedRef(entityName, fieldName, field.ref);
670
661
  }
671
662
  }
672
663
  const primaryKey = entity.primaryKey ?? pkFromField;
@@ -823,12 +814,10 @@ function parseEntityContexts(entityContexts) {
823
814
  }
824
815
  return result;
825
816
  }
826
- var warnedDeprecatedRefs;
827
817
  var init_parser = __esm({
828
818
  "src/config/parser.ts"() {
829
819
  "use strict";
830
820
  init_user_config();
831
- warnedDeprecatedRefs = /* @__PURE__ */ new Set();
832
821
  }
833
822
  });
834
823
 
@@ -54094,6 +54083,8 @@ var css = `
54094
54083
  .app-version:empty { display: none; }
54095
54084
  .app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
54096
54085
  .app-update[hidden] { display: none; }
54086
+ #app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
54087
+ #app-update-link[hidden] { display: none; }
54097
54088
  /* Unseen-change count next to a sidebar entity. */
54098
54089
  .nav-badge {
54099
54090
  display: inline-block; min-width: 16px; text-align: center;
@@ -55572,6 +55563,12 @@ var appJs = `
55572
55563
  // drag handle once the app has booted.
55573
55564
  var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
55574
55565
  if (!isNaN(savedRail)) applyRailWidth(savedRail);
55566
+ // The version chip + manual-upgrade link live in the static shell (present
55567
+ // from first paint, in both the normal and virgin-state boots), so wire the
55568
+ // click handler and run the first availability check here \u2014 independent of
55569
+ // the async workspace bootstrap. checkServerVersion() refreshes it later.
55570
+ wireUpdateLink();
55571
+ checkUpdateAvailable();
55575
55572
  // Failsafe: never leave the overlay up forever if a fetch hangs without
55576
55573
  // rejecting, or a future early-return (e.g. the virgin-state screen)
55577
55574
  // bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
@@ -56056,6 +56053,26 @@ var appJs = `
56056
56053
  showUpdatePill(label || 'Updated \u2014 reloading\u2026');
56057
56054
  setTimeout(function () { location.reload(); }, 600);
56058
56055
  }
56056
+ // Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
56057
+ // the version chip only when the server reports a newer, installable version.
56058
+ // The auto-updater installs in the background on its own cadence; this lets
56059
+ // the user force it now. Best-effort; the link stays hidden on any failure.
56060
+ function checkUpdateAvailable() {
56061
+ var el = document.getElementById('app-update-link');
56062
+ if (!el) return;
56063
+ fetch('/api/update/status')
56064
+ .then(function (r) { return r.ok ? r.json() : null; })
56065
+ .then(function (s) {
56066
+ if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
56067
+ el.textContent = 'Update available \u2014 Upgrade';
56068
+ el.title = 'Install v' + s.latest + ' and restart';
56069
+ el.hidden = false;
56070
+ } else {
56071
+ el.hidden = true;
56072
+ }
56073
+ })
56074
+ .catch(function () { /* best-effort \u2014 keep the link hidden */ });
56075
+ }
56059
56076
  // On every (re)connect, ask the server its version. A change vs BOOT_VERSION
56060
56077
  // means a relaunch onto new code \u2192 reload. Best-effort; never throws.
56061
56078
  function checkServerVersion() {
@@ -56069,6 +56086,31 @@ var appJs = `
56069
56086
  else hideUpdatePill();
56070
56087
  })
56071
56088
  .catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
56089
+ // Refresh the manual-upgrade link alongside the reconnect version check.
56090
+ checkUpdateAvailable();
56091
+ }
56092
+ // Wire the manual-upgrade link's click: kick off the install (the server
56093
+ // installs the latest and restarts onto it) and surface the progress. On
56094
+ // success we do nothing else \u2014 the update-applied event + the reconnect
56095
+ // version check land the page on the new version (no manual reload). A
56096
+ // false ok means the install can't run (unsupervised) \u2014 toast why.
56097
+ function wireUpdateLink() {
56098
+ var el = document.getElementById('app-update-link');
56099
+ if (!el) return;
56100
+ el.addEventListener('click', function (e) {
56101
+ e.preventDefault();
56102
+ el.hidden = true;
56103
+ showUpdatePill('Updating\u2026');
56104
+ fetch('/api/update/apply', { method: 'POST' })
56105
+ .then(function (r) { return r.json(); })
56106
+ .then(function (d) {
56107
+ if (d && d.ok === false) {
56108
+ hideUpdatePill();
56109
+ showToast(d.error || 'Update unavailable', {});
56110
+ }
56111
+ })
56112
+ .catch(function () { /* server may already be restarting */ });
56113
+ });
56072
56114
  }
56073
56115
  function dispatchStreamMessage(type, data) {
56074
56116
  if (type === 'realtime-state') {
@@ -62886,6 +62928,7 @@ var guiAppHtml = `<!doctype html>
62886
62928
  <span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
62887
62929
  <span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
62888
62930
  <span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
62931
+ <a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
62889
62932
  <button id="settings-gear" title="Settings" aria-label="Open settings">
62890
62933
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
62891
62934
  <circle cx="12" cy="12" r="3"/>
@@ -68096,6 +68139,28 @@ async function startGuiServer(options) {
68096
68139
  setActive(next, created.id);
68097
68140
  return created.id;
68098
68141
  };
68142
+ const cleanupWorkspaceFiles = (root6, ws) => {
68143
+ if (!ws.configPath && ws.kind === "local") {
68144
+ rmSync(workspaceDir(root6, ws.dir), { recursive: true, force: true });
68145
+ } else if (ws.kind === "cloud") {
68146
+ if (ws.configPath && existsSync25(ws.configPath)) {
68147
+ rmSync(ws.configPath, { force: true });
68148
+ }
68149
+ const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
68150
+ const label = labelMatch?.[1];
68151
+ if (label) {
68152
+ const stillUsed = listWorkspaces(root6).some(
68153
+ (w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
68154
+ );
68155
+ if (!stillUsed) {
68156
+ try {
68157
+ deleteDbCredential(label);
68158
+ } catch {
68159
+ }
68160
+ }
68161
+ }
68162
+ }
68163
+ };
68099
68164
  const handleVirginRoute = async (req, res, pathname, method) => {
68100
68165
  if (method === "GET" && pathname === "/") {
68101
68166
  sendText(
@@ -68147,6 +68212,35 @@ async function startGuiServer(options) {
68147
68212
  }
68148
68213
  return true;
68149
68214
  }
68215
+ if (method === "POST" && pathname === "/api/workspaces/delete") {
68216
+ if (!latticeRoot) {
68217
+ sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
68218
+ return true;
68219
+ }
68220
+ const body = await readJson(req);
68221
+ if (typeof body.id !== "string") {
68222
+ sendJson(res, { error: "id must be a string" }, 400);
68223
+ return true;
68224
+ }
68225
+ const ws = getWorkspace(latticeRoot, body.id);
68226
+ if (!ws) {
68227
+ sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
68228
+ return true;
68229
+ }
68230
+ removeWorkspace(latticeRoot, ws.id);
68231
+ try {
68232
+ cleanupWorkspaceFiles(latticeRoot, ws);
68233
+ } catch (e6) {
68234
+ sendJson(
68235
+ res,
68236
+ { error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
68237
+ 500
68238
+ );
68239
+ return true;
68240
+ }
68241
+ sendJson(res, { ok: true, switchedTo: null });
68242
+ return true;
68243
+ }
68150
68244
  if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
68151
68245
  await redeemInvite(createCloudWorkspace, req, res);
68152
68246
  return true;
@@ -68181,6 +68275,18 @@ async function startGuiServer(options) {
68181
68275
  );
68182
68276
  return;
68183
68277
  }
68278
+ if (method === "POST" && pathname === "/api/update/apply") {
68279
+ if (updateService) {
68280
+ void updateService.checkNow(true);
68281
+ sendJson(res, { ok: true, status: updateService.status() });
68282
+ } else {
68283
+ sendJson(res, {
68284
+ ok: false,
68285
+ error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
68286
+ });
68287
+ }
68288
+ return;
68289
+ }
68184
68290
  if (!activeRef) {
68185
68291
  if (await handleVirginRoute(req, res, pathname, method)) return;
68186
68292
  sendJson(res, { error: "No active workspace" }, 409);
@@ -69274,26 +69380,7 @@ async function startGuiServer(options) {
69274
69380
  }
69275
69381
  removeWorkspace(latticeRoot, ws.id);
69276
69382
  try {
69277
- if (!ws.configPath && ws.kind === "local") {
69278
- rmSync(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
69279
- } else if (ws.kind === "cloud") {
69280
- if (ws.configPath && existsSync25(ws.configPath)) {
69281
- rmSync(ws.configPath, { force: true });
69282
- }
69283
- const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
69284
- const label = labelMatch?.[1];
69285
- if (label) {
69286
- const stillUsed = listWorkspaces(latticeRoot).some(
69287
- (w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
69288
- );
69289
- if (!stillUsed) {
69290
- try {
69291
- deleteDbCredential(label);
69292
- } catch {
69293
- }
69294
- }
69295
- }
69296
- }
69383
+ cleanupWorkspaceFiles(latticeRoot, ws);
69297
69384
  } catch (e6) {
69298
69385
  sendJson(
69299
69386
  res,
@@ -69759,7 +69846,9 @@ ${e6.stack ?? ""}`
69759
69846
  }
69760
69847
  }
69761
69848
  };
69762
- if (options.selfUpdate && guiVersion) {
69849
+ if (options.updateServiceFactory) {
69850
+ updateService = options.updateServiceFactory(broadcast);
69851
+ } else if (options.selfUpdate && guiVersion) {
69763
69852
  updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
69764
69853
  }
69765
69854
  const handleEventStream = (ws) => {
@@ -70132,7 +70221,7 @@ function printHelp() {
70132
70221
  );
70133
70222
  }
70134
70223
  function getVersion() {
70135
- if (true) return "3.4.4";
70224
+ if (true) return "3.4.5";
70136
70225
  try {
70137
70226
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
70138
70227
  const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
package/dist/index.cjs CHANGED
@@ -4527,14 +4527,6 @@ function resolveDbPath(raw, configDir2) {
4527
4527
  }
4528
4528
  return (0, import_node_path11.resolve)(configDir2, raw);
4529
4529
  }
4530
- function warnDeprecatedRef(entity, field, target) {
4531
- const key = `${entity}.${field}`;
4532
- if (warnedDeprecatedRefs.has(key)) return;
4533
- warnedDeprecatedRefs.add(key);
4534
- console.warn(
4535
- `Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
4536
- );
4537
- }
4538
4530
  function entityToTableDef(entityName, entity) {
4539
4531
  const rawFields = entity.fields;
4540
4532
  if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
@@ -4561,7 +4553,6 @@ function entityToTableDef(entityName, entity) {
4561
4553
  table: field.ref,
4562
4554
  foreignKey: fieldName
4563
4555
  };
4564
- warnDeprecatedRef(entityName, fieldName, field.ref);
4565
4556
  }
4566
4557
  }
4567
4558
  const primaryKey = entity.primaryKey ?? pkFromField;
@@ -4718,7 +4709,7 @@ function parseEntityContexts(entityContexts) {
4718
4709
  }
4719
4710
  return result;
4720
4711
  }
4721
- var import_node_fs10, import_node_path11, import_yaml3, warnedDeprecatedRefs;
4712
+ var import_node_fs10, import_node_path11, import_yaml3;
4722
4713
  var init_parser = __esm({
4723
4714
  "src/config/parser.ts"() {
4724
4715
  "use strict";
@@ -4726,7 +4717,6 @@ var init_parser = __esm({
4726
4717
  import_node_path11 = require("path");
4727
4718
  import_yaml3 = require("yaml");
4728
4719
  init_user_config();
4729
- warnedDeprecatedRefs = /* @__PURE__ */ new Set();
4730
4720
  }
4731
4721
  });
4732
4722
 
@@ -55971,6 +55961,8 @@ var css = `
55971
55961
  .app-version:empty { display: none; }
55972
55962
  .app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
55973
55963
  .app-update[hidden] { display: none; }
55964
+ #app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
55965
+ #app-update-link[hidden] { display: none; }
55974
55966
  /* Unseen-change count next to a sidebar entity. */
55975
55967
  .nav-badge {
55976
55968
  display: inline-block; min-width: 16px; text-align: center;
@@ -57449,6 +57441,12 @@ var appJs = `
57449
57441
  // drag handle once the app has booted.
57450
57442
  var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
57451
57443
  if (!isNaN(savedRail)) applyRailWidth(savedRail);
57444
+ // The version chip + manual-upgrade link live in the static shell (present
57445
+ // from first paint, in both the normal and virgin-state boots), so wire the
57446
+ // click handler and run the first availability check here \u2014 independent of
57447
+ // the async workspace bootstrap. checkServerVersion() refreshes it later.
57448
+ wireUpdateLink();
57449
+ checkUpdateAvailable();
57452
57450
  // Failsafe: never leave the overlay up forever if a fetch hangs without
57453
57451
  // rejecting, or a future early-return (e.g. the virgin-state screen)
57454
57452
  // bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
@@ -57933,6 +57931,26 @@ var appJs = `
57933
57931
  showUpdatePill(label || 'Updated \u2014 reloading\u2026');
57934
57932
  setTimeout(function () { location.reload(); }, 600);
57935
57933
  }
57934
+ // Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
57935
+ // the version chip only when the server reports a newer, installable version.
57936
+ // The auto-updater installs in the background on its own cadence; this lets
57937
+ // the user force it now. Best-effort; the link stays hidden on any failure.
57938
+ function checkUpdateAvailable() {
57939
+ var el = document.getElementById('app-update-link');
57940
+ if (!el) return;
57941
+ fetch('/api/update/status')
57942
+ .then(function (r) { return r.ok ? r.json() : null; })
57943
+ .then(function (s) {
57944
+ if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
57945
+ el.textContent = 'Update available \u2014 Upgrade';
57946
+ el.title = 'Install v' + s.latest + ' and restart';
57947
+ el.hidden = false;
57948
+ } else {
57949
+ el.hidden = true;
57950
+ }
57951
+ })
57952
+ .catch(function () { /* best-effort \u2014 keep the link hidden */ });
57953
+ }
57936
57954
  // On every (re)connect, ask the server its version. A change vs BOOT_VERSION
57937
57955
  // means a relaunch onto new code \u2192 reload. Best-effort; never throws.
57938
57956
  function checkServerVersion() {
@@ -57946,6 +57964,31 @@ var appJs = `
57946
57964
  else hideUpdatePill();
57947
57965
  })
57948
57966
  .catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
57967
+ // Refresh the manual-upgrade link alongside the reconnect version check.
57968
+ checkUpdateAvailable();
57969
+ }
57970
+ // Wire the manual-upgrade link's click: kick off the install (the server
57971
+ // installs the latest and restarts onto it) and surface the progress. On
57972
+ // success we do nothing else \u2014 the update-applied event + the reconnect
57973
+ // version check land the page on the new version (no manual reload). A
57974
+ // false ok means the install can't run (unsupervised) \u2014 toast why.
57975
+ function wireUpdateLink() {
57976
+ var el = document.getElementById('app-update-link');
57977
+ if (!el) return;
57978
+ el.addEventListener('click', function (e) {
57979
+ e.preventDefault();
57980
+ el.hidden = true;
57981
+ showUpdatePill('Updating\u2026');
57982
+ fetch('/api/update/apply', { method: 'POST' })
57983
+ .then(function (r) { return r.json(); })
57984
+ .then(function (d) {
57985
+ if (d && d.ok === false) {
57986
+ hideUpdatePill();
57987
+ showToast(d.error || 'Update unavailable', {});
57988
+ }
57989
+ })
57990
+ .catch(function () { /* server may already be restarting */ });
57991
+ });
57949
57992
  }
57950
57993
  function dispatchStreamMessage(type, data) {
57951
57994
  if (type === 'realtime-state') {
@@ -64763,6 +64806,7 @@ var guiAppHtml = `<!doctype html>
64763
64806
  <span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
64764
64807
  <span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
64765
64808
  <span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
64809
+ <a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
64766
64810
  <button id="settings-gear" title="Settings" aria-label="Open settings">
64767
64811
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
64768
64812
  <circle cx="12" cy="12" r="3"/>
@@ -69305,6 +69349,28 @@ async function startGuiServer(options) {
69305
69349
  setActive(next, created.id);
69306
69350
  return created.id;
69307
69351
  };
69352
+ const cleanupWorkspaceFiles = (root6, ws) => {
69353
+ if (!ws.configPath && ws.kind === "local") {
69354
+ (0, import_node_fs35.rmSync)(workspaceDir(root6, ws.dir), { recursive: true, force: true });
69355
+ } else if (ws.kind === "cloud") {
69356
+ if (ws.configPath && (0, import_node_fs35.existsSync)(ws.configPath)) {
69357
+ (0, import_node_fs35.rmSync)(ws.configPath, { force: true });
69358
+ }
69359
+ const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
69360
+ const label = labelMatch?.[1];
69361
+ if (label) {
69362
+ const stillUsed = listWorkspaces(root6).some(
69363
+ (w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
69364
+ );
69365
+ if (!stillUsed) {
69366
+ try {
69367
+ deleteDbCredential(label);
69368
+ } catch {
69369
+ }
69370
+ }
69371
+ }
69372
+ }
69373
+ };
69308
69374
  const handleVirginRoute = async (req, res, pathname, method) => {
69309
69375
  if (method === "GET" && pathname === "/") {
69310
69376
  sendText(
@@ -69356,6 +69422,35 @@ async function startGuiServer(options) {
69356
69422
  }
69357
69423
  return true;
69358
69424
  }
69425
+ if (method === "POST" && pathname === "/api/workspaces/delete") {
69426
+ if (!latticeRoot) {
69427
+ sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
69428
+ return true;
69429
+ }
69430
+ const body = await readJson(req);
69431
+ if (typeof body.id !== "string") {
69432
+ sendJson(res, { error: "id must be a string" }, 400);
69433
+ return true;
69434
+ }
69435
+ const ws = getWorkspace(latticeRoot, body.id);
69436
+ if (!ws) {
69437
+ sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
69438
+ return true;
69439
+ }
69440
+ removeWorkspace(latticeRoot, ws.id);
69441
+ try {
69442
+ cleanupWorkspaceFiles(latticeRoot, ws);
69443
+ } catch (e6) {
69444
+ sendJson(
69445
+ res,
69446
+ { error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
69447
+ 500
69448
+ );
69449
+ return true;
69450
+ }
69451
+ sendJson(res, { ok: true, switchedTo: null });
69452
+ return true;
69453
+ }
69359
69454
  if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
69360
69455
  await redeemInvite(createCloudWorkspace, req, res);
69361
69456
  return true;
@@ -69390,6 +69485,18 @@ async function startGuiServer(options) {
69390
69485
  );
69391
69486
  return;
69392
69487
  }
69488
+ if (method === "POST" && pathname === "/api/update/apply") {
69489
+ if (updateService) {
69490
+ void updateService.checkNow(true);
69491
+ sendJson(res, { ok: true, status: updateService.status() });
69492
+ } else {
69493
+ sendJson(res, {
69494
+ ok: false,
69495
+ error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
69496
+ });
69497
+ }
69498
+ return;
69499
+ }
69393
69500
  if (!activeRef) {
69394
69501
  if (await handleVirginRoute(req, res, pathname, method)) return;
69395
69502
  sendJson(res, { error: "No active workspace" }, 409);
@@ -70483,26 +70590,7 @@ async function startGuiServer(options) {
70483
70590
  }
70484
70591
  removeWorkspace(latticeRoot, ws.id);
70485
70592
  try {
70486
- if (!ws.configPath && ws.kind === "local") {
70487
- (0, import_node_fs35.rmSync)(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
70488
- } else if (ws.kind === "cloud") {
70489
- if (ws.configPath && (0, import_node_fs35.existsSync)(ws.configPath)) {
70490
- (0, import_node_fs35.rmSync)(ws.configPath, { force: true });
70491
- }
70492
- const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
70493
- const label = labelMatch?.[1];
70494
- if (label) {
70495
- const stillUsed = listWorkspaces(latticeRoot).some(
70496
- (w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
70497
- );
70498
- if (!stillUsed) {
70499
- try {
70500
- deleteDbCredential(label);
70501
- } catch {
70502
- }
70503
- }
70504
- }
70505
- }
70593
+ cleanupWorkspaceFiles(latticeRoot, ws);
70506
70594
  } catch (e6) {
70507
70595
  sendJson(
70508
70596
  res,
@@ -70968,7 +71056,9 @@ ${e6.stack ?? ""}`
70968
71056
  }
70969
71057
  }
70970
71058
  };
70971
- if (options.selfUpdate && guiVersion) {
71059
+ if (options.updateServiceFactory) {
71060
+ updateService = options.updateServiceFactory(broadcast);
71061
+ } else if (options.selfUpdate && guiVersion) {
70972
71062
  updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
70973
71063
  }
70974
71064
  const handleEventStream = (ws) => {
package/dist/index.d.cts CHANGED
@@ -4923,6 +4923,37 @@ interface PdfOptions {
4923
4923
  */
4924
4924
  declare function describePdf(auth: ClaudeAuth, path: string, opts?: PdfOptions): Promise<string>;
4925
4925
 
4926
+ /** How the running copy of the package was installed. */
4927
+ type InstallKind = 'global' | 'local' | 'npx' | 'linked-dev' | 'unknown';
4928
+ interface InstallContext {
4929
+ kind: InstallKind;
4930
+ /** True only when an `npm install` may safely upgrade this copy in place. */
4931
+ installable: boolean;
4932
+ /** Directory to run the install from (the consumer project root for `local`). */
4933
+ cwd: string;
4934
+ /** Resolved package root of the running copy, if found. */
4935
+ packageRoot: string | null;
4936
+ /** Human-readable explanation (logged / surfaced in `/api/update/status`). */
4937
+ reason: string;
4938
+ }
4939
+
4940
+ interface UpdateStatus {
4941
+ current: string;
4942
+ latest: string | null;
4943
+ kind: InstallContext['kind'];
4944
+ installable: boolean;
4945
+ checking: boolean;
4946
+ installing: boolean;
4947
+ lastError: string | null;
4948
+ }
4949
+ interface UpdateService {
4950
+ start(): void;
4951
+ stop(): void;
4952
+ status(): UpdateStatus;
4953
+ /** Run a check now (and install if applicable). Returns the resulting status. */
4954
+ checkNow(force?: boolean): Promise<UpdateStatus>;
4955
+ }
4956
+
4926
4957
  interface StartGuiServerOptions {
4927
4958
  /**
4928
4959
  * Active workspace config to open. NULL/empty ⇒ boot into the zero-workspace
@@ -4979,6 +5010,13 @@ interface StartGuiServerOptions {
4979
5010
  * `GET /api/version` + `GET /api/update/status` are served regardless.
4980
5011
  */
4981
5012
  selfUpdate?: boolean;
5013
+ /**
5014
+ * Test seam: supply the update service instead of building one from the real
5015
+ * npm-backed install context. Lets tests exercise the update routes against a
5016
+ * deterministic fake (no real registry check, no real npm install). When set,
5017
+ * it overrides `selfUpdate`'s default factory.
5018
+ */
5019
+ updateServiceFactory?: (emit: (type: string, data: unknown) => void) => UpdateService;
4982
5020
  }
4983
5021
  interface GuiServerHandle {
4984
5022
  server: Server;
package/dist/index.d.ts CHANGED
@@ -4923,6 +4923,37 @@ interface PdfOptions {
4923
4923
  */
4924
4924
  declare function describePdf(auth: ClaudeAuth, path: string, opts?: PdfOptions): Promise<string>;
4925
4925
 
4926
+ /** How the running copy of the package was installed. */
4927
+ type InstallKind = 'global' | 'local' | 'npx' | 'linked-dev' | 'unknown';
4928
+ interface InstallContext {
4929
+ kind: InstallKind;
4930
+ /** True only when an `npm install` may safely upgrade this copy in place. */
4931
+ installable: boolean;
4932
+ /** Directory to run the install from (the consumer project root for `local`). */
4933
+ cwd: string;
4934
+ /** Resolved package root of the running copy, if found. */
4935
+ packageRoot: string | null;
4936
+ /** Human-readable explanation (logged / surfaced in `/api/update/status`). */
4937
+ reason: string;
4938
+ }
4939
+
4940
+ interface UpdateStatus {
4941
+ current: string;
4942
+ latest: string | null;
4943
+ kind: InstallContext['kind'];
4944
+ installable: boolean;
4945
+ checking: boolean;
4946
+ installing: boolean;
4947
+ lastError: string | null;
4948
+ }
4949
+ interface UpdateService {
4950
+ start(): void;
4951
+ stop(): void;
4952
+ status(): UpdateStatus;
4953
+ /** Run a check now (and install if applicable). Returns the resulting status. */
4954
+ checkNow(force?: boolean): Promise<UpdateStatus>;
4955
+ }
4956
+
4926
4957
  interface StartGuiServerOptions {
4927
4958
  /**
4928
4959
  * Active workspace config to open. NULL/empty ⇒ boot into the zero-workspace
@@ -4979,6 +5010,13 @@ interface StartGuiServerOptions {
4979
5010
  * `GET /api/version` + `GET /api/update/status` are served regardless.
4980
5011
  */
4981
5012
  selfUpdate?: boolean;
5013
+ /**
5014
+ * Test seam: supply the update service instead of building one from the real
5015
+ * npm-backed install context. Lets tests exercise the update routes against a
5016
+ * deterministic fake (no real registry check, no real npm install). When set,
5017
+ * it overrides `selfUpdate`'s default factory.
5018
+ */
5019
+ updateServiceFactory?: (emit: (type: string, data: unknown) => void) => UpdateService;
4982
5020
  }
4983
5021
  interface GuiServerHandle {
4984
5022
  server: Server;
package/dist/index.js CHANGED
@@ -4531,14 +4531,6 @@ function resolveDbPath(raw, configDir2) {
4531
4531
  }
4532
4532
  return resolve2(configDir2, raw);
4533
4533
  }
4534
- function warnDeprecatedRef(entity, field, target) {
4535
- const key = `${entity}.${field}`;
4536
- if (warnedDeprecatedRefs.has(key)) return;
4537
- warnedDeprecatedRefs.add(key);
4538
- console.warn(
4539
- `Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
4540
- );
4541
- }
4542
4534
  function entityToTableDef(entityName, entity) {
4543
4535
  const rawFields = entity.fields;
4544
4536
  if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
@@ -4565,7 +4557,6 @@ function entityToTableDef(entityName, entity) {
4565
4557
  table: field.ref,
4566
4558
  foreignKey: fieldName
4567
4559
  };
4568
- warnDeprecatedRef(entityName, fieldName, field.ref);
4569
4560
  }
4570
4561
  }
4571
4562
  const primaryKey = entity.primaryKey ?? pkFromField;
@@ -4722,12 +4713,10 @@ function parseEntityContexts(entityContexts) {
4722
4713
  }
4723
4714
  return result;
4724
4715
  }
4725
- var warnedDeprecatedRefs;
4726
4716
  var init_parser = __esm({
4727
4717
  "src/config/parser.ts"() {
4728
4718
  "use strict";
4729
4719
  init_user_config();
4730
- warnedDeprecatedRefs = /* @__PURE__ */ new Set();
4731
4720
  }
4732
4721
  });
4733
4722
 
@@ -55797,6 +55786,8 @@ var css = `
55797
55786
  .app-version:empty { display: none; }
55798
55787
  .app-update { flex: 0 0 auto; color: var(--accent, #4a9); font-size: 12px; white-space: nowrap; }
55799
55788
  .app-update[hidden] { display: none; }
55789
+ #app-update-link { flex: 0 0 auto; margin-left: 8px; color: var(--accent, #4a9); font-size: 12px; cursor: pointer; white-space: nowrap; }
55790
+ #app-update-link[hidden] { display: none; }
55800
55791
  /* Unseen-change count next to a sidebar entity. */
55801
55792
  .nav-badge {
55802
55793
  display: inline-block; min-width: 16px; text-align: center;
@@ -57275,6 +57266,12 @@ var appJs = `
57275
57266
  // drag handle once the app has booted.
57276
57267
  var savedRail = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
57277
57268
  if (!isNaN(savedRail)) applyRailWidth(savedRail);
57269
+ // The version chip + manual-upgrade link live in the static shell (present
57270
+ // from first paint, in both the normal and virgin-state boots), so wire the
57271
+ // click handler and run the first availability check here \u2014 independent of
57272
+ // the async workspace bootstrap. checkServerVersion() refreshes it later.
57273
+ wireUpdateLink();
57274
+ checkUpdateAvailable();
57278
57275
  // Failsafe: never leave the overlay up forever if a fetch hangs without
57279
57276
  // rejecting, or a future early-return (e.g. the virgin-state screen)
57280
57277
  // bypasses the .then() tail. Idempotent, so a later real hide is a no-op.
@@ -57759,6 +57756,26 @@ var appJs = `
57759
57756
  showUpdatePill(label || 'Updated \u2014 reloading\u2026');
57760
57757
  setTimeout(function () { location.reload(); }, 600);
57761
57758
  }
57759
+ // Manual upgrade fallback: show an "Update available \u2014 Upgrade" link next to
57760
+ // the version chip only when the server reports a newer, installable version.
57761
+ // The auto-updater installs in the background on its own cadence; this lets
57762
+ // the user force it now. Best-effort; the link stays hidden on any failure.
57763
+ function checkUpdateAvailable() {
57764
+ var el = document.getElementById('app-update-link');
57765
+ if (!el) return;
57766
+ fetch('/api/update/status')
57767
+ .then(function (r) { return r.ok ? r.json() : null; })
57768
+ .then(function (s) {
57769
+ if (s && s.latest && s.current && s.latest !== s.current && s.installable) {
57770
+ el.textContent = 'Update available \u2014 Upgrade';
57771
+ el.title = 'Install v' + s.latest + ' and restart';
57772
+ el.hidden = false;
57773
+ } else {
57774
+ el.hidden = true;
57775
+ }
57776
+ })
57777
+ .catch(function () { /* best-effort \u2014 keep the link hidden */ });
57778
+ }
57762
57779
  // On every (re)connect, ask the server its version. A change vs BOOT_VERSION
57763
57780
  // means a relaunch onto new code \u2192 reload. Best-effort; never throws.
57764
57781
  function checkServerVersion() {
@@ -57772,6 +57789,31 @@ var appJs = `
57772
57789
  else hideUpdatePill();
57773
57790
  })
57774
57791
  .catch(function () { /* offline / mid-restart \u2014 the next reconnect retries */ });
57792
+ // Refresh the manual-upgrade link alongside the reconnect version check.
57793
+ checkUpdateAvailable();
57794
+ }
57795
+ // Wire the manual-upgrade link's click: kick off the install (the server
57796
+ // installs the latest and restarts onto it) and surface the progress. On
57797
+ // success we do nothing else \u2014 the update-applied event + the reconnect
57798
+ // version check land the page on the new version (no manual reload). A
57799
+ // false ok means the install can't run (unsupervised) \u2014 toast why.
57800
+ function wireUpdateLink() {
57801
+ var el = document.getElementById('app-update-link');
57802
+ if (!el) return;
57803
+ el.addEventListener('click', function (e) {
57804
+ e.preventDefault();
57805
+ el.hidden = true;
57806
+ showUpdatePill('Updating\u2026');
57807
+ fetch('/api/update/apply', { method: 'POST' })
57808
+ .then(function (r) { return r.json(); })
57809
+ .then(function (d) {
57810
+ if (d && d.ok === false) {
57811
+ hideUpdatePill();
57812
+ showToast(d.error || 'Update unavailable', {});
57813
+ }
57814
+ })
57815
+ .catch(function () { /* server may already be restarting */ });
57816
+ });
57775
57817
  }
57776
57818
  function dispatchStreamMessage(type, data) {
57777
57819
  if (type === 'realtime-state') {
@@ -64589,6 +64631,7 @@ var guiAppHtml = `<!doctype html>
64589
64631
  <span class="offline-pill" id="offline-pill" title="Edits queued offline \u2014 will sync when the cloud reconnects" hidden></span>
64590
64632
  <span class="app-update" id="app-update" title="A new version is being applied" hidden></span>
64591
64633
  <span class="app-version" id="app-version" title="Lattice version"><!--LATTICE_VERSION--></span>
64634
+ <a id="app-update-link" href="#" hidden>Update available \u2014 Upgrade</a>
64592
64635
  <button id="settings-gear" title="Settings" aria-label="Open settings">
64593
64636
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
64594
64637
  <circle cx="12" cy="12" r="3"/>
@@ -69130,6 +69173,28 @@ async function startGuiServer(options) {
69130
69173
  setActive(next, created.id);
69131
69174
  return created.id;
69132
69175
  };
69176
+ const cleanupWorkspaceFiles = (root6, ws) => {
69177
+ if (!ws.configPath && ws.kind === "local") {
69178
+ rmSync(workspaceDir(root6, ws.dir), { recursive: true, force: true });
69179
+ } else if (ws.kind === "cloud") {
69180
+ if (ws.configPath && existsSync24(ws.configPath)) {
69181
+ rmSync(ws.configPath, { force: true });
69182
+ }
69183
+ const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
69184
+ const label = labelMatch?.[1];
69185
+ if (label) {
69186
+ const stillUsed = listWorkspaces(root6).some(
69187
+ (w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
69188
+ );
69189
+ if (!stillUsed) {
69190
+ try {
69191
+ deleteDbCredential(label);
69192
+ } catch {
69193
+ }
69194
+ }
69195
+ }
69196
+ }
69197
+ };
69133
69198
  const handleVirginRoute = async (req, res, pathname, method) => {
69134
69199
  if (method === "GET" && pathname === "/") {
69135
69200
  sendText(
@@ -69181,6 +69246,35 @@ async function startGuiServer(options) {
69181
69246
  }
69182
69247
  return true;
69183
69248
  }
69249
+ if (method === "POST" && pathname === "/api/workspaces/delete") {
69250
+ if (!latticeRoot) {
69251
+ sendJson(res, { error: "No .lattice root \u2014 workspaces unavailable" }, 400);
69252
+ return true;
69253
+ }
69254
+ const body = await readJson(req);
69255
+ if (typeof body.id !== "string") {
69256
+ sendJson(res, { error: "id must be a string" }, 400);
69257
+ return true;
69258
+ }
69259
+ const ws = getWorkspace(latticeRoot, body.id);
69260
+ if (!ws) {
69261
+ sendJson(res, { error: `No workspace with id ${body.id}` }, 400);
69262
+ return true;
69263
+ }
69264
+ removeWorkspace(latticeRoot, ws.id);
69265
+ try {
69266
+ cleanupWorkspaceFiles(latticeRoot, ws);
69267
+ } catch (e6) {
69268
+ sendJson(
69269
+ res,
69270
+ { error: `Workspace unregistered but file cleanup failed: ${e6.message}` },
69271
+ 500
69272
+ );
69273
+ return true;
69274
+ }
69275
+ sendJson(res, { ok: true, switchedTo: null });
69276
+ return true;
69277
+ }
69184
69278
  if (method === "POST" && pathname === "/api/cloud/redeem-invite") {
69185
69279
  await redeemInvite(createCloudWorkspace, req, res);
69186
69280
  return true;
@@ -69215,6 +69309,18 @@ async function startGuiServer(options) {
69215
69309
  );
69216
69310
  return;
69217
69311
  }
69312
+ if (method === "POST" && pathname === "/api/update/apply") {
69313
+ if (updateService) {
69314
+ void updateService.checkNow(true);
69315
+ sendJson(res, { ok: true, status: updateService.status() });
69316
+ } else {
69317
+ sendJson(res, {
69318
+ ok: false,
69319
+ error: "Automatic update is not available for this install. Reinstall from https://latticesql.com to get the latest version."
69320
+ });
69321
+ }
69322
+ return;
69323
+ }
69218
69324
  if (!activeRef) {
69219
69325
  if (await handleVirginRoute(req, res, pathname, method)) return;
69220
69326
  sendJson(res, { error: "No active workspace" }, 409);
@@ -70308,26 +70414,7 @@ async function startGuiServer(options) {
70308
70414
  }
70309
70415
  removeWorkspace(latticeRoot, ws.id);
70310
70416
  try {
70311
- if (!ws.configPath && ws.kind === "local") {
70312
- rmSync(workspaceDir(latticeRoot, ws.dir), { recursive: true, force: true });
70313
- } else if (ws.kind === "cloud") {
70314
- if (ws.configPath && existsSync24(ws.configPath)) {
70315
- rmSync(ws.configPath, { force: true });
70316
- }
70317
- const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(ws.db.trim());
70318
- const label = labelMatch?.[1];
70319
- if (label) {
70320
- const stillUsed = listWorkspaces(latticeRoot).some(
70321
- (w2) => w2.db.includes("${LATTICE_DB:" + label + "}")
70322
- );
70323
- if (!stillUsed) {
70324
- try {
70325
- deleteDbCredential(label);
70326
- } catch {
70327
- }
70328
- }
70329
- }
70330
- }
70417
+ cleanupWorkspaceFiles(latticeRoot, ws);
70331
70418
  } catch (e6) {
70332
70419
  sendJson(
70333
70420
  res,
@@ -70793,7 +70880,9 @@ ${e6.stack ?? ""}`
70793
70880
  }
70794
70881
  }
70795
70882
  };
70796
- if (options.selfUpdate && guiVersion) {
70883
+ if (options.updateServiceFactory) {
70884
+ updateService = options.updateServiceFactory(broadcast);
70885
+ } else if (options.selfUpdate && guiVersion) {
70797
70886
  updateService = createUpdateService({ currentVersion: guiVersion, emit: broadcast });
70798
70887
  }
70799
70888
  const handleEventStream = (ws) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "3.4.4",
3
+ "version": "3.4.5",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",