@staff0rd/assist 0.315.1 → 0.316.0

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +141 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -145,8 +145,8 @@ The first backlog command in a repository that still has a local `.assist/backlo
145
145
  - `assist backlog phase-done <id> <phase> <summary>` - Signal that a plan phase is complete with a summary (used by orchestrator)
146
146
  - `assist backlog rewind <id> <phase> --reason <reason>` - Rewind a backlog item to an earlier phase, setting status to in-progress
147
147
  - `assist backlog run <id>` - Run a backlog item's plan phase-by-phase with Claude; `--resume-session <id>` resumes an interrupted Claude session for the current phase (used by the sessions daemon when it restarts a running item)
148
- - `assist backlog export [file]` - Export the entire backlog database (all tables, all repos) to a file, or stdout if omitted
149
- - `assist backlog import [file]` - Restore the entire backlog database from a dump (file or stdin), faithfully replacing all data; prompts for confirmation (use `-y, --yes` to skip; required when reading from stdin)
148
+ - `assist backlog export [file]` - Export every table in the backlog database (discovered by live schema introspection, so new tables are covered automatically) to a file, or stdout if omitted
149
+ - `assist backlog import [file]` - Restore every table present in a dump (file or stdin) back into the backlog database in foreign-key-safe order, faithfully replacing all data and resyncing identity sequences; prompts for confirmation (use `-y, --yes` to skip; required when reading from stdin)
150
150
  - `assist backlog move-repo <old-origin> [new-origin]` - Retag all items from one origin to another after a repo rename; the new origin defaults to the current repo's remote, both accept URL or `git@` forms, and a bare repo name works for the old origin when unambiguous. Prompts for confirmation (use `-y, --yes` to skip)
151
151
  - `assist backlog web [-p, --port <number>] [--no-open]` - Open the backlog tab in the web dashboard (default port 3100); `--no-open` skips opening a browser on startup
152
152
  - `assist roam auth` - Authenticate with Roam via OAuth (opens browser, saves tokens to ~/.assist.yml)
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.315.1",
9
+ version: "0.316.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -3391,40 +3391,17 @@ import chalk29 from "chalk";
3391
3391
  // src/commands/backlog/dump/DumpTable.ts
3392
3392
  var DUMP_FORMAT = "assist-backlog-dump";
3393
3393
  var DUMP_VERSION = 1;
3394
- var DUMP_TABLES = [
3395
- {
3396
- name: "items",
3397
- columns: [
3398
- "id",
3399
- "origin",
3400
- "type",
3401
- "name",
3402
- "description",
3403
- "acceptance_criteria",
3404
- "status",
3405
- "current_phase"
3406
- ]
3407
- },
3408
- {
3409
- name: "comments",
3410
- columns: ["id", "item_id", "idx", "text", "phase", "timestamp", "type"]
3411
- },
3412
- { name: "links", columns: ["item_id", "type", "target_id"] },
3413
- { name: "plan_phases", columns: ["item_id", "idx", "name", "manual_checks"] },
3414
- { name: "plan_tasks", columns: ["item_id", "phase_idx", "idx", "task"] },
3415
- { name: "metadata", columns: ["key", "value"] }
3416
- ];
3417
3394
 
3418
3395
  // src/commands/backlog/dump/buildDump.ts
3419
- async function buildDump(copyOut) {
3396
+ async function buildDump(tables, copyOut) {
3420
3397
  const header = JSON.stringify({
3421
3398
  format: DUMP_FORMAT,
3422
3399
  version: DUMP_VERSION,
3423
- tables: DUMP_TABLES.map(({ name, columns }) => ({ name, columns }))
3400
+ tables: tables.map(({ name, columns }) => ({ name, columns }))
3424
3401
  });
3425
3402
  const parts = [Buffer.from(`${header}
3426
3403
  `, "utf8")];
3427
- for (const table of DUMP_TABLES) {
3404
+ for (const table of tables) {
3428
3405
  const data = await copyOut(table);
3429
3406
  parts.push(Buffer.from(`@table ${table.name} ${data.length}
