latticesql 1.13.0 → 1.13.2

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/README.md CHANGED
@@ -2066,12 +2066,25 @@ npx lattice gui --config ./lattice.config.yml --output ./context --port 4317
2066
2066
 
2067
2067
  **Options**
2068
2068
 
2069
- | Flag | Default | Description |
2070
- | --------------------- | ---------------------- | ----------------------------------------------------- |
2071
- | `--config, -c <path>` | `./lattice.config.yml` | Path to the config file |
2072
- | `--output <dir>` | `./context` | Output directory (used by the relationship graph) |
2073
- | `--port <number>` | `4317` | Localhost port; auto-increments when the port is busy |
2074
- | `--no-open` | off | Print the URL without opening a browser |
2069
+ | Flag | Default | Description |
2070
+ | --------------------- | ---------------------- | -------------------------------------------------------- |
2071
+ | `--config, -c <path>` | `./lattice.config.yml` | Path to the config file |
2072
+ | `--output <dir>` | (auto-detected) | Output directory containing rendered context see below |
2073
+ | `--port <number>` | `4317` | Localhost port; auto-increments when the port is busy |
2074
+ | `--no-open` | off | Print the URL without opening a browser |
2075
+
2076
+ **Output-directory auto-detection (v1.13.1+).** When `--output` is not passed explicitly, the GUI probes `./context`, `.`, and `./generated` in order and uses the first directory containing a `.lattice/manifest.json` (announced via a one-line `auto-detected rendered context at "<dir>"` log on stdout). Projects whose `lattice render` writes into the project root no longer need to pass `--output .` every time. An explicit `--output` is always honoured.
2077
+
2078
+ **Entity-context discovery (v1.13.1+).** The Database panel's row-context viewer reads entity contexts from two layered sources so it works regardless of how you register them:
2079
+
2080
+ 1. **Live Lattice schema** — anything declared in `lattice.config.yml` or added programmatically via `db.defineEntityContext()` against the active Lattice. Exposed via the new public `Lattice.entityContexts()` accessor.
2081
+ 2. **Render manifest fallback** — when a table has no schema-registered entity context but the on-disk `.lattice/manifest.json` names it (typical for projects that register entity contexts in a JS / TS module like `lattice.schema.mjs` that the GUI process never imports), the GUI derives the row → slug mapping heuristically from `row.slug` / `row.id` / `row.name` and surfaces the rendered files anyway.
2082
+
2083
+ The convergence means you don't need to duplicate entity-context definitions in YAML for the GUI to find rendered files.
2084
+
2085
+ **Database wizard form (v1.13.2+).** The Postgres connection form (used by Migrate to cloud + Connect to existing cloud) disables browser autocapitalize, autocorrect, and spellcheck on every text input, and trims whitespace on every read. This avoids silent failure modes where macOS Safari / iOS turned a Supabase tenant user `postgres.<ref>` into `Postgres.<ref>` on submit, and where pasted credentials carrying a trailing newline produced opaque "zero-length delimiter identifier" or SCRAM-mismatch errors. `probeCloud` also folds SQLSTATE + `routine` into `result.error` so the GUI's "Unreachable: …" surface is actionable.
2086
+
2087
+ **Switch vs. migrate (v1.13.2+ wording).** "Connect to existing cloud" _switches_ the project's `db:` line to point at the cloud; the local SQLite file stays on disk and you can switch back by editing the YAML or via the Databases catalog under User Config. Use "Migrate to cloud" only when you want to _push_ the local data into a fresh empty target.
2075
2088
 
2076
2089
  **Views**
2077
2090
 
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/cli.ts
10
- import { resolve as resolve8, dirname as dirname7 } from "path";
10
+ import { resolve as resolve9, dirname as dirname7 } from "path";
11
11
  import { readFileSync as readFileSync12 } from "fs";
12
12
  import { execSync } from "child_process";
13
13
  import { parse as parse2 } from "yaml";
@@ -3369,6 +3369,16 @@ var Lattice = class {
3369
3369
  this._schema.defineEntityContext(table, def);
3370
3370
  return this;
3371
3371
  }
