@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.
- package/README.md +2 -2
- package/dist/index.js +150 -61
- 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: {
|
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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/
|
|
6700
|
-
var
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
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 =
|
|
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 ${
|
|
6710
|
-
for (const table of
|
|
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
|
|
6715
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
);
|