3430
3407
  `, "utf8"));
@@ -3445,11 +3422,95 @@ async function copyTableOut(client, table) {
3445
3422
  return Buffer.concat(chunks);
3446
3423
  }
3447
3424
 
3425
+ // src/commands/backlog/dump/groupColumns.ts
3426
+ function groupColumns(rows) {
3427
+ const byName = /* @__PURE__ */ new Map();
3428
+ for (const { table_name, quoted_table, quoted_column } of rows) {
3429
+ let entry = byName.get(table_name);
3430
+ if (!entry) {
3431
+ entry = { raw: table_name, table: { name: quoted_table, columns: [] } };
3432
+ byName.set(table_name, entry);
3433
+ }
3434
+ entry.table.columns.push(quoted_column);
3435
+ }
3436
+ return [...byName.values()];
3437
+ }
3438
+
3439
+ // src/commands/backlog/dump/orderDumpTables.ts
3440
+ function buildGraph(names, edges) {
3441
+ const present = new Set(names);
3442
+ const indegree = new Map(names.map((n) => [n, 0]));
3443
+ const children = /* @__PURE__ */ new Map();
3444
+ for (const { child, parent } of edges) {
3445
+ if (!present.has(child) || !present.has(parent)) continue;
3446
+ indegree.set(child, (indegree.get(child) ?? 0) + 1);
3447
+ children.set(parent, [...children.get(parent) ?? [], child]);
3448
+ }
3449
+ return { indegree, children };
3450
+ }
3451
+ function nextReady(names, emitted, indegree) {
3452
+ return names.find((n) => !emitted.has(n) && (indegree.get(n) ?? 0) === 0);
3453
+ }
3454
+ function topoSort(entries, edges) {
3455
+ const names = entries.map((e) => e.raw);
3456
+ const { indegree, children } = buildGraph(names, edges);
3457
+ const byRaw = new Map(entries.map((e) => [e.raw, e.table]));
3458
+ const ordered = [];
3459
+ const emitted = /* @__PURE__ */ new Set();
3460
+ while (ordered.length < entries.length) {
3461
+ const next3 = nextReady(names, emitted, indegree);
3462
+ if (!next3)
3463
+ throw new Error("Cannot dump: foreign-key cycle between tables.");
3464
+ emitted.add(next3);
3465
+ ordered.push(byRaw.get(next3));
3466
+ for (const child of children.get(next3) ?? []) {
3467
+ indegree.set(child, (indegree.get(child) ?? 0) - 1);
3468
+ }
3469
+ }
3470
+ return ordered;
3471
+ }
3472
+ function orderDumpTables(columnRows, fkRows) {
3473
+ return topoSort(groupColumns(columnRows), fkRows);
3474
+ }
3475
+
3476
+ // src/commands/backlog/dump/introspectDumpTables.ts
3477
+ var COLUMNS_SQL = `
3478
+ SELECT
3479
+ c.table_name AS table_name,
3480
+ quote_ident(c.table_name) AS quoted_table,
3481
+ quote_ident(c.column_name) AS quoted_column
3482
+ FROM information_schema.columns c
3483
+ JOIN information_schema.tables t
3484
+ ON t.table_schema = c.table_schema
3485
+ AND t.table_name = c.table_name
3486
+ WHERE c.table_schema = 'public'
3487
+ AND t.table_type = 'BASE TABLE'
3488
+ ORDER BY c.table_name, c.ordinal_position
3489
+ `;
3490
+ var FK_SQL = `
3491
+ SELECT child.relname AS child, parent.relname AS parent
3492
+ FROM pg_constraint con
3493
+ JOIN pg_class child ON child.oid = con.conrelid
3494
+ JOIN pg_class parent ON parent.oid = con.confrelid
3495
+ JOIN pg_namespace n ON n.oid = con.connamespace
3496
+ WHERE con.contype = 'f'
3497
+ AND n.nspname = 'public'
3498
+ AND con.conrelid <> con.confrelid
3499
+ `;
3500
+ async function introspectDumpTables(client) {
3501
+ const [{ rows: columnRows }, { rows: fkRows }] = await Promise.all([
3502
+ client.query(COLUMNS_SQL),
3503
+ client.query(FK_SQL)
3504
+ ]);
3505
+ return orderDumpTables(columnRows, fkRows);
3506
+ }
3507
+
3448
3508
  // src/commands/backlog/export/index.ts
3449
3509
  async function exportBacklog(file) {
3450
- const dump = await withDbClient(
3451
- (client) => buildDump((table) => copyTableOut(client, table))
3452
- );
3510
+ const dump = await withDbClient(async (client) => {
3511
+ const tables = await introspectDumpTables(client);
3512
+ return buildDump(tables, (table) => copyTableOut(client, table));
3513
+ });
3453
3514
  if (file) {
3454
3515
  await writeFile(file, dump);
3455
3516
  console.error(
@@ -6573,7 +6634,7 @@ function registerCommentCommands(cmd) {
6573
6634
  // src/commands/backlog/registerExportCommand.ts
6574
6635
  function registerExportCommand(cmd) {
6575
6636
  cmd.command("export [file]").description(
6576
- "Export the entire backlog database to a file, or stdout if omitted"
6637
+ "Export every table in the backlog database (discovered by introspection) to a file, or stdout if omitted"
6577
6638
  ).action(exportBacklog);
6578
6639
  }
6579
6640
 
@@ -6631,11 +6692,9 @@ function parseDump(dump) {
6631
6692
  }
6632
6693
 
6633
6694
  // src/commands/backlog/dump/validateDump.ts
6634
- function tableShape(tables) {
6635
- return JSON.stringify(tables.map(({ name, columns }) => ({ name, columns })));
6636
- }
6637
6695
  function validateDump({ header, sections }) {
6638
- const missing = DUMP_TABLES.find(({ name }) => !sections.has(name))?.name;
6696
+ const tables = header.tables ?? [];
6697
+ const missing = tables.find(({ name }) => !sections.has(name))?.name;
6639
6698
  const checks = [
6640
6699
  [
6641
6700
  header.format === DUMP_FORMAT,
@@ -6645,10 +6704,7 @@ function validateDump({ header, sections }) {
6645
6704
  header.version === DUMP_VERSION,
6646
6705
  `Unsupported dump version ${header.version} (this build restores version ${DUMP_VERSION}).`
6647
6706
  ],
6648
- [
6649
- tableShape(header.tables ?? []) === tableShape(DUMP_TABLES),
6650
- "Dump table set does not match this build's backlog schema; cannot restore."
6651
- ],
6707
+ [tables.length > 0, "Dump header lists no tables; cannot restore."],
6652
6708
  [!missing, `Invalid dump: missing data section for "${missing}".`]
6653
6709
  ];
6654
6710
  const failure = checks.find(([ok]) => !ok);
@@ -6663,24 +6719,24 @@ async function countRows(client, table) {
6663
6719
  );
6664
6720
  return rows[0].n;
6665
6721
  }
6666
- function printSummary(current, incoming) {
6667
- const lines = DUMP_TABLES.map(
6722
+ function printSummary(tables, current, incoming) {
6723
+ const lines = tables.map(
6668
6724
  (t, i) => ` ${t.name}: ${current[i]} \u2192 ${incoming[i]} rows`
6669
6725
  );
6670
6726
  console.error(chalk57.bold("\nThis will REPLACE all backlog data:"));
6671
6727
  console.error(`${lines.join("\n")}