3372
+ /**
3373
+ * All entity contexts currently registered on this Lattice — both those
3374
+ * declared in `lattice.config.yml` and those added programmatically via
3375
+ * `defineEntityContext()`.
3376
+ *
3377
+ * Returns a defensive copy so callers can't mutate the schema.
3378
+ */
3379
+ entityContexts() {
3380
+ return new Map(this._schema.getEntityContexts());
3381
+ }
3372
3382
  /**
3373
3383
  * Register a write hook that fires after insert/update/delete operations.
3374
3384
  * Hooks run synchronously after the DB write and audit emit.
@@ -5358,8 +5368,13 @@ var guiAppHtml = `<!doctype html>
5358
5368
  }
5359
5369
 
5360
5370
  /* \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5371
+ /* minmax(0, 1fr) on the content track lets a wide child (a table with
5372
+ chip-heavy cells) shrink instead of forcing the page wider than the
5373
+ viewport. Without the explicit 0 lower bound, the implicit auto
5374
+ minimum keeps the track at content-width and the whole page scrolls
5375
+ horizontally. */
5361
5376
  .layout {
5362
- display: grid; grid-template-columns: 220px 1fr;
5377
+ display: grid; grid-template-columns: 220px minmax(0, 1fr);
5363
5378
  height: calc(100vh - 56px);
5364
5379
  }
5365
5380
  nav.sidebar {
@@ -5431,6 +5446,19 @@ var guiAppHtml = `<!doctype html>
5431
5446
  tbody tr { cursor: pointer; }
5432
5447
  tbody tr:hover td { background: var(--row-hover); }
5433
5448
  td.muted { color: var(--text-muted); }
5449
+ /* Row cells truncate at 3 lines so a row with many chips or a long text
5450
+ blob stays one consistent visual height instead of wrapping into a
5451
+ paragraph. The wrapping <div class="cell-clip"> is necessary because
5452
+ -webkit-line-clamp doesn't apply to <td> directly in all engines. */
5453
+ td .cell-clip {
5454
+ display: -webkit-box;
5455
+ -webkit-line-clamp: 3;
5456
+ -webkit-box-orient: vertical;
5457
+ overflow: hidden;
5458
+ line-height: 1.45;
5459
+ max-height: calc(1.45em * 3);
5460
+ word-break: break-word;
5461
+ }
5434
5462
  .chip {
5435
5463
  display: inline-block; padding: 2px 8px; margin: 1px 3px 1px 0;
5436
5464
  background: var(--accent-soft); color: var(--accent);
@@ -6390,11 +6418,11 @@ var guiAppHtml = `<!doctype html>
6390
6418
  if (isSecretColumn(tableName, c) && r[c] != null && r[c] !== '') {
6391
6419
  return '<td class="muted">' + SECRET_MASK + '</td>';
6392
6420
  }
6393
- return '<td>' + escapeHtml(truncate(r[c], 120)) + '</td>';
6421
+ return '<td><div class="cell-clip">' + escapeHtml(truncate(r[c], 120)) + '</div></td>';
6394
6422
  });
6395
6423
  belongsTo.forEach(function (b) {
6396
6424
  var ref = (loadedTables[b.rel.table] || []).find(function (x) { return x.id === r[b.rel.foreignKey]; });
6397
- tds.push('<td>' + chipLink(b.rel.table, ref) + '</td>');
6425
+ tds.push('<td><div class="cell-clip">' + chipLink(b.rel.table, ref) + '</div></td>');
6398
6426
  });
