@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.
- package/README.md +2 -2
- package/dist/index.js +141 -59
- 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
|
|
149
|
-
- `assist backlog import [file]` - Restore
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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/
|
|
6707
|
-
var
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
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 =
|
|
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 ${
|
|
6717
|
-
for (const table of
|
|
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
|
|
6722
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
);
|