@staff0rd/assist 0.315.0 → 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 +150 -61
  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.0",
9
+ version: "0.316.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -663,8 +663,9 @@ function setupVerifyRunEntry(scriptName, command, options2) {
663
663
  // src/commands/verify/setup/expectedScripts.ts
664
664
  var expectedScripts = {
665
665
  "verify:knip": "knip --no-progress --treat-config-hints-as-errors",
666
- "verify:lint": "oxlint --config .oxlintrc.json .",
667
- "verify:duplicate-code": "jscpd --format 'typescript,tsx' --exitCode 1 --ignore '**/*.test.*' -r consoleFull src",
666
+ "verify:lint": "oxlint --config .oxlintrc.json --fix .",
667
+ "verify:format": "oxfmt .",
668
+ "verify:duplicate-code": "jscpd --format 'typescript,tsx' --exit-code 1 --ignore '**/*.test.*' -r consoleFull src",
668
669
  "verify:test": "vitest run --reporter=dot --silent",
669
670
  "verify:hardcoded-colors": "assist verify hardcoded-colors",
670
671
  "verify:no-venv": "assist verify no-venv",
@@ -990,6 +991,12 @@ async function setupLint(packageJsonPath, writer) {
990
991
  }
991
992
  await init();
992
993
  writer("verify:lint", expectedScripts["verify:lint"]);
994
+ if (!pkg.devDependencies?.oxfmt) {
995
+ if (!installPackage("oxfmt", cwd)) {
996
+ return;
997
+ }
998
+ }
999
+ writer("verify:format", expectedScripts["verify:format"]);
993
1000
  }
994
1001
 
995
1002
  // src/commands/verify/setup/setupMaintainability.ts
@@ -3384,40 +3391,17 @@ import chalk29 from "chalk";
3384
3391
  // src/commands/backlog/dump/DumpTable.ts
3385
3392
  var DUMP_FORMAT = "assist-backlog-dump";
3386
3393
  var DUMP_VERSION = 1;
3387
- var DUMP_TABLES = [
3388
- {
3389
- name: "items",
3390
- columns: [
3391
- "id",
3392
- "origin",
3393
- "type",
3394
- "name",
3395
- "description",
3396
- "acceptance_criteria",
3397
- "status",
3398
- "current_phase"
3399
- ]
3400
- },
3401
- {
3402
- name: "comments",
3403
- columns: ["id", "item_id", "idx", "text", "phase", "timestamp", "type"]
3404
- },
3405
- { name: "links", columns: ["item_id", "type", "target_id"] },
3406
- { name: "plan_phases", columns: ["item_id", "idx", "name", "manual_checks"] },
3407
- { name: "plan_tasks", columns: ["item_id", "phase_idx", "idx", "task"] },
3408
- { name: "metadata", columns: ["key", "value"] }
3409
- ];
3410
3394
 
3411
3395
  // src/commands/backlog/dump/buildDump.ts