6399
6427
  junctions.forEach(function (j) {
6400
6428
  var matches = (loadedTables[j.junction] || []).filter(function (jr) { return jr[j.localFk] === r.id; });
@@ -6403,7 +6431,7 @@ var guiAppHtml = `<!doctype html>
6403
6431
  var ref = (loadedTables[j.remoteRel.table] || []).find(function (x) { return x.id === jr[remoteFkCol]; });
6404
6432
  return ref ? chipLink(j.remoteRel.table, ref) : '';
6405
6433
  }).join('');
6406
- tds.push('<td>' + (chips || '<span class="muted">\u2014</span>') + '</td>');
6434
+ tds.push('<td><div class="cell-clip">' + (chips || '<span class="muted">\u2014</span>') + '</div></td>');
6407
6435
  });
6408
6436
  if (viewMode === 'trash') {
6409
6437
  tds.push('<td class="row-actions">' +
@@ -7971,27 +7999,39 @@ var guiAppHtml = `<!doctype html>
7971
7999
 
7972
8000
  function postgresFormHtml(prefill) {
7973
8001
  prefill = prefill || {};
8002
+ // autocapitalize="off" + autocorrect="off" + spellcheck="false" keep
8003
+ // mobile / macOS keyboards from "helpfully" capitalizing the first
8004
+ // letter of usernames + host fragments. Supabase tenant users
8005
+ // (postgres.<ref>) are case-sensitive and silently failed
8006
+ // authentication when iOS Safari turned the leading "p" into "P".
8007
+ var attrs = ' autocapitalize="off" autocorrect="off" spellcheck="false"';
7974
8008
  return (
7975
8009
  '<div class="grid" style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">' +
7976
- '<div><label class="field-label">Label</label><input type="text" id="w-label" placeholder="atlas" value="' + escapeHtml(prefill.label || '') + '" style="width:100%"></div>' +
7977
- '<div><label class="field-label">Host</label><input type="text" id="w-host" placeholder="db.example.com" value="' + escapeHtml(prefill.host || '') + '" style="width:100%"></div>' +
8010
+ '<div><label class="field-label">Label</label><input type="text" id="w-label" placeholder="atlas" value="' + escapeHtml(prefill.label || '') + '" style="width:100%"' + attrs + '></div>' +
8011
+ '<div><label class="field-label">Host</label><input type="text" id="w-host" placeholder="db.example.com" value="' + escapeHtml(prefill.host || '') + '" style="width:100%"' + attrs + '></div>' +
7978
8012
  '<div><label class="field-label">Port</label><input type="number" id="w-port" placeholder="5432" value="' + escapeHtml(String(prefill.port || 5432)) + '" style="width:100%"></div>' +
7979
- '<div><label class="field-label">Database name</label><input type="text" id="w-dbname" placeholder="app" value="' + escapeHtml(prefill.dbname || '') + '" style="width:100%"></div>' +
7980
- '<div><label class="field-label">User</label><input type="text" id="w-user" placeholder="lattice_user" value="' + escapeHtml(prefill.user || '') + '" style="width:100%"></div>' +
7981
- '<div><label class="field-label">Password</label><input type="password" id="w-password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" style="width:100%"></div>' +
8013
+ '<div><label class="field-label">Database name</label><input type="text" id="w-dbname" placeholder="app" value="' + escapeHtml(prefill.dbname || '') + '" style="width:100%"' + attrs + '></div>' +
8014
+ '<div><label class="field-label">User</label><input type="text" id="w-user" placeholder="lattice_user" value="' + escapeHtml(prefill.user || '') + '" style="width:100%"' + attrs + '></div>' +
8015
+ '<div><label class="field-label">Password</label><input type="password" id="w-password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" style="width:100%"' + attrs + '></div>' +
7982
8016
  '</div>'
7983
8017
  );
7984
8018
  }
7985
8019
 
7986
8020
  function readPostgresWizardForm() {
8021
+ // Every text field is trimmed \u2014 pasted credentials frequently carry a
8022
+ // trailing newline or leading space that breaks URL construction
8023
+ // (zero-length identifier errors from the Postgres parser) or SCRAM
8024
+ // auth (silent password mismatch). Trim once, here, so every caller
8025
+ // benefits.
8026
+ var get = function (id) { return (document.getElementById(id).value || '').trim(); };
7987
8027
  return {
7988
8028
  type: 'postgres',
7989
- label: (document.getElementById('w-label').value || '').trim(),
7990
- host: (document.getElementById('w-host').value || '').trim(),
8029
+ label: get('w-label'),
8030
+ host: get('w-host'),
7991
8031
  port: Number(document.getElementById('w-port').value || 5432),
7992
- dbname: (document.getElementById('w-dbname').value || '').trim(),
7993
- user: document.getElementById('w-user').value || '',
7994
- password: document.getElementById('w-password').value || '',
8032
+ dbname: get('w-dbname'),
8033
+ user: get('w-user'),
8034
+ password: get('w-password'),
7995
8035
  };
7996
8036
  }
7997
8037
 
@@ -8026,10 +8066,14 @@ var guiAppHtml = `<!doctype html>
8026
8066
  function showConnectExistingModal(onClose) {
8027
8067
  var bodyHtml =
8028
8068
  '<p style="margin:0 0 12px;font-size:13px;color:var(--text-muted)">' +
8029
- 'Connect this project to an <strong>existing</strong> cloud Postgres. ' +
8030
- 'Your local SQLite data will be ignored \u2014 use Migrate to cloud instead ' +
8031
- 'if you want to push it. If the target is a teams DB you\\'ll be asked ' +
8032
- 'for an invite token after the probe.' +
8069
+ 'Switch this project to an <strong>existing</strong> cloud Postgres. ' +
8070
+ 'Your local SQLite file is preserved \u2014 only this project\\'s active ' +
8071
+ 'connection changes. Switch back any time by editing ' +
8072
+ '<code>lattice.config.yml</code>\\'s <code>db:</code> line or via the ' +
8073
+ 'Databases catalog under User Config. If you want to <em>push</em> ' +
8074
+ 'your local rows into the target instead, use Migrate to cloud. If ' +
8075
+ 'the target is a teams DB you\\'ll be asked for an invite token ' +
8076
+ 'after the probe.' +
8033
8077
  '</p>' +
8034
8078
  postgresFormHtml({}) +
8035
8079
  '<div id="w-team-zone" style="margin-top:10px"></div>' +
@@ -8703,7 +8747,7 @@ function sendJson(res, body, status = 200) {
8703
8747
  res.end(JSON.stringify(body));
8704
8748
  }
8705
8749
  function readJson(req) {
8706
- return new Promise((resolve9, reject) => {
8750
+ return new Promise((resolve10, reject) => {
8707
8751
  let raw = "";
8708
8752
  req.setEncoding("utf8");
8709
8753
  req.on("data", (chunk) => {
@@ -8712,7 +8756,7 @@ function readJson(req) {
8712
8756
  });
8713
8757
  req.on("end", () => {
8714
8758
  try {
8715
- resolve9(raw ? JSON.parse(raw) : {});
8759
+ resolve10(raw ? JSON.parse(raw) : {});
8716
8760
  } catch (e) {
8717
8761
  reject(new Error(`Invalid JSON body: ${e.message}`));
8718
8762
  }
@@ -9720,11 +9764,18 @@ async function probeCloud(targetUrl) {
9720
9764
  }
9721
9765
  return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
9722
9766
  } catch (e) {
9767
+ const err = e;
9768
+ const parts = [];
9769
+ if (err.code) parts.push(`[${err.code}]`);
9770
+ if (err.message) parts.push(err.message);
9771
+ if (err.routine && !err.message.includes(err.routine)) {
9772
+ parts.push(`(routine: ${err.routine})`);
9773
+ }
9723
9774
  return {
9724
9775
  reachable: false,
9725
9776
  dialect,
9726
9777
  teamEnabled: false,
9727
- error: e.message
9778
+ error: parts.join(" ") || "unknown"
9728
9779
  };
9729
9780
  } finally {
9730
9781
  if (probe) {
@@ -10403,7 +10454,7 @@ function sendJson2(res, body, status = 200) {
10403
10454
  res.end(JSON.stringify(body));
10404
10455
  }
10405
10456
  function readJson2(req) {
10406
- return new Promise((resolve9, reject) => {
10457
+ return new Promise((resolve10, reject) => {
10407
10458
  let raw = "";
10408
10459
  req.setEncoding("utf8");
10409
10460
  req.on("data", (chunk) => {
@@ -10412,7 +10463,7 @@ function readJson2(req) {
10412
10463
  });
10413
10464
  req.on("end", () => {
10414
10465
  try {
10415
- resolve9(raw ? JSON.parse(raw) : {});
10466
+ resolve10(raw ? JSON.parse(raw) : {});
10416
10467
  } catch (e) {
10417
10468
  reject(new Error(`Invalid JSON body: ${e.message}`));
10418
10469
  }
@@ -10676,7 +10727,7 @@ function sendJson3(res, body, status = 200) {
10676
10727
  res.end(JSON.stringify(body));
10677
10728
  }
10678
10729
  function readJson3(req) {
10679
- return new Promise((resolve9, reject) => {
10730
+ return new Promise((resolve10, reject) => {
10680
10731
  let raw = "";
10681
10732
  req.setEncoding("utf8");
10682
10733
  req.on("data", (chunk) => {
@@ -10685,7 +10736,7 @@ function readJson3(req) {
10685
10736
  });
10686
10737
  req.on("end", () => {
10687
10738
  try {
10688
- resolve9(raw ? JSON.parse(raw) : {});
10739
+ resolve10(raw ? JSON.parse(raw) : {});
10689
10740
  } catch (e) {
10690
10741
  reject(new Error(`Invalid JSON body: ${e.message}`));
10691
10742
  }
@@ -11520,15 +11571,43 @@ async function applyForward(db, entry) {
11520
11571
  }
11521
11572
  void before;
11522
11573
  }
11523
- function readRowContext(outputDir, def, row, secretCols) {
11524
- const slug = def.slug(row);
11525
- const directoryRoot = def.directoryRoot ?? "";
11574
+ function deriveSlugFromManifest(row, knownSlugs) {
11575
+ const candidateFields = ["slug", "id", "name"];
11576
+ for (const field of candidateFields) {
11577
+ const value = row[field];
11578
+ if (typeof value === "string" && knownSlugs.has(value)) return value;
11579
+ }
11580
+ return null;
11581
+ }
11582
+ function buildRowContextLocator(table, row, schemaDef, manifest) {
11583
+ if (schemaDef) {
11584
+ return {
11585
+ directoryRoot: schemaDef.directoryRoot ?? "",
11586
+ slug: schemaDef.slug(row),
11587
+ fileNames: Object.keys(schemaDef.files)
11588
+ };
11589
+ }
11590
+ const manifestEntry = manifest?.entityContexts[table];
11591
+ if (!manifestEntry) return null;
11592
+ const knownSlugs = new Set(Object.keys(manifestEntry.entities));
11593
+ const derivedSlug = deriveSlugFromManifest(row, knownSlugs);
11594
+ if (!derivedSlug) return null;
11595
+ const entityFiles = manifestEntry.entities[derivedSlug];
11596
+ const fileNames = entityFiles ? entityFileNames(entityFiles) : manifestEntry.declaredFiles;
11597
+ return {
11598
+ directoryRoot: manifestEntry.directoryRoot,
11599
+ slug: derivedSlug,
11600
+ fileNames
11601
+ };
11602
+ }
11603
+ function readRowContext(outputDir, locator, secretCols) {
11604
+ const { slug, directoryRoot, fileNames } = locator;
11526
11605
  const entityDir = resolve6(outputDir, directoryRoot, slug);
11527
11606
  const resolvedBase = resolve6(outputDir);
11528
11607
  if (entityDir !== resolvedBase && !entityDir.startsWith(resolvedBase + sep3)) {
11529
11608
  throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
11530
11609
  }
11531
- return Object.keys(def.files).map((filename) => {
11610
+ return fileNames.map((filename) => {
11532
11611
  const absPath = join13(entityDir, filename);
11533
11612
  const relPath = join13(directoryRoot, slug, filename);
11534
11613
  if (!existsSync14(absPath)) return { name: filename, path: relPath, content: "" };
@@ -11598,10 +11677,8 @@ async function openConfig(configPath, outputDir) {
11598
11677
  const junctionTables = new Set(
11599
11678
  getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t) => t.name)
11600
11679
  );
11601
- const entityContextByTable = /* @__PURE__ */ new Map();
11602
- for (const { table, definition } of parsed.entityContexts) {
11603
- entityContextByTable.set(table, definition);
11604
- }
11680
+ const entityContextByTable = db.entityContexts();
11681
+ const manifest = readManifest(outputDir);
11605
11682
  const softDeletable = new Set(
11606
11683
  parsed.tables.filter(({ definition }) => "deleted_at" in definition.columns).map(({ name }) => name)
11607
11684
  );
@@ -11615,6 +11692,7 @@ async function openConfig(configPath, outputDir) {
11615
11692
  validTables,
11616
11693
  junctionTables,
11617
11694
  entityContextByTable,
11695
+ manifest,
11618
11696
  softDeletable
11619
11697
  };
11620
11698
  }
@@ -12116,16 +12194,17 @@ async function startGuiServer(options) {
12116
12194
  sendJson5(res, { error: `Unknown table: ${ctxTable}` }, 400);
12117
12195
  return;
12118
12196
  }
12119
- const def = active.entityContextByTable.get(ctxTable);
12120
- if (!def) {
12121
- sendJson5(res, { files: [] });
12122
- return;
12123
- }
12124
12197
  const row = await active.db.get(ctxTable, ctxId);
12125
12198
  if (row === null) {
12126
12199
  sendJson5(res, { error: "Row not found" }, 404);
12127
12200
  return;
12128
12201
  }
12202
+ const def = active.entityContextByTable.get(ctxTable);
12203
+ const locator = buildRowContextLocator(ctxTable, row, def, active.manifest);
12204
+ if (!locator) {
12205
+ sendJson5(res, { files: [] });
12206
+ return;
12207
+ }
12129
12208
  const colMetaRows = await active.db.query("_lattice_gui_column_meta", {
12130
12209
  filters: [
12131
12210
  { col: "table_name", op: "eq", val: ctxTable },
@@ -12133,7 +12212,7 @@ async function startGuiServer(options) {
12133
12212
  ]
12134
12213
  });
12135
12214
  const secretCols = new Set(colMetaRows.map((r) => r.column_name));
12136
- sendJson5(res, { files: readRowContext(active.outputDir, def, row, secretCols) });
12215
+ sendJson5(res, { files: readRowContext(active.outputDir, locator, secretCols) });
12137
12216
  return;
12138
12217
  }
12139
12218
  const rowsMatch = ROWS_PATH.exec(pathname);
@@ -12288,8 +12367,20 @@ async function startGuiServer(options) {
12288
12367
  };
12289
12368
  }
12290
12369
 
12370
+ // src/gui/discover-output-dir.ts
12371
+ import { existsSync as existsSync15 } from "fs";
12372
+ import { join as join14, resolve as resolve7 } from "path";
12373
+ function discoverOutputDir(explicitOutput, explicit) {
12374
+ if (explicit) return explicitOutput;
12375
+ const candidates = ["./context", ".", "./generated"];
12376
+ for (const dir of candidates) {
12377
+ if (existsSync15(join14(resolve7(dir), ".lattice", "manifest.json"))) return dir;
12378
+ }
12379
+ return explicitOutput;
12380
+ }
12381
+
12291
12382
  // src/teams/cli-commands.ts
12292
- import { resolve as resolve7 } from "path";
12383
+ import { resolve as resolve8 } from "path";
12293
12384
  var TEAMS_USAGE = [
12294
12385
  "lattice teams <subcommand> [options]",
12295
12386
  "",
@@ -12405,7 +12496,7 @@ function requireArg(args, key, label) {
12405
12496
  return v.trim();
12406
12497
  }
12407
12498
  async function openLocal(configPath) {
12408
- const db = new Lattice({ config: resolve7(configPath) });
12499
+ const db = new Lattice({ config: resolve8(configPath) });
12409
12500
  await db.init();
12410
12501
  return db;
12411
12502
  }
@@ -12729,6 +12820,7 @@ function parseArgs(argv) {
12729
12820
  let config = "./lattice.config.yml";
12730
12821
  let out = "./generated";
12731
12822
  let output = "./context";
12823
+ let outputExplicit = false;
12732
12824
  let scaffold = false;
12733
12825
  let help = false;
12734
12826
  let version = false;
@@ -12779,6 +12871,7 @@ function parseArgs(argv) {
12779
12871
  } else if ((arg === "--output" || arg === "--output-dir") && i + 1 < argv.length) {
12780
12872
  i++;
12781
12873
  output = argv[i] ?? output;
12874
+ outputExplicit = true;
12782
12875
  } else if (arg === "--scaffold") {
12783
12876
  scaffold = true;
12784
12877
  } else if (arg === "--dry-run") {
@@ -12854,6 +12947,7 @@ function parseArgs(argv) {
12854
12947
  config,
12855
12948
  out,
12856
12949
  output,
12950
+ outputExplicit,
12857
12951
  scaffold,
12858
12952
  help,
12859
12953
  version,
@@ -12979,7 +13073,7 @@ async function runUpdate() {
12979
13073
  }
12980
13074
  }
12981
13075
  function runGenerate(args) {
12982
- const configPath = resolve8(args.config);
13076
+ const configPath = resolve9(args.config);
12983
13077
  let raw;
12984
13078
  try {
12985
13079
  raw = readFileSync12(configPath, "utf-8");
@@ -12999,7 +13093,7 @@ function runGenerate(args) {
12999
13093
  process.exit(1);
13000
13094
  }
13001
13095
  const configDir2 = dirname7(configPath);
13002
- const outDir = resolve8(args.out);
13096
+ const outDir = resolve9(args.out);
13003
13097
  try {
13004
13098
  const result = generateAll({ config, configDir: configDir2, outDir, scaffold: args.scaffold });
13005
13099
  console.log(`Generated ${String(result.filesWritten.length)} file(s):`);
@@ -13012,15 +13106,15 @@ function runGenerate(args) {
13012
13106
  }
13013
13107
  }
13014
13108
  async function runRender(args) {
13015
- const outputDir = resolve8(args.output);
13109
+ const outputDir = resolve9(args.output);
13016
13110
  let parsed;
13017
13111
  try {
13018
- parsed = parseConfigFile(resolve8(args.config));
13112
+ parsed = parseConfigFile(resolve9(args.config));
13019
13113
  } catch (e) {
13020
13114
  console.error(`Error: ${e.message}`);
13021
13115
  process.exit(1);
13022
13116
  }
13023
- const db = new Lattice({ config: resolve8(args.config) });
13117
+ const db = new Lattice({ config: resolve9(args.config) });
13024
13118
  try {
13025
13119
  await db.init();
13026
13120
  const start = Date.now();
@@ -13039,8 +13133,8 @@ async function runRender(args) {
13039
13133
  void parsed;
13040
13134
  }
13041
13135
  async function runReconcile(args, isDryRun) {
13042
- const outputDir = resolve8(args.output);
13043
- const db = new Lattice({ config: resolve8(args.config) });
13136
+ const outputDir = resolve9(args.output);
13137
+ const db = new Lattice({ config: resolve9(args.config) });
13044
13138
  try {
13045
13139
  await db.init();
13046
13140
  const start = Date.now();
@@ -13099,8 +13193,8 @@ function formatTimestamp() {
13099
13193
  return `${hh}:${mm}:${ss}`;
13100
13194
  }
13101
13195
  async function runWatch(args) {
13102
- const outputDir = resolve8(args.output);
13103
- const db = new Lattice({ config: resolve8(args.config) });
13196
+ const outputDir = resolve9(args.output);
13197
+ const db = new Lattice({ config: resolve9(args.config) });
13104
13198
  try {
13105
13199
  await db.init();
13106
13200
  } catch (e) {
@@ -13141,9 +13235,15 @@ async function runWatch(args) {
13141
13235
  }
13142
13236
  async function runGui(args) {
13143
13237
  try {
13238
+ const resolvedOutput = discoverOutputDir(args.output, args.outputExplicit);
13239
+ if (!args.outputExplicit && resolvedOutput !== args.output) {
13240
+ console.log(
13241
+ `Lattice GUI: auto-detected rendered context at "${resolvedOutput}" (use --output to override).`
13242
+ );
13243
+ }
13144
13244
  const handle = await startGuiServer({
13145
- configPath: resolve8(args.config),
13146
- outputDir: resolve8(args.output),
13245
+ configPath: resolve9(args.config),
13246
+ outputDir: resolve9(resolvedOutput),
13147
13247
  port: args.port,
13148
13248
  openBrowser: !args.noOpen
13149
13249
  });
@@ -13162,8 +13262,8 @@ async function runGui(args) {
13162
13262
  async function runServe(args) {
13163
13263
  try {
13164
13264
  const handle = await startGuiServer({
13165
- configPath: resolve8(args.config),
13166
- outputDir: resolve8(args.output),
13265
+ configPath: resolve9(args.config),
13266
+ outputDir: resolve9(args.output),
13167
13267
  host: args.host,
13168
13268
  port: args.port,
13169
13269
  openBrowser: false,
package/dist/index.cjs CHANGED
@@ -3434,6 +3434,16 @@ var Lattice = class {
3434
3434
  this._schema.defineEntityContext(table, def);
3435
3435
  return this;
3436
3436
  }
3437
+ /**
3438
+ * All entity contexts currently registered on this Lattice — both those
3439
+ * declared in `lattice.config.yml` and those added programmatically via
3440
+ * `defineEntityContext()`.
3441
+ *
3442
+ * Returns a defensive copy so callers can't mutate the schema.
3443
+ */
3444
+ entityContexts() {
3445
+ return new Map(this._schema.getEntityContexts());
3446
+ }
3437
3447
  /**
3438
3448
  * Register a write hook that fires after insert/update/delete operations.
3439
3449
  * Hooks run synchronously after the DB write and audit emit.
@@ -5720,11 +5730,18 @@ async function probeCloud(targetUrl) {
5720
5730
  }
5721
5731
  return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
5722
5732
  } catch (e) {
5733
+ const err = e;
5734
+ const parts = [];
5735
+ if (err.code) parts.push(`[${err.code}]`);
5736
+ if (err.message) parts.push(err.message);
5737
+ if (err.routine && !err.message.includes(err.routine)) {
5738
+ parts.push(`(routine: ${err.routine})`);
5739
+ }
5723
5740
  return {
5724
5741
  reachable: false,
5725
5742
  dialect,
5726
5743
  teamEnabled: false,
5727
- error: e.message
5744
+ error: parts.join(" ") || "unknown"
5728
5745
  };
5729
5746
  } finally {
5730
5747
  if (probe) {
package/dist/index.d.cts CHANGED
@@ -1608,6 +1608,14 @@ declare class Lattice {
1608
1608
  private _registerTable;
1609
1609
  defineMulti(name: string, def: MultiTableDefinition): this;
1610
1610
  defineEntityContext(table: string, def: EntityContextDefinition): this;
1611
+ /**
1612
+ * All entity contexts currently registered on this Lattice — both those
1613
+ * declared in `lattice.config.yml` and those added programmatically via
1614
+ * `defineEntityContext()`.
1615
+ *
1616
+ * Returns a defensive copy so callers can't mutate the schema.
1617
+ */
1618
+ entityContexts(): Map<string, EntityContextDefinition>;
1611
1619
  /**
1612
1620
  * Register a write hook that fires after insert/update/delete operations.
1613
1621
  * Hooks run synchronously after the DB write and audit emit.
@@ -2731,8 +2739,8 @@ declare function attachBlob(srcPath: string, latticeRoot: string): Promise<BlobM
2731
2739
  * keys/<label>.token per-joined-team bearer tokens (added later).
2732
2740
  * db-credentials.enc encrypted Postgres URLs (added later).
2733
2741
  *
2734
- * Per Rule 7 (public-repo isolation), do NOT log filesystem paths or
2735
- * user identity values from this module.
2742
+ * Security: do NOT log filesystem paths or user identity values from
2743
+ * this module. Errors must be thrown without echoing sensitive arguments.
2736
2744
  */
2737
2745
  /** Root directory for machine-local lattice config. Override via env. */
2738
2746
  declare function configDir(): string;
package/dist/index.d.ts CHANGED
@@ -1608,6 +1608,14 @@ declare class Lattice {
1608
1608
  private _registerTable;
1609
1609
  defineMulti(name: string, def: MultiTableDefinition): this;
1610
1610
  defineEntityContext(table: string, def: EntityContextDefinition): this;
1611
+ /**
1612
+ * All entity contexts currently registered on this Lattice — both those
1613
+ * declared in `lattice.config.yml` and those added programmatically via
1614
+ * `defineEntityContext()`.
1615
+ *
1616
+ * Returns a defensive copy so callers can't mutate the schema.
1617
+ */
1618
+ entityContexts(): Map<string, EntityContextDefinition>;
1611
1619
  /**
1612
1620
  * Register a write hook that fires after insert/update/delete operations.
1613
1621
  * Hooks run synchronously after the DB write and audit emit.
@@ -2731,8 +2739,8 @@ declare function attachBlob(srcPath: string, latticeRoot: string): Promise<BlobM
2731
2739
  * keys/<label>.token per-joined-team bearer tokens (added later).
2732
2740
  * db-credentials.enc encrypted Postgres URLs (added later).
2733
2741
  *
2734
- * Per Rule 7 (public-repo isolation), do NOT log filesystem paths or
2735
- * user identity values from this module.
2742
+ * Security: do NOT log filesystem paths or user identity values from
2743
+ * this module. Errors must be thrown without echoing sensitive arguments.
2736
2744
  */
2737
2745
  /** Root directory for machine-local lattice config. Override via env. */
2738
2746
  declare function configDir(): string;
package/dist/index.js CHANGED
@@ -3362,6 +3362,16 @@ var Lattice = class {
3362
3362
  this._schema.defineEntityContext(table, def);
3363
3363
  return this;
3364
3364
  }
3365
+ /**
3366
+ * All entity contexts currently registered on this Lattice — both those
3367
+ * declared in `lattice.config.yml` and those added programmatically via
3368
+ * `defineEntityContext()`.
3369
+ *
3370
+ * Returns a defensive copy so callers can't mutate the schema.
3371
+ */
3372
+ entityContexts() {
3373
+ return new Map(this._schema.getEntityContexts());
3374
+ }
3365
3375
  /**
3366
3376
  * Register a write hook that fires after insert/update/delete operations.
3367
3377
  * Hooks run synchronously after the DB write and audit emit.
@@ -5648,11 +5658,18 @@ async function probeCloud(targetUrl) {
5648
5658
  }
5649
5659
  return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
5650
5660
  } catch (e) {
5661
+ const err = e;
5662
+ const parts = [];
5663
+ if (err.code) parts.push(`[${err.code}]`);
5664
+ if (err.message) parts.push(err.message);
5665
+ if (err.routine && !err.message.includes(err.routine)) {
5666
+ parts.push(`(routine: ${err.routine})`);
5667
+ }
5651
5668
  return {
5652
5669
  reachable: false,
5653
5670
  dialect,
5654
5671
  teamEnabled: false,
5655
- error: e.message
5672
+ error: parts.join(" ") || "unknown"
5656
5673
  };
5657
5674
  } finally {
5658
5675
  if (probe) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
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",