6672
6728
  `);
6673
6729
  }
6674
- async function confirmReplace(client, incoming, fromStdin) {
6730
+ async function confirmReplace(client, tables, incoming, fromStdin) {
6675
6731
  if (fromStdin) {
6676
6732
  throw new Error(
6677
6733
  "Reading a dump from stdin requires --yes (stdin is consumed by the dump)."
6678
6734
  );
6679
6735
  }
6680
6736
  const current = await Promise.all(
6681
- DUMP_TABLES.map((t) => countRows(client, t.name))
6737
+ tables.map((t) => countRows(client, t.name))
6682
6738
  );
6683
- printSummary(current, incoming);
6739
+ printSummary(tables, current, incoming);
6684
6740
  return promptConfirm("Replace all backlog data with this dump?", false);
6685
6741
  }
6686
6742
 
@@ -6703,23 +6759,48 @@ async function copyTableIn(client, table, data) {
6703
6759
  await finished(stream);
6704
6760
  }
6705
6761
 
6706
- // src/commands/backlog/import/restore.ts
6707
- var IDENTITY_TABLES = ["items", "comments"];
6708
- function resyncIdentitySql(table) {
6709
- return `SELECT setval(pg_get_serial_sequence('${table}', 'id'),
6710
- GREATEST(COALESCE((SELECT max(id) FROM ${table}), 0), 1),
6711
- (SELECT count(*) FROM ${table}) > 0)`;
6762
+ // src/commands/backlog/dump/introspectIdentityColumns.ts
6763
+ var IDENTITY_SQL = `
6764
+ SELECT
6765
+ quote_ident(table_name) AS table,
6766
+ quote_ident(column_name) AS column,
6767
+ column_name AS raw_column
6768
+ FROM information_schema.columns
6769
+ WHERE table_schema = 'public'
6770
+ AND is_identity = 'YES'
6771
+ ORDER BY table_name, ordinal_position
6772
+ `;
6773
+ async function introspectIdentityColumns(client) {
6774
+ const { rows } = await client.query(IDENTITY_SQL);
6775
+ return rows.map((row) => ({
6776
+ table: row.table,
6777
+ column: row.column,
6778
+ rawColumn: row.raw_column
6779
+ }));
6780
+ }
6781
+ function resyncIdentitySql(col) {
6782
+ return {
6783
+ text: `SELECT setval(
6784
+ pg_get_serial_sequence($1, $2),
6785
+ GREATEST(COALESCE((SELECT max(${col.column}) FROM ${col.table}), 0), 1),
6786
+ (SELECT count(*) FROM ${col.table}) > 0)`,
6787
+ values: [col.table, col.rawColumn]
6788
+ };
6712
6789
  }
6790
+
6791
+ // src/commands/backlog/import/restore.ts
6713
6792
  async function replaceData(client, parsed) {
6714
- const tables = DUMP_TABLES.map((t) => t.name).join(", ");
6793
+ const { tables } = parsed.header;
6794
+ const tableList = tables.map((t) => t.name).join(", ");
6715
6795
  await client.query(SCHEMA);
6716
- await client.query(`TRUNCATE ${tables} RESTART IDENTITY CASCADE`);
6717
- for (const table of DUMP_TABLES) {
6796
+ await client.query(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`);
6797
+ for (const table of tables) {
6718
6798
  const data = parsed.sections.get(table.name) ?? Buffer.alloc(0);
6719
6799
  await copyTableIn(client, table, data);
6720
6800
  }