3412
- async function buildDump(copyOut) {
3396
+ async function buildDump(tables, copyOut) {
3413
3397
  const header = JSON.stringify({
3414
3398
  format: DUMP_FORMAT,
3415
3399
  version: DUMP_VERSION,
3416
- tables: DUMP_TABLES.map(({ name, columns }) => ({ name, columns }))
3400
+ tables: tables.map(({ name, columns }) => ({ name, columns }))
3417
3401
  });
3418
3402
  const parts = [Buffer.from(`${header}
3419
3403
  `, "utf8")];
3420
- for (const table of DUMP_TABLES) {
3404
+ for (const table of tables) {
3421
3405
  const data = await copyOut(table);
3422
3406
  parts.push(Buffer.from(`@table ${table.name} ${data.length}
3423
3407
  `, "utf8"));
@@ -3438,11 +3422,95 @@ async function copyTableOut(client, table) {
3438
3422
  return Buffer.concat(chunks);
3439
3423
  }
3440
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
+
3441
3508
  // src/commands/backlog/export/index.ts
3442
3509
  async function exportBacklog(file) {
3443
- const dump = await withDbClient(
3444
- (client) => buildDump((table) => copyTableOut(client, table))
3445
- );
3510
+ const dump = await withDbClient(async (client) => {
3511
+ const tables = await introspectDumpTables(client);
3512
+ return buildDump(tables, (table) => copyTableOut(client, table));
3513
+ });
3446
3514
  if (file) {
3447
3515
  await writeFile(file, dump);
3448
3516
  console.error(
@@ -6566,7 +6634,7 @@ function registerCommentCommands(cmd) {
6566
6634
  // src/commands/backlog/registerExportCommand.ts
6567
6635
  function registerExportCommand(cmd) {
6568
6636
  cmd.command("export [file]").description(
6569
- "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"
6570
6638
  ).action(exportBacklog);
6571
6639
  }
6572
6640
 
@@ -6624,11 +6692,9 @@ function parseDump(dump) {
6624
6692
  }
6625
6693
 
6626
6694
  // src/commands/backlog/dump/validateDump.ts
6627
- function tableShape(tables) {
6628
- return JSON.stringify(tables.map(({ name, columns }) => ({ name, columns })));
6629
- }
6630
6695
  function validateDump({ header, sections }) {
6631
- 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;
6632
6698
  const checks = [
6633
6699
  [
6634
6700
  header.format === DUMP_FORMAT,
@@ -6638,10 +6704,7 @@ function validateDump({ header, sections }) {
6638
6704
  header.version === DUMP_VERSION,
6639
6705
  `Unsupported dump version ${header.version} (this build restores version ${DUMP_VERSION}).`
6640
6706
  ],
6641
- [
6642
- tableShape(header.tables ?? []) === tableShape(DUMP_TABLES),
6643
- "Dump table set does not match this build's backlog schema; cannot restore."
6644
- ],
6707
+ [tables.length > 0, "Dump header lists no tables; cannot restore."],
6645
6708
  [!missing, `Invalid dump: missing data section for "${missing}".`]
6646
6709
  ];
6647
6710
  const failure = checks.find(([ok]) => !ok);
@@ -6656,24 +6719,24 @@ async function countRows(client, table) {
6656
6719
  );
6657
6720
  return rows[0].n;
6658
6721
  }
6659
- function printSummary(current, incoming) {
6660
- const lines = DUMP_TABLES.map(
6722
+ function printSummary(tables, current, incoming) {
6723
+ const lines = tables.map(
6661
6724
  (t, i) => ` ${t.name}: ${current[i]} \u2192 ${incoming[i]} rows`
6662
6725
  );
6663
6726
  console.error(chalk57.bold("\nThis will REPLACE all backlog data:"));
6664
6727
  console.error(`${lines.join("\n")}
6665
6728
  `);
6666
6729
  }
6667
- async function confirmReplace(client, incoming, fromStdin) {
6730
+ async function confirmReplace(client, tables, incoming, fromStdin) {
6668
6731
  if (fromStdin) {
6669
6732
  throw new Error(
6670
6733
  "Reading a dump from stdin requires --yes (stdin is consumed by the dump)."
6671
6734
  );
6672
6735
  }
6673
6736
  const current = await Promise.all(
6674
- DUMP_TABLES.map((t) => countRows(client, t.name))
6737
+ tables.map((t) => countRows(client, t.name))
6675
6738
  );
6676
- printSummary(current, incoming);
6739
+ printSummary(tables, current, incoming);
6677
6740
  return promptConfirm("Replace all backlog data with this dump?", false);
6678
6741
  }
6679
6742
 
@@ -6696,23 +6759,48 @@ async function copyTableIn(client, table, data) {
6696
6759
  await finished(stream);
6697
6760
  }
6698
6761
 
6699
- // src/commands/backlog/import/restore.ts
6700
- var IDENTITY_TABLES = ["items", "comments"];
6701
- function resyncIdentitySql(table) {
6702
- return `SELECT setval(pg_get_serial_sequence('${table}', 'id'),
6703
- GREATEST(COALESCE((SELECT max(id) FROM ${table}), 0), 1),
6704
- (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
+ }));
6705
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
+ };
6789
+ }
6790
+
6791
+ // src/commands/backlog/import/restore.ts
6706
6792
  async function replaceData(client, parsed) {
6707
- const tables = DUMP_TABLES.map((t) => t.name).join(", ");
6793
+ const { tables } = parsed.header;
6794
+ const tableList = tables.map((t) => t.name).join(", ");
6708
6795
  await client.query(SCHEMA);
6709
- await client.query(`TRUNCATE ${tables} RESTART IDENTITY CASCADE`);
6710
- for (const table of DUMP_TABLES) {
6796
+ await client.query(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`);
6797
+ for (const table of tables) {
6711
6798
  const data = parsed.sections.get(table.name) ?? Buffer.alloc(0);
6712
6799
  await copyTableIn(client, table, data);
6713
6800
  }
6714
- for (const table of IDENTITY_TABLES) {
6715
- 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);
6716
6804
  }
6717
6805
  }
6718
6806
  async function restore(client, parsed) {
@@ -6731,11 +6819,12 @@ async function importBacklog(file, options2 = {}) {
6731
6819
  const raw = file ? await readFile(file) : await readStdinBuffer();
6732
6820
  const parsed = parseDump(raw);
6733
6821
  validateDump(parsed);
6734
- const incoming = DUMP_TABLES.map(
6822
+ const { tables } = parsed.header;
6823
+ const incoming = tables.map(
6735
6824
  (t) => countCopyRows(parsed.sections.get(t.name) ?? Buffer.alloc(0))
6736
6825
  );
6737
6826
  await withDbClient(async (client) => {
6738
- if (!options2.yes && !await confirmReplace(client, incoming, !file)) {
6827
+ if (!options2.yes && !await confirmReplace(client, tables, incoming, !file)) {
6739
6828
  console.error(chalk58.yellow("Import cancelled; no changes made."));
6740
6829
  return;
6741
6830
  }
@@ -6743,7 +6832,7 @@ async function importBacklog(file, options2 = {}) {
6743
6832
  const total = incoming.reduce((sum, n) => sum + n, 0);
6744
6833
  console.error(
6745
6834
  chalk58.green(
6746
- `Imported backlog: ${total} rows restored across ${DUMP_TABLES.length} tables.`
6835
+ `Imported backlog: ${total} rows restored across ${tables.length} tables.`
6747
6836
  )
6748
6837
  );
6749
6838
  });
@@ -6752,7 +6841,7 @@ async function importBacklog(file, options2 = {}) {
6752
6841
  // src/commands/backlog/registerImportCommand.ts
6753
6842
  function registerImportCommand(cmd) {
6754
6843
  cmd.command("import [file]").description(
6755
- "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)"
6756
6845
  ).option("-y, --yes", "Skip the confirmation prompt").action(
6757
6846
  (file, options2) => importBacklog(file, options2)
6758
6847
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.315.0",
3
+ "version": "0.316.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {