@thxgg/steward 0.1.24 → 0.1.25
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/9ce7f1bc-d5e2-47bf-8026-f4910c257b2e.json +1 -0
- package/.output/server/chunks/_/prd-service.mjs.map +1 -1
- package/.output/server/chunks/nitro/nitro.mjs +818 -516
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/README.md +41 -0
- package/bin/prd +1 -1
- package/dist/host/src/index.js +10 -0
- package/dist/host/src/sync.js +201 -0
- package/dist/server/utils/db.js +64 -0
- package/dist/server/utils/prd-state.js +24 -2
- package/dist/server/utils/repos.js +12 -2
- package/dist/server/utils/state-migration.js +4 -3
- package/dist/server/utils/sync-apply.js +380 -0
- package/dist/server/utils/sync-export.js +183 -0
- package/dist/server/utils/sync-identity.js +231 -0
- package/dist/server/utils/sync-inspect.js +103 -0
- package/dist/server/utils/sync-merge.js +579 -0
- package/dist/server/utils/sync-schema.js +100 -0
- package/package.json +1 -1
- package/.output/public/_nuxt/builds/meta/8c342d49-fe70-4f67-a987-821c16f86125.json +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.mjs","sources":["../../../../node_modules/destr/dist/index.mjs","../../../../node_modules/ufo/dist/index.mjs","../../../../node_modules/radix3/dist/index.mjs","../../../../node_modules/defu/dist/defu.mjs","../../../../node_modules/node-mock-http/dist/index.mjs","../../../../node_modules/h3/dist/index.mjs","../../../../node_modules/hookable/dist/index.mjs","../../../../node_modules/node-fetch-native/dist/native.mjs","../../../../node_modules/ofetch/dist/shared/ofetch.CWycOUEr.mjs","../../../../node_modules/ofetch/dist/node.mjs","../../../../node_modules/unstorage/dist/shared/unstorage.zVDD2mZo.mjs","../../../../node_modules/unstorage/dist/index.mjs","../../../../node_modules/unstorage/drivers/utils/index.mjs","../../../../node_modules/unstorage/drivers/utils/node-fs.mjs","../../../../node_modules/unstorage/drivers/fs-lite.mjs","../../../../node_modules/nitropack/dist/runtime/internal/storage.mjs","../../../../node_modules/ohash/dist/shared/ohash.D__AXeF1.mjs","../../../../node_modules/ohash/dist/crypto/node/index.mjs","../../../../node_modules/ohash/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/hash.mjs","../../../../node_modules/nitropack/dist/runtime/internal/cache.mjs","../../../../node_modules/klona/dist/index.mjs","../../../../node_modules/scule/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.env.mjs","../../../../node_modules/nitropack/dist/runtime/internal/config.mjs","../../../../node_modules/unctx/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/context.mjs","../../../../node_modules/nitropack/dist/runtime/internal/route-rules.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/utils/error.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/handlers/error.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/utils.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/prod.mjs","../../../../node_modules/@nuxtjs/color-mode/dist/runtime/nitro-plugin.js","../../../../node_modules/nitropack/dist/runtime/internal/plugin.mjs","../../../../server/utils/db.ts","../../../../server/utils/change-events.ts","../../../../server/utils/git-repo-path.ts","../../../../server/utils/git.ts","../../../../server/utils/repos.ts","../../../../server/utils/state-schema.ts","../../../../server/utils/state-migration.ts","../../../../server/plugins/00-state-migration.ts","../../../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs","../../../../node_modules/nitropack/dist/runtime/internal/static.mjs","../../../../server/middleware/00-security.ts","../../../../node_modules/nitropack/dist/runtime/internal/app.mjs","../../../../node_modules/nitropack/dist/runtime/internal/renderer.mjs","../../../../node_modules/nitropack/dist/runtime/internal/lib/http-graceful-shutdown.mjs","../../../../node_modules/nitropack/dist/runtime/internal/shutdown.mjs","../../../../node_modules/nitropack/dist/presets/node/runtime/node-server.mjs"],"names":["getQuery","createRouter","f","h","c","i","l","createError","mergeHeaders","s","nodeFetch","Headers","Headers$1","AbortController$1","normalizeKey","defineDriver","DRIVER_NAME","dirname","fsPromises","resolve","fsp","serialize","hash","_inlineAppConfig","createRadixRouter","fs","normalizePathSlashes","isAbsolute","isGitRepo","nitroApp","callNodeRequestHandler","fetchNodeRequestHandler","gracefulShutdown","HttpsServer","HttpServer"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,
|
|
1
|
+
{"version":3,"file":"nitro.mjs","sources":["../../../../node_modules/destr/dist/index.mjs","../../../../node_modules/ufo/dist/index.mjs","../../../../node_modules/radix3/dist/index.mjs","../../../../node_modules/defu/dist/defu.mjs","../../../../node_modules/node-mock-http/dist/index.mjs","../../../../node_modules/h3/dist/index.mjs","../../../../node_modules/hookable/dist/index.mjs","../../../../node_modules/node-fetch-native/dist/native.mjs","../../../../node_modules/ofetch/dist/shared/ofetch.CWycOUEr.mjs","../../../../node_modules/ofetch/dist/node.mjs","../../../../node_modules/unstorage/dist/shared/unstorage.zVDD2mZo.mjs","../../../../node_modules/unstorage/dist/index.mjs","../../../../node_modules/unstorage/drivers/utils/index.mjs","../../../../node_modules/unstorage/drivers/utils/node-fs.mjs","../../../../node_modules/unstorage/drivers/fs-lite.mjs","../../../../node_modules/nitropack/dist/runtime/internal/storage.mjs","../../../../node_modules/ohash/dist/shared/ohash.D__AXeF1.mjs","../../../../node_modules/ohash/dist/crypto/node/index.mjs","../../../../node_modules/ohash/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/hash.mjs","../../../../node_modules/nitropack/dist/runtime/internal/cache.mjs","../../../../node_modules/klona/dist/index.mjs","../../../../node_modules/scule/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.env.mjs","../../../../node_modules/nitropack/dist/runtime/internal/config.mjs","../../../../node_modules/unctx/dist/index.mjs","../../../../node_modules/nitropack/dist/runtime/internal/context.mjs","../../../../node_modules/nitropack/dist/runtime/internal/route-rules.mjs","../../../../node_modules/nitropack/dist/runtime/internal/utils.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/utils/error.mjs","../../../../node_modules/@nuxt/nitro-server/dist/runtime/handlers/error.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/utils.mjs","../../../../node_modules/nitropack/dist/runtime/internal/error/prod.mjs","../../../../node_modules/@nuxtjs/color-mode/dist/runtime/nitro-plugin.js","../../../../node_modules/nitropack/dist/runtime/internal/plugin.mjs","../../../../server/utils/db.ts","../../../../server/utils/change-events.ts","../../../../server/utils/git-repo-path.ts","../../../../server/utils/git.ts","../../../../server/utils/sync-identity.ts","../../../../server/utils/repos.ts","../../../../server/utils/state-schema.ts","../../../../server/utils/state-migration.ts","../../../../server/plugins/00-state-migration.ts","../../../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs","../../../../node_modules/nitropack/dist/runtime/internal/static.mjs","../../../../server/middleware/00-security.ts","../../../../node_modules/nitropack/dist/runtime/internal/app.mjs","../../../../node_modules/nitropack/dist/runtime/internal/renderer.mjs","../../../../node_modules/nitropack/dist/runtime/internal/lib/http-graceful-shutdown.mjs","../../../../node_modules/nitropack/dist/runtime/internal/shutdown.mjs","../../../../node_modules/nitropack/dist/presets/node/runtime/node-server.mjs"],"names":["getQuery","createRouter","f","h","c","i","l","createError","mergeHeaders","s","nodeFetch","Headers","Headers$1","AbortController$1","normalizeKey","defineDriver","DRIVER_NAME","dirname","fsPromises","resolve","fsp","serialize","hash","_inlineAppConfig","createRadixRouter","fs","normalizePathSlashes","isAbsolute","execGit","isGitRepo","nowIso","nitroApp","callNodeRequestHandler","fetchNodeRequestHandler","gracefulShutdown","HttpsServer","HttpServer"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,44,45,47,48,49,50,51]}
|
package/README.md
CHANGED
|
@@ -58,14 +58,55 @@ Note: `execute` runs in a VM sandbox by design, so globals like `process` are in
|
|
|
58
58
|
prd ui
|
|
59
59
|
prd ui --port 3100 --host 127.0.0.1
|
|
60
60
|
prd mcp
|
|
61
|
+
prd sync export ./steward-sync.json
|
|
62
|
+
prd sync inspect ./steward-sync.json
|
|
63
|
+
prd sync merge ./steward-sync.json
|
|
64
|
+
prd sync merge ./steward-sync.json --apply
|
|
65
|
+
prd sync merge ./steward-sync.json --apply --map rsk_source=/Users/you/Projects/repo
|
|
61
66
|
```
|
|
62
67
|
|
|
68
|
+
### Sync Bundles (Cross-Device)
|
|
69
|
+
|
|
70
|
+
Steward supports local-first state sharing across devices using portable JSON bundles.
|
|
71
|
+
|
|
72
|
+
- `prd sync export <bundle-path>` writes a versioned bundle of repos/state/archives.
|
|
73
|
+
- `prd sync inspect <bundle-path>` validates and summarizes bundle contents.
|
|
74
|
+
- `prd sync merge <bundle-path>` plans a merge in dry-run mode by default.
|
|
75
|
+
- `prd sync merge <bundle-path> --apply` applies the planned merge transactionally.
|
|
76
|
+
|
|
77
|
+
Path hint privacy defaults:
|
|
78
|
+
|
|
79
|
+
- Export defaults to `--path-hints basename` to avoid leaking absolute filesystem paths.
|
|
80
|
+
- Use `--path-hints none` to omit path hints entirely.
|
|
81
|
+
- Use `--path-hints absolute` only when you explicitly want full paths in the bundle.
|
|
82
|
+
|
|
83
|
+
Representative command output summaries:
|
|
84
|
+
|
|
85
|
+
- `sync export` prints bundle path, bundle id, and row totals (`repos/states/archives`).
|
|
86
|
+
- `sync inspect` prints bundle metadata (`bundleId`, `sourceDeviceId`, format version), totals, and unknown references.
|
|
87
|
+
- `sync merge` prints mapping totals, state/archive action counts, and conflict counts.
|
|
88
|
+
- `sync merge --apply` also prints backup path and retention cleanup counts.
|
|
89
|
+
|
|
90
|
+
Safety defaults and retention:
|
|
91
|
+
|
|
92
|
+
- Merge defaults to dry-run; no writes happen unless `--apply` is provided.
|
|
93
|
+
- Apply creates a SQLite backup before any write and runs an integrity check before commit.
|
|
94
|
+
- Re-applying the same bundle id is idempotent and returns a no-op result.
|
|
95
|
+
- Default retention keeps backups for 30 days (max 20 files) and sync apply logs for 180 days (max 10,000 rows).
|
|
96
|
+
|
|
97
|
+
Troubleshooting sync:
|
|
98
|
+
|
|
99
|
+
- Unresolved mapping on apply: run `prd sync inspect <bundle-path>` and pass one or more `--map <incomingRepoSyncKey>=<localPathOrRepoRef>` values.
|
|
100
|
+
- Expected no-op reapply: if bundle id was already applied, merge returns "already applied" and leaves state unchanged.
|
|
101
|
+
- Restore from backup: stop Steward processes, then replace your DB file with a backup in the same directory (files match `state.db.sync-backup.*.db`).
|
|
102
|
+
|
|
63
103
|
## Architecture
|
|
64
104
|
|
|
65
105
|
```
|
|
66
106
|
┌─────────────────────────────────────────┐
|
|
67
107
|
│ Steward CLI (Node) │
|
|
68
108
|
│ - `prd ui` runs prebuilt UI server │
|
|
109
|
+
│ - `prd sync` manages state bundles │
|
|
69
110
|
│ - `prd mcp` starts MCP over stdio │
|
|
70
111
|
└─────────────────────────────────────────┘
|
|
71
112
|
│
|
package/bin/prd
CHANGED
|
@@ -12,7 +12,7 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
12
12
|
const entryPath = resolve(packageRoot, 'dist', 'host', 'src', 'index.js')
|
|
13
13
|
|
|
14
14
|
function requiresSqliteRuntime(command) {
|
|
15
|
-
return command === 'mcp' || command === 'ui'
|
|
15
|
+
return command === 'mcp' || command === 'ui' || command === 'sync'
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function isSqliteRuntimeError(error) {
|
package/dist/host/src/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { runMcpServer } from './mcp.js';
|
|
2
|
+
import { runSync } from './sync.js';
|
|
2
3
|
import { runUi } from './ui.js';
|
|
3
4
|
function printUsage() {
|
|
4
5
|
console.log(`prd - Steward CLI
|
|
5
6
|
|
|
6
7
|
Usage:
|
|
7
8
|
prd ui [--preview] [--port <port>] [--host <host>]
|
|
9
|
+
prd sync <subcommand> [options]
|
|
8
10
|
prd mcp
|
|
9
11
|
|
|
10
12
|
Commands:
|
|
11
13
|
ui Launch the prebuilt PRD web UI server
|
|
14
|
+
sync Export/inspect/merge sync bundles
|
|
12
15
|
mcp Start MCP server over stdio (codemode)
|
|
13
16
|
|
|
14
17
|
Options:
|
|
@@ -84,6 +87,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
84
87
|
}
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
90
|
+
if (command === 'sync') {
|
|
91
|
+
const exitCode = await runSync(rest);
|
|
92
|
+
if (exitCode !== 0) {
|
|
93
|
+
process.exitCode = exitCode;
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
87
97
|
throw new Error(`Unknown command: ${command}`);
|
|
88
98
|
}
|
|
89
99
|
if (import.meta.main) {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { buildSyncBundle, serializeSyncBundle } from '../../server/utils/sync-export.js';
|
|
4
|
+
import { executeSyncMergeJson } from '../../server/utils/sync-apply.js';
|
|
5
|
+
import { inspectSyncBundleJson } from '../../server/utils/sync-inspect.js';
|
|
6
|
+
function printSyncUsage() {
|
|
7
|
+
console.log(`Sync Usage:
|
|
8
|
+
prd sync export <bundle-path> [--path-hints basename|none|absolute]
|
|
9
|
+
prd sync inspect <bundle-path>
|
|
10
|
+
prd sync merge <bundle-path> [--map <incomingRepoSyncKey>=<localPathOrRepoRef>] [--dry-run] [--apply]
|
|
11
|
+
|
|
12
|
+
Notes:
|
|
13
|
+
- merge defaults to --dry-run when --apply is omitted
|
|
14
|
+
- --map can be provided multiple times
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
function resolveBundlePath(pathInput) {
|
|
18
|
+
return resolve(process.cwd(), pathInput);
|
|
19
|
+
}
|
|
20
|
+
function parsePathHintsMode(rawValue) {
|
|
21
|
+
if (rawValue === 'basename' || rawValue === 'none' || rawValue === 'absolute') {
|
|
22
|
+
return rawValue;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Invalid --path-hints value: ${rawValue}`);
|
|
25
|
+
}
|
|
26
|
+
function parseMapAssignment(rawValue) {
|
|
27
|
+
const separatorIndex = rawValue.indexOf('=');
|
|
28
|
+
if (separatorIndex <= 0 || separatorIndex >= rawValue.length - 1) {
|
|
29
|
+
throw new Error(`Invalid --map value: ${rawValue}. Expected <incomingRepoSyncKey>=<localPathOrRepoRef>`);
|
|
30
|
+
}
|
|
31
|
+
const source = rawValue.slice(0, separatorIndex).trim();
|
|
32
|
+
const target = rawValue.slice(separatorIndex + 1).trim();
|
|
33
|
+
if (!source || !target) {
|
|
34
|
+
throw new Error(`Invalid --map value: ${rawValue}. Expected <incomingRepoSyncKey>=<localPathOrRepoRef>`);
|
|
35
|
+
}
|
|
36
|
+
return { source, target };
|
|
37
|
+
}
|
|
38
|
+
export function parseSyncExportArgs(args) {
|
|
39
|
+
let bundlePath = null;
|
|
40
|
+
let pathHints = 'basename';
|
|
41
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
42
|
+
const arg = args[index];
|
|
43
|
+
if (!arg) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === '--path-hints') {
|
|
47
|
+
const next = args[index + 1];
|
|
48
|
+
if (!next) {
|
|
49
|
+
throw new Error('--path-hints requires a value');
|
|
50
|
+
}
|
|
51
|
+
pathHints = parsePathHintsMode(next);
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg.startsWith('--')) {
|
|
56
|
+
throw new Error(`Unknown option for sync export: ${arg}`);
|
|
57
|
+
}
|
|
58
|
+
if (bundlePath !== null) {
|
|
59
|
+
throw new Error(`Unexpected argument for sync export: ${arg}`);
|
|
60
|
+
}
|
|
61
|
+
bundlePath = arg;
|
|
62
|
+
}
|
|
63
|
+
if (!bundlePath) {
|
|
64
|
+
throw new Error('sync export requires <bundle-path>');
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
bundlePath: resolveBundlePath(bundlePath),
|
|
68
|
+
pathHints
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function parseSyncInspectArgs(args) {
|
|
72
|
+
const [bundlePath] = args;
|
|
73
|
+
if (args.length !== 1 || !bundlePath || bundlePath.startsWith('--')) {
|
|
74
|
+
throw new Error('sync inspect requires exactly one <bundle-path>');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
bundlePath: resolveBundlePath(bundlePath)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function parseSyncMergeArgs(args) {
|
|
81
|
+
let bundlePath = null;
|
|
82
|
+
let apply = false;
|
|
83
|
+
let dryRun = false;
|
|
84
|
+
const repoMap = {};
|
|
85
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
86
|
+
const arg = args[index];
|
|
87
|
+
if (!arg) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg === '--apply') {
|
|
91
|
+
apply = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === '--dry-run') {
|
|
95
|
+
dryRun = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === '--map') {
|
|
99
|
+
const next = args[index + 1];
|
|
100
|
+
if (!next) {
|
|
101
|
+
throw new Error('--map requires a value');
|
|
102
|
+
}
|
|
103
|
+
const { source, target } = parseMapAssignment(next);
|
|
104
|
+
repoMap[source] = target;
|
|
105
|
+
index += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg.startsWith('--')) {
|
|
109
|
+
throw new Error(`Unknown option for sync merge: ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
if (bundlePath !== null) {
|
|
112
|
+
throw new Error(`Unexpected argument for sync merge: ${arg}`);
|
|
113
|
+
}
|
|
114
|
+
bundlePath = arg;
|
|
115
|
+
}
|
|
116
|
+
if (!bundlePath) {
|
|
117
|
+
throw new Error('sync merge requires <bundle-path>');
|
|
118
|
+
}
|
|
119
|
+
if (apply && dryRun) {
|
|
120
|
+
throw new Error('Cannot use --apply and --dry-run together');
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
bundlePath: resolveBundlePath(bundlePath),
|
|
124
|
+
apply,
|
|
125
|
+
repoMap
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function runSyncExport(args) {
|
|
129
|
+
const parsed = parseSyncExportArgs(args);
|
|
130
|
+
const bundle = await buildSyncBundle({
|
|
131
|
+
pathHints: parsed.pathHints
|
|
132
|
+
});
|
|
133
|
+
await fs.mkdir(dirname(parsed.bundlePath), { recursive: true });
|
|
134
|
+
await fs.writeFile(parsed.bundlePath, `${serializeSyncBundle(bundle)}\n`, 'utf-8');
|
|
135
|
+
console.log(`[steward] Exported bundle: ${parsed.bundlePath}`);
|
|
136
|
+
console.log(`[steward] Bundle ID: ${bundle.bundleId}`);
|
|
137
|
+
console.log(`[steward] Rows: repos=${bundle.repos.length} states=${bundle.states.length} archives=${bundle.archives.length}`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
async function runSyncInspect(args) {
|
|
141
|
+
const parsed = parseSyncInspectArgs(args);
|
|
142
|
+
const jsonPayload = await fs.readFile(parsed.bundlePath, 'utf-8');
|
|
143
|
+
const inspection = inspectSyncBundleJson(jsonPayload);
|
|
144
|
+
console.log(`[steward] Bundle: ${parsed.bundlePath}`);
|
|
145
|
+
console.log(`[steward] ID=${inspection.bundleId} sourceDevice=${inspection.sourceDeviceId} format=v${inspection.formatVersion}`);
|
|
146
|
+
console.log(`[steward] Totals: repos=${inspection.totals.repos} states=${inspection.totals.states} archives=${inspection.totals.archives}`);
|
|
147
|
+
console.log(`[steward] Unknown references: states=${inspection.totals.unknownRepoStates} archives=${inspection.totals.unknownRepoArchives}`);
|
|
148
|
+
for (const repo of inspection.repos) {
|
|
149
|
+
const hint = repo.pathHint ? ` pathHint=${repo.pathHint}` : '';
|
|
150
|
+
console.log(`[steward] Repo ${repo.repoSyncKey} (${repo.name})${hint} states=${repo.stateCount} archives=${repo.archiveCount}`);
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
async function runSyncMerge(args) {
|
|
155
|
+
const parsed = parseSyncMergeArgs(args);
|
|
156
|
+
const jsonPayload = await fs.readFile(parsed.bundlePath, 'utf-8');
|
|
157
|
+
const result = await executeSyncMergeJson(jsonPayload, {
|
|
158
|
+
apply: parsed.apply,
|
|
159
|
+
repoMap: parsed.repoMap
|
|
160
|
+
});
|
|
161
|
+
const mode = result.mode === 'dry_run' ? 'dry-run' : 'apply';
|
|
162
|
+
console.log(`[steward] Merge mode: ${mode}`);
|
|
163
|
+
console.log(`[steward] Bundle ID: ${result.bundleId}`);
|
|
164
|
+
console.log(`[steward] Repo mapping: mapped=${result.plan.summary.repos.mapped} unresolved=${result.plan.summary.repos.unresolved}`);
|
|
165
|
+
console.log(`[steward] State actions: insert=${result.plan.summary.states.insert} update=${result.plan.summary.states.update} skip=${result.plan.summary.states.skip} unresolved=${result.plan.summary.states.unresolved} conflicts=${result.plan.summary.states.conflicts}`);
|
|
166
|
+
console.log(`[steward] Archive actions: insert=${result.plan.summary.archives.insert} update=${result.plan.summary.archives.update} skip=${result.plan.summary.archives.skip} unresolved=${result.plan.summary.archives.unresolved}`);
|
|
167
|
+
if (result.mode === 'apply') {
|
|
168
|
+
if (result.alreadyApplied) {
|
|
169
|
+
console.log('[steward] Bundle already applied; no changes were written.');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(`[steward] Applied: ${result.applied ? 'yes' : 'no'}`);
|
|
173
|
+
if (result.backupPath) {
|
|
174
|
+
console.log(`[steward] Backup: ${result.backupPath}`);
|
|
175
|
+
}
|
|
176
|
+
console.log(`[steward] Retention: backupsDeleted=${result.retention.backupsDeleted} logsDeleted=${result.retention.logsDeleted}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log('[steward] Dry-run only; re-run with --apply to write changes.');
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
export async function runSync(args) {
|
|
185
|
+
const [subcommandRaw, ...rest] = args;
|
|
186
|
+
const subcommand = subcommandRaw || '';
|
|
187
|
+
if (subcommand === '' || subcommand === '-h' || subcommand === '--help') {
|
|
188
|
+
printSyncUsage();
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (subcommand === 'export') {
|
|
192
|
+
return await runSyncExport(rest);
|
|
193
|
+
}
|
|
194
|
+
if (subcommand === 'inspect') {
|
|
195
|
+
return await runSyncInspect(rest);
|
|
196
|
+
}
|
|
197
|
+
if (subcommand === 'merge') {
|
|
198
|
+
return await runSyncMerge(rest);
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Unknown sync subcommand: ${subcommandRaw}`);
|
|
201
|
+
}
|
package/dist/server/utils/db.js
CHANGED
|
@@ -34,6 +34,47 @@ function resolveDbPath() {
|
|
|
34
34
|
}
|
|
35
35
|
return DEFAULT_DB_PATH;
|
|
36
36
|
}
|
|
37
|
+
function getTableColumnNames(adapter, tableName) {
|
|
38
|
+
const rows = adapter.all(`PRAGMA table_info(${tableName})`);
|
|
39
|
+
const names = new Set();
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
if (typeof row.name === 'string' && row.name.length > 0) {
|
|
42
|
+
names.add(row.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
function ensurePrdStateFieldClockColumns(adapter) {
|
|
48
|
+
const columnNames = getTableColumnNames(adapter, 'prd_states');
|
|
49
|
+
const requiredColumns = ['tasks_updated_at', 'progress_updated_at', 'notes_updated_at'];
|
|
50
|
+
for (const columnName of requiredColumns) {
|
|
51
|
+
if (columnNames.has(columnName)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
adapter.exec(`ALTER TABLE prd_states ADD COLUMN ${columnName} TEXT;`);
|
|
55
|
+
columnNames.add(columnName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function backfillPrdStateFieldClocks(adapter) {
|
|
59
|
+
adapter.run(`
|
|
60
|
+
UPDATE prd_states
|
|
61
|
+
SET tasks_updated_at = updated_at
|
|
62
|
+
WHERE tasks_updated_at IS NULL
|
|
63
|
+
AND tasks_json IS NOT NULL
|
|
64
|
+
`);
|
|
65
|
+
adapter.run(`
|
|
66
|
+
UPDATE prd_states
|
|
67
|
+
SET progress_updated_at = updated_at
|
|
68
|
+
WHERE progress_updated_at IS NULL
|
|
69
|
+
AND progress_json IS NOT NULL
|
|
70
|
+
`);
|
|
71
|
+
adapter.run(`
|
|
72
|
+
UPDATE prd_states
|
|
73
|
+
SET notes_updated_at = updated_at
|
|
74
|
+
WHERE notes_updated_at IS NULL
|
|
75
|
+
AND notes_md IS NOT NULL
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
37
78
|
function formatNodeRuntimeHint() {
|
|
38
79
|
const nodeOptions = process.env.NODE_OPTIONS || '';
|
|
39
80
|
const hasDisableFlag = process.execArgv.includes(SQLITE_DISABLE_FLAG)
|
|
@@ -177,6 +218,9 @@ async function initializeDatabase() {
|
|
|
177
218
|
tasks_json TEXT,
|
|
178
219
|
progress_json TEXT,
|
|
179
220
|
notes_md TEXT,
|
|
221
|
+
tasks_updated_at TEXT,
|
|
222
|
+
progress_updated_at TEXT,
|
|
223
|
+
notes_updated_at TEXT,
|
|
180
224
|
updated_at TEXT NOT NULL,
|
|
181
225
|
PRIMARY KEY (repo_id, slug),
|
|
182
226
|
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
|
|
@@ -196,9 +240,29 @@ async function initializeDatabase() {
|
|
|
196
240
|
updated_at TEXT NOT NULL
|
|
197
241
|
);
|
|
198
242
|
|
|
243
|
+
CREATE TABLE IF NOT EXISTS repo_sync_meta (
|
|
244
|
+
repo_id TEXT PRIMARY KEY,
|
|
245
|
+
sync_key TEXT NOT NULL UNIQUE,
|
|
246
|
+
fingerprint TEXT,
|
|
247
|
+
fingerprint_kind TEXT,
|
|
248
|
+
updated_at TEXT NOT NULL,
|
|
249
|
+
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
CREATE TABLE IF NOT EXISTS sync_bundle_log (
|
|
253
|
+
bundle_id TEXT PRIMARY KEY,
|
|
254
|
+
source_device_id TEXT,
|
|
255
|
+
applied_at TEXT NOT NULL,
|
|
256
|
+
summary_json TEXT NOT NULL
|
|
257
|
+
);
|
|
258
|
+
|
|
199
259
|
CREATE INDEX IF NOT EXISTS idx_prd_states_repo_id ON prd_states(repo_id);
|
|
200
260
|
CREATE INDEX IF NOT EXISTS idx_prd_archives_repo_id ON prd_archives(repo_id);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_repo_sync_meta_sync_key ON repo_sync_meta(sync_key);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_sync_bundle_log_applied_at ON sync_bundle_log(applied_at);
|
|
201
263
|
`);
|
|
264
|
+
ensurePrdStateFieldClockColumns(adapter);
|
|
265
|
+
backfillPrdStateFieldClocks(adapter);
|
|
202
266
|
return adapter;
|
|
203
267
|
}
|
|
204
268
|
async function getAdapter() {
|
|
@@ -92,13 +92,29 @@ export async function upsertPrdState(repoId, slug, update) {
|
|
|
92
92
|
? null
|
|
93
93
|
: update.notes;
|
|
94
94
|
const updatedAt = new Date().toISOString();
|
|
95
|
+
const tasksUpdatedAt = updateTasks ? updatedAt : null;
|
|
96
|
+
const progressUpdatedAt = updateProgress ? updatedAt : null;
|
|
97
|
+
const notesUpdatedAt = updateNotes ? updatedAt : null;
|
|
95
98
|
await dbRun(`
|
|
96
|
-
INSERT INTO prd_states (
|
|
97
|
-
|
|
99
|
+
INSERT INTO prd_states (
|
|
100
|
+
repo_id,
|
|
101
|
+
slug,
|
|
102
|
+
tasks_json,
|
|
103
|
+
progress_json,
|
|
104
|
+
notes_md,
|
|
105
|
+
tasks_updated_at,
|
|
106
|
+
progress_updated_at,
|
|
107
|
+
notes_updated_at,
|
|
108
|
+
updated_at
|
|
109
|
+
)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
98
111
|
ON CONFLICT(repo_id, slug) DO UPDATE SET
|
|
99
112
|
tasks_json = CASE WHEN ? THEN excluded.tasks_json ELSE prd_states.tasks_json END,
|
|
100
113
|
progress_json = CASE WHEN ? THEN excluded.progress_json ELSE prd_states.progress_json END,
|
|
101
114
|
notes_md = CASE WHEN ? THEN excluded.notes_md ELSE prd_states.notes_md END,
|
|
115
|
+
tasks_updated_at = CASE WHEN ? THEN excluded.tasks_updated_at ELSE prd_states.tasks_updated_at END,
|
|
116
|
+
progress_updated_at = CASE WHEN ? THEN excluded.progress_updated_at ELSE prd_states.progress_updated_at END,
|
|
117
|
+
notes_updated_at = CASE WHEN ? THEN excluded.notes_updated_at ELSE prd_states.notes_updated_at END,
|
|
102
118
|
updated_at = excluded.updated_at
|
|
103
119
|
`, [
|
|
104
120
|
repoId,
|
|
@@ -106,9 +122,15 @@ export async function upsertPrdState(repoId, slug, update) {
|
|
|
106
122
|
tasksJson,
|
|
107
123
|
progressJson,
|
|
108
124
|
notesMd,
|
|
125
|
+
tasksUpdatedAt,
|
|
126
|
+
progressUpdatedAt,
|
|
127
|
+
notesUpdatedAt,
|
|
109
128
|
updatedAt,
|
|
110
129
|
updateTasks ? 1 : 0,
|
|
111
130
|
updateProgress ? 1 : 0,
|
|
131
|
+
updateNotes ? 1 : 0,
|
|
132
|
+
updateTasks ? 1 : 0,
|
|
133
|
+
updateProgress ? 1 : 0,
|
|
112
134
|
updateNotes ? 1 : 0
|
|
113
135
|
]);
|
|
114
136
|
if (validatedTasks !== undefined) {
|
|
@@ -3,6 +3,7 @@ import { join, basename, dirname, resolve, relative, isAbsolute } from 'node:pat
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { dbAll, dbGet, dbRun } from './db.js';
|
|
6
|
+
import { ensureRepoSyncMetaForRepo, ensureRepoSyncMetaForRepos } from './sync-identity.js';
|
|
6
7
|
function findPackageRoot(startDir) {
|
|
7
8
|
let currentDir = startDir;
|
|
8
9
|
while (true) {
|
|
@@ -136,7 +137,9 @@ async function importLegacyReposIfNeeded() {
|
|
|
136
137
|
export async function getRepos() {
|
|
137
138
|
await importLegacyReposIfNeeded();
|
|
138
139
|
const rows = await dbAll('SELECT id, name, path, added_at, git_repos_json FROM repos ORDER BY added_at ASC');
|
|
139
|
-
|
|
140
|
+
const repos = rows.map(rowToRepo);
|
|
141
|
+
await ensureRepoSyncMetaForRepos(repos);
|
|
142
|
+
return repos;
|
|
140
143
|
}
|
|
141
144
|
export async function saveRepos(repos) {
|
|
142
145
|
await importLegacyReposIfNeeded();
|
|
@@ -158,6 +161,7 @@ export async function saveRepos(repos) {
|
|
|
158
161
|
const repoIds = repos.map(repo => repo.id);
|
|
159
162
|
const placeholders = repoIds.map(() => '?').join(', ');
|
|
160
163
|
await dbRun(`DELETE FROM repos WHERE id NOT IN (${placeholders})`, repoIds);
|
|
164
|
+
await ensureRepoSyncMetaForRepos(repos);
|
|
161
165
|
}
|
|
162
166
|
export async function addRepo(path, name) {
|
|
163
167
|
await importLegacyReposIfNeeded();
|
|
@@ -175,6 +179,7 @@ export async function addRepo(path, name) {
|
|
|
175
179
|
...(gitRepos.length > 0 && { gitRepos })
|
|
176
180
|
};
|
|
177
181
|
await dbRun('INSERT INTO repos (id, name, path, added_at, git_repos_json) VALUES (?, ?, ?, ?, ?)', [repo.id, repo.name, repo.path, repo.addedAt, serializeGitRepos(repo.gitRepos)]);
|
|
182
|
+
await ensureRepoSyncMetaForRepo(repo);
|
|
178
183
|
return repo;
|
|
179
184
|
}
|
|
180
185
|
/**
|
|
@@ -183,7 +188,12 @@ export async function addRepo(path, name) {
|
|
|
183
188
|
export async function getRepoById(id) {
|
|
184
189
|
await importLegacyReposIfNeeded();
|
|
185
190
|
const row = await dbGet('SELECT id, name, path, added_at, git_repos_json FROM repos WHERE id = ?', [id]);
|
|
186
|
-
|
|
191
|
+
if (!row) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const repo = rowToRepo(row);
|
|
195
|
+
await ensureRepoSyncMetaForRepo(repo);
|
|
196
|
+
return repo;
|
|
187
197
|
}
|
|
188
198
|
export async function updateRepoGitRepos(id, gitRepos) {
|
|
189
199
|
await importLegacyReposIfNeeded();
|
|
@@ -209,9 +209,9 @@ async function migrateProgressRows() {
|
|
|
209
209
|
const updatedAt = nowIso();
|
|
210
210
|
await dbRun(`
|
|
211
211
|
UPDATE prd_states
|
|
212
|
-
SET progress_json = ?, updated_at = ?
|
|
212
|
+
SET progress_json = ?, progress_updated_at = ?, updated_at = ?
|
|
213
213
|
WHERE repo_id = ? AND slug = ?
|
|
214
|
-
`, [JSON.stringify(normalized), updatedAt, row.repo_id, row.slug]);
|
|
214
|
+
`, [JSON.stringify(normalized), updatedAt, updatedAt, row.repo_id, row.slug]);
|
|
215
215
|
status.migratedRows += 1;
|
|
216
216
|
emitChange({
|
|
217
217
|
type: 'change',
|
|
@@ -362,7 +362,7 @@ async function migrateCommitRepoRefs() {
|
|
|
362
362
|
const updatedAt = nowIso();
|
|
363
363
|
await dbRun(`
|
|
364
364
|
UPDATE prd_states
|
|
365
|
-
SET progress_json = ?, updated_at = ?
|
|
365
|
+
SET progress_json = ?, progress_updated_at = ?, updated_at = ?
|
|
366
366
|
WHERE repo_id = ? AND slug = ?
|
|
367
367
|
`, [
|
|
368
368
|
JSON.stringify({
|
|
@@ -370,6 +370,7 @@ async function migrateCommitRepoRefs() {
|
|
|
370
370
|
taskLogs: normalized.taskLogs
|
|
371
371
|
}),
|
|
372
372
|
updatedAt,
|
|
373
|
+
updatedAt,
|
|
373
374
|
row.repo_id,
|
|
374
375
|
row.slug
|
|
375
376
|
]);
|