6721
- for (const table of IDENTITY_TABLES) {
6722
- await client.query(resyncIdentitySql(table));
6801
+ for (const col of await introspectIdentityColumns(client)) {
6802
+ const { text: text6, values } = resyncIdentitySql(col);
6803
+ await client.query(text6, values);
6723
6804
  }
6724
6805
  }
6725
6806
  async function restore(client, parsed) {
@@ -6738,11 +6819,12 @@ async function importBacklog(file, options2 = {}) {
6738
6819
  const raw = file ? await readFile(file) : await readStdinBuffer();
6739
6820
  const parsed = parseDump(raw);
6740
6821
  validateDump(parsed);
6741
- const incoming = DUMP_TABLES.map(
6822
+ const { tables } = parsed.header;
6823
+ const incoming = tables.map(
6742
6824
  (t) => countCopyRows(parsed.sections.get(t.name) ?? Buffer.alloc(0))
6743
6825
  );
6744
6826
  await withDbClient(async (client) => {
6745
- if (!options2.yes && !await confirmReplace(client, incoming, !file)) {
6827
+ if (!options2.yes && !await confirmReplace(client, tables, incoming, !file)) {
6746
6828
  console.error(chalk58.yellow("Import cancelled; no changes made."));
6747
6829
  return;
6748
6830
  }
@@ -6750,7 +6832,7 @@ async function importBacklog(file, options2 = {}) {
6750
6832
  const total = incoming.reduce((sum, n) => sum + n, 0);
6751
6833
  console.error(
6752
6834
  chalk58.green(
6753
- `Imported backlog: ${total} rows restored across ${DUMP_TABLES.length} tables.`
6835
+ `Imported backlog: ${total} rows restored across ${tables.length} tables.`
6754
6836
  )
6755
6837
  );
6756
6838
  });
@@ -6759,7 +6841,7 @@ async function importBacklog(file, options2 = {}) {
6759
6841
  // src/commands/backlog/registerImportCommand.ts
6760
6842
  function registerImportCommand(cmd) {
6761
6843
  cmd.command("import [file]").description(
6762
- "Restore the entire backlog database from a dump file, or stdin if omitted (destructive)"
6844
+ "Restore every table in a dump back into the backlog database, in foreign-key-safe order, from a file or stdin if omitted (destructive)"
6763
6845
  ).option("-y, --yes", "Skip the confirmation prompt").action(
6764
6846
  (file, options2) => importBacklog(file, options2)
6765
6847
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.315.1",
3
+ "version": "0.316.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {