@hasna/todos 0.11.15 → 0.11.16
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/dashboard/dist/assets/index-B-w1tUlm.js +346 -0
- package/dashboard/dist/assets/index-BXQ39iMX.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/cli/index.js +850 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/machines.d.ts +17 -0
- package/dist/db/machines.d.ts.map +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -0
- package/dist/mcp/index.js +12744 -12259
- package/dist/mcp/tools/cloud.d.ts +12 -0
- package/dist/mcp/tools/cloud.d.ts.map +1 -0
- package/dist/server/index.js +4933 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2260,6 +2260,13 @@ function ensureSchema(db) {
|
|
|
2260
2260
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2261
2261
|
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
2262
2262
|
)`);
|
|
2263
|
+
ensureTable("machines", `
|
|
2264
|
+
CREATE TABLE machines (
|
|
2265
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
|
|
2266
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2267
|
+
metadata TEXT DEFAULT '{}',
|
|
2268
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2269
|
+
)`);
|
|
2263
2270
|
ensureColumn("projects", "task_list_id", "TEXT");
|
|
2264
2271
|
ensureColumn("projects", "task_prefix", "TEXT");
|
|
2265
2272
|
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
@@ -2360,6 +2367,38 @@ function ensureSchema(db) {
|
|
|
2360
2367
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
2361
2368
|
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
2362
2369
|
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
2370
|
+
ensureColumn("projects", "machine_id", "TEXT");
|
|
2371
|
+
ensureColumn("projects", "synced_at", "TEXT");
|
|
2372
|
+
ensureColumn("tasks", "machine_id", "TEXT");
|
|
2373
|
+
ensureColumn("tasks", "synced_at", "TEXT");
|
|
2374
|
+
ensureColumn("agents", "machine_id", "TEXT");
|
|
2375
|
+
ensureColumn("agents", "synced_at", "TEXT");
|
|
2376
|
+
ensureColumn("task_lists", "machine_id", "TEXT");
|
|
2377
|
+
ensureColumn("task_lists", "synced_at", "TEXT");
|
|
2378
|
+
ensureColumn("plans", "machine_id", "TEXT");
|
|
2379
|
+
ensureColumn("plans", "synced_at", "TEXT");
|
|
2380
|
+
ensureColumn("task_comments", "machine_id", "TEXT");
|
|
2381
|
+
ensureColumn("task_comments", "synced_at", "TEXT");
|
|
2382
|
+
ensureColumn("sessions", "machine_id", "TEXT");
|
|
2383
|
+
ensureColumn("sessions", "synced_at", "TEXT");
|
|
2384
|
+
ensureColumn("task_history", "machine_id", "TEXT");
|
|
2385
|
+
ensureColumn("webhooks", "machine_id", "TEXT");
|
|
2386
|
+
ensureColumn("webhooks", "synced_at", "TEXT");
|
|
2387
|
+
ensureColumn("task_templates", "machine_id", "TEXT");
|
|
2388
|
+
ensureColumn("task_templates", "synced_at", "TEXT");
|
|
2389
|
+
ensureColumn("orgs", "machine_id", "TEXT");
|
|
2390
|
+
ensureColumn("orgs", "synced_at", "TEXT");
|
|
2391
|
+
ensureColumn("handoffs", "machine_id", "TEXT");
|
|
2392
|
+
ensureColumn("handoffs", "synced_at", "TEXT");
|
|
2393
|
+
ensureColumn("task_checklists", "machine_id", "TEXT");
|
|
2394
|
+
ensureColumn("project_sources", "machine_id", "TEXT");
|
|
2395
|
+
ensureColumn("project_sources", "synced_at", "TEXT");
|
|
2396
|
+
ensureColumn("task_files", "machine_id", "TEXT");
|
|
2397
|
+
ensureColumn("task_relationships", "machine_id", "TEXT");
|
|
2398
|
+
ensureColumn("kg_edges", "machine_id", "TEXT");
|
|
2399
|
+
ensureColumn("project_agent_roles", "machine_id", "TEXT");
|
|
2400
|
+
ensureColumn("dispatches", "machine_id", "TEXT");
|
|
2401
|
+
ensureColumn("dispatches", "synced_at", "TEXT");
|
|
2363
2402
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
2364
2403
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
2365
2404
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
@@ -2388,6 +2427,10 @@ function ensureSchema(db) {
|
|
|
2388
2427
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
2389
2428
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
2390
2429
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
2430
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
|
|
2431
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
|
|
2432
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
|
|
2433
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
|
|
2391
2434
|
}
|
|
2392
2435
|
function backfillTaskTags(db) {
|
|
2393
2436
|
try {
|
|
@@ -3039,10 +3082,163 @@ var init_schema = __esm(() => {
|
|
|
3039
3082
|
CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id);
|
|
3040
3083
|
|
|
3041
3084
|
INSERT OR IGNORE INTO _migrations (id) VALUES (40);
|
|
3085
|
+
`,
|
|
3086
|
+
`
|
|
3087
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
3088
|
+
id TEXT PRIMARY KEY,
|
|
3089
|
+
name TEXT NOT NULL UNIQUE,
|
|
3090
|
+
hostname TEXT,
|
|
3091
|
+
platform TEXT,
|
|
3092
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3093
|
+
metadata TEXT DEFAULT '{}',
|
|
3094
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3095
|
+
);
|
|
3096
|
+
|
|
3097
|
+
ALTER TABLE projects ADD COLUMN machine_id TEXT;
|
|
3098
|
+
ALTER TABLE projects ADD COLUMN synced_at TEXT;
|
|
3099
|
+
ALTER TABLE tasks ADD COLUMN machine_id TEXT;
|
|
3100
|
+
ALTER TABLE tasks ADD COLUMN synced_at TEXT;
|
|
3101
|
+
ALTER TABLE agents ADD COLUMN machine_id TEXT;
|
|
3102
|
+
ALTER TABLE agents ADD COLUMN synced_at TEXT;
|
|
3103
|
+
ALTER TABLE task_lists ADD COLUMN machine_id TEXT;
|
|
3104
|
+
ALTER TABLE task_lists ADD COLUMN synced_at TEXT;
|
|
3105
|
+
ALTER TABLE plans ADD COLUMN machine_id TEXT;
|
|
3106
|
+
ALTER TABLE plans ADD COLUMN synced_at TEXT;
|
|
3107
|
+
ALTER TABLE task_comments ADD COLUMN machine_id TEXT;
|
|
3108
|
+
ALTER TABLE task_comments ADD COLUMN synced_at TEXT;
|
|
3109
|
+
ALTER TABLE sessions ADD COLUMN machine_id TEXT;
|
|
3110
|
+
ALTER TABLE sessions ADD COLUMN synced_at TEXT;
|
|
3111
|
+
ALTER TABLE task_history ADD COLUMN machine_id TEXT;
|
|
3112
|
+
ALTER TABLE webhooks ADD COLUMN machine_id TEXT;
|
|
3113
|
+
ALTER TABLE webhooks ADD COLUMN synced_at TEXT;
|
|
3114
|
+
ALTER TABLE task_templates ADD COLUMN machine_id TEXT;
|
|
3115
|
+
ALTER TABLE task_templates ADD COLUMN synced_at TEXT;
|
|
3116
|
+
ALTER TABLE orgs ADD COLUMN machine_id TEXT;
|
|
3117
|
+
ALTER TABLE orgs ADD COLUMN synced_at TEXT;
|
|
3118
|
+
ALTER TABLE handoffs ADD COLUMN machine_id TEXT;
|
|
3119
|
+
ALTER TABLE handoffs ADD COLUMN synced_at TEXT;
|
|
3120
|
+
ALTER TABLE task_checklists ADD COLUMN machine_id TEXT;
|
|
3121
|
+
ALTER TABLE project_sources ADD COLUMN machine_id TEXT;
|
|
3122
|
+
ALTER TABLE project_sources ADD COLUMN synced_at TEXT;
|
|
3123
|
+
ALTER TABLE task_files ADD COLUMN machine_id TEXT;
|
|
3124
|
+
ALTER TABLE task_relationships ADD COLUMN machine_id TEXT;
|
|
3125
|
+
ALTER TABLE kg_edges ADD COLUMN machine_id TEXT;
|
|
3126
|
+
ALTER TABLE project_agent_roles ADD COLUMN machine_id TEXT;
|
|
3127
|
+
ALTER TABLE dispatches ADD COLUMN machine_id TEXT;
|
|
3128
|
+
ALTER TABLE dispatches ADD COLUMN synced_at TEXT;
|
|
3129
|
+
|
|
3130
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id);
|
|
3131
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at);
|
|
3132
|
+
CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id);
|
|
3133
|
+
CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id);
|
|
3134
|
+
|
|
3135
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (41);
|
|
3042
3136
|
`
|
|
3043
3137
|
];
|
|
3044
3138
|
});
|
|
3045
3139
|
|
|
3140
|
+
// src/db/machines.ts
|
|
3141
|
+
var exports_machines = {};
|
|
3142
|
+
__export(exports_machines, {
|
|
3143
|
+
resetMachineId: () => resetMachineId,
|
|
3144
|
+
listMachines: () => listMachines,
|
|
3145
|
+
getOrCreateLocalMachine: () => getOrCreateLocalMachine,
|
|
3146
|
+
getMachineId: () => getMachineId,
|
|
3147
|
+
getMachineByName: () => getMachineByName,
|
|
3148
|
+
getMachine: () => getMachine,
|
|
3149
|
+
deleteMachine: () => deleteMachine,
|
|
3150
|
+
backfillMachineId: () => backfillMachineId
|
|
3151
|
+
});
|
|
3152
|
+
import { hostname as osHostname, platform as osPlatform } from "os";
|
|
3153
|
+
function rowToMachine(row) {
|
|
3154
|
+
return {
|
|
3155
|
+
...row,
|
|
3156
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
function getOrCreateLocalMachine(db) {
|
|
3160
|
+
const d = db || getDatabase();
|
|
3161
|
+
const name = process.env["TODOS_MACHINE_NAME"] || osHostname();
|
|
3162
|
+
const host = osHostname();
|
|
3163
|
+
const plat = osPlatform();
|
|
3164
|
+
const existing = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
3165
|
+
if (existing) {
|
|
3166
|
+
d.run("UPDATE machines SET hostname = ?, platform = ?, last_seen_at = ? WHERE id = ?", [host, plat, now(), existing.id]);
|
|
3167
|
+
return rowToMachine({ ...existing, hostname: host, platform: plat, last_seen_at: now() });
|
|
3168
|
+
}
|
|
3169
|
+
const id = uuid();
|
|
3170
|
+
const ts = now();
|
|
3171
|
+
d.run("INSERT INTO machines (id, name, hostname, platform, last_seen_at, metadata, created_at) VALUES (?, ?, ?, ?, ?, '{}', ?)", [id, name, host, plat, ts, ts]);
|
|
3172
|
+
return { id, name, hostname: host, platform: plat, last_seen_at: ts, metadata: {}, created_at: ts };
|
|
3173
|
+
}
|
|
3174
|
+
function getMachineId(db) {
|
|
3175
|
+
if (_machineId)
|
|
3176
|
+
return _machineId;
|
|
3177
|
+
const machine = getOrCreateLocalMachine(db);
|
|
3178
|
+
_machineId = machine.id;
|
|
3179
|
+
return _machineId;
|
|
3180
|
+
}
|
|
3181
|
+
function resetMachineId() {
|
|
3182
|
+
_machineId = null;
|
|
3183
|
+
}
|
|
3184
|
+
function getMachine(id, db) {
|
|
3185
|
+
const d = db || getDatabase();
|
|
3186
|
+
const row = d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
3187
|
+
return row ? rowToMachine(row) : null;
|
|
3188
|
+
}
|
|
3189
|
+
function getMachineByName(name, db) {
|
|
3190
|
+
const d = db || getDatabase();
|
|
3191
|
+
const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
3192
|
+
return row ? rowToMachine(row) : null;
|
|
3193
|
+
}
|
|
3194
|
+
function listMachines(db) {
|
|
3195
|
+
const d = db || getDatabase();
|
|
3196
|
+
const rows = d.query("SELECT * FROM machines ORDER BY last_seen_at DESC").all();
|
|
3197
|
+
return rows.map(rowToMachine);
|
|
3198
|
+
}
|
|
3199
|
+
function deleteMachine(id, db) {
|
|
3200
|
+
const d = db || getDatabase();
|
|
3201
|
+
const result = d.run("DELETE FROM machines WHERE id = ?", [id]);
|
|
3202
|
+
return result.changes > 0;
|
|
3203
|
+
}
|
|
3204
|
+
function backfillMachineId(db, force = false) {
|
|
3205
|
+
if (!force && process.env["TODOS_DB_PATH"] === ":memory:")
|
|
3206
|
+
return;
|
|
3207
|
+
try {
|
|
3208
|
+
const machine = getOrCreateLocalMachine(db);
|
|
3209
|
+
for (const table of TABLES_WITH_MACHINE_ID) {
|
|
3210
|
+
try {
|
|
3211
|
+
db.run(`UPDATE "${table}" SET machine_id = ? WHERE machine_id IS NULL`, [machine.id]);
|
|
3212
|
+
} catch {}
|
|
3213
|
+
}
|
|
3214
|
+
} catch {}
|
|
3215
|
+
}
|
|
3216
|
+
var _machineId = null, TABLES_WITH_MACHINE_ID;
|
|
3217
|
+
var init_machines = __esm(() => {
|
|
3218
|
+
init_database();
|
|
3219
|
+
TABLES_WITH_MACHINE_ID = [
|
|
3220
|
+
"projects",
|
|
3221
|
+
"tasks",
|
|
3222
|
+
"agents",
|
|
3223
|
+
"task_lists",
|
|
3224
|
+
"plans",
|
|
3225
|
+
"task_comments",
|
|
3226
|
+
"sessions",
|
|
3227
|
+
"task_history",
|
|
3228
|
+
"webhooks",
|
|
3229
|
+
"task_templates",
|
|
3230
|
+
"orgs",
|
|
3231
|
+
"handoffs",
|
|
3232
|
+
"task_checklists",
|
|
3233
|
+
"project_sources",
|
|
3234
|
+
"task_files",
|
|
3235
|
+
"task_relationships",
|
|
3236
|
+
"kg_edges",
|
|
3237
|
+
"project_agent_roles",
|
|
3238
|
+
"dispatches"
|
|
3239
|
+
];
|
|
3240
|
+
});
|
|
3241
|
+
|
|
3046
3242
|
// src/db/database.ts
|
|
3047
3243
|
var exports_database = {};
|
|
3048
3244
|
__export(exports_database, {
|
|
@@ -3132,6 +3328,7 @@ function getDatabase(dbPath) {
|
|
|
3132
3328
|
_db.run("PRAGMA foreign_keys = ON");
|
|
3133
3329
|
runMigrations(_db);
|
|
3134
3330
|
backfillTaskTags(_db);
|
|
3331
|
+
backfillMachineId(_db);
|
|
3135
3332
|
return _db;
|
|
3136
3333
|
}
|
|
3137
3334
|
function closeDatabase() {
|
|
@@ -3197,6 +3394,7 @@ function resolvePartialId(db, table, partialId) {
|
|
|
3197
3394
|
var LOCK_EXPIRY_MINUTES = 30, _db = null;
|
|
3198
3395
|
var init_database = __esm(() => {
|
|
3199
3396
|
init_schema();
|
|
3397
|
+
init_machines();
|
|
3200
3398
|
});
|
|
3201
3399
|
|
|
3202
3400
|
// src/types/index.ts
|
|
@@ -22603,6 +22801,332 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
22603
22801
|
init_adapter();
|
|
22604
22802
|
});
|
|
22605
22803
|
|
|
22804
|
+
// src/mcp/tools/cloud.ts
|
|
22805
|
+
async function detectAndLogConflicts(local, cloud, table) {
|
|
22806
|
+
if (!CONFLICT_TABLES.has(table))
|
|
22807
|
+
return 0;
|
|
22808
|
+
try {
|
|
22809
|
+
const { detectConflicts: detectConflicts2, storeConflicts: storeConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
22810
|
+
const localRows = local.all(`SELECT * FROM "${table}"`);
|
|
22811
|
+
const remoteRows = await cloud.all(`SELECT * FROM "${table}"`);
|
|
22812
|
+
if (localRows.length === 0 || remoteRows.length === 0)
|
|
22813
|
+
return 0;
|
|
22814
|
+
const conflicts = detectConflicts2(localRows, remoteRows, table, "id", "updated_at");
|
|
22815
|
+
if (conflicts.length > 0) {
|
|
22816
|
+
storeConflicts2(local, conflicts);
|
|
22817
|
+
}
|
|
22818
|
+
return conflicts.length;
|
|
22819
|
+
} catch {
|
|
22820
|
+
return 0;
|
|
22821
|
+
}
|
|
22822
|
+
}
|
|
22823
|
+
function registerCloudSyncTools(server, { shouldRegisterTool, formatError }) {
|
|
22824
|
+
if (shouldRegisterTool("todos_cloud_status")) {
|
|
22825
|
+
server.tool("todos_cloud_status", "Show cloud configuration, connection health, and local machine info", {}, async () => {
|
|
22826
|
+
try {
|
|
22827
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, PgAdapterAsync: PgAdapterAsync2, listConflicts: listConflicts2, ensureConflictsTable: ensureConflictsTable2, SqliteAdapter: SqliteAdapter2, getDbPath: getDbPath3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
22828
|
+
const config = getCloudConfig2();
|
|
22829
|
+
const db = getDatabase();
|
|
22830
|
+
const machineId = getMachineId(db);
|
|
22831
|
+
const lines = [
|
|
22832
|
+
`Mode: ${config.mode}`,
|
|
22833
|
+
`Service: todos`,
|
|
22834
|
+
`Machine ID: ${machineId}`,
|
|
22835
|
+
`RDS Host: ${config.rds.host || "(not configured)"}`
|
|
22836
|
+
];
|
|
22837
|
+
if (config.rds.host && config.rds.username) {
|
|
22838
|
+
try {
|
|
22839
|
+
const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
|
|
22840
|
+
await pg.get("SELECT 1 as ok");
|
|
22841
|
+
lines.push("PostgreSQL: connected");
|
|
22842
|
+
await pg.close();
|
|
22843
|
+
} catch (err) {
|
|
22844
|
+
lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
|
|
22845
|
+
}
|
|
22846
|
+
}
|
|
22847
|
+
try {
|
|
22848
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
22849
|
+
ensureConflictsTable2(local);
|
|
22850
|
+
const unresolved = listConflicts2(local, { resolved: false });
|
|
22851
|
+
const resolved = listConflicts2(local, { resolved: true });
|
|
22852
|
+
lines.push(`Sync conflicts: ${unresolved.length} unresolved, ${resolved.length} resolved`);
|
|
22853
|
+
local.close();
|
|
22854
|
+
} catch {}
|
|
22855
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
22856
|
+
`) }] };
|
|
22857
|
+
} catch (e) {
|
|
22858
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
22859
|
+
}
|
|
22860
|
+
});
|
|
22861
|
+
}
|
|
22862
|
+
if (shouldRegisterTool("todos_cloud_push")) {
|
|
22863
|
+
server.tool("todos_cloud_push", "Push local data to cloud PostgreSQL. Stamps machine_id on all rows, detects conflicts, and sets synced_at after successful push.", {
|
|
22864
|
+
tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
|
|
22865
|
+
}, async ({ tables: tablesStr }) => {
|
|
22866
|
+
try {
|
|
22867
|
+
const {
|
|
22868
|
+
getCloudConfig: getCloudConfig2,
|
|
22869
|
+
getConnectionString: getConnectionString2,
|
|
22870
|
+
syncPush: syncPush2,
|
|
22871
|
+
listSqliteTables: listSqliteTables2,
|
|
22872
|
+
SqliteAdapter: SqliteAdapter2,
|
|
22873
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
22874
|
+
getDbPath: getDbPath3
|
|
22875
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
22876
|
+
const config = getCloudConfig2();
|
|
22877
|
+
if (config.mode === "local") {
|
|
22878
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
22879
|
+
}
|
|
22880
|
+
const db = getDatabase();
|
|
22881
|
+
const machineId = getMachineId(db);
|
|
22882
|
+
const localPath = getDbPath3("todos");
|
|
22883
|
+
const local = new SqliteAdapter2(localPath);
|
|
22884
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("todos"));
|
|
22885
|
+
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
22886
|
+
for (const table of tableList) {
|
|
22887
|
+
try {
|
|
22888
|
+
local.run(`UPDATE "${table}" SET machine_id = ? WHERE machine_id IS NULL`, machineId);
|
|
22889
|
+
} catch {}
|
|
22890
|
+
}
|
|
22891
|
+
let totalConflicts = 0;
|
|
22892
|
+
for (const table of tableList) {
|
|
22893
|
+
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
22894
|
+
}
|
|
22895
|
+
const results = await syncPush2(local, cloud, { tables: tableList });
|
|
22896
|
+
const syncTime = now();
|
|
22897
|
+
for (const result of results) {
|
|
22898
|
+
if (result.rowsWritten > 0) {
|
|
22899
|
+
try {
|
|
22900
|
+
local.run(`UPDATE "${result.table}" SET synced_at = ? WHERE machine_id = ?`, syncTime, machineId);
|
|
22901
|
+
} catch {}
|
|
22902
|
+
}
|
|
22903
|
+
}
|
|
22904
|
+
local.close();
|
|
22905
|
+
await cloud.close();
|
|
22906
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
22907
|
+
const errors2 = results.flatMap((r) => r.errors);
|
|
22908
|
+
const lines = [`Pushed ${total} rows across ${tableList.length} table(s).`, `Machine: ${machineId}`];
|
|
22909
|
+
if (totalConflicts > 0)
|
|
22910
|
+
lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
|
|
22911
|
+
if (errors2.length > 0)
|
|
22912
|
+
lines.push(`Errors: ${errors2.join("; ")}`);
|
|
22913
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
22914
|
+
`) }] };
|
|
22915
|
+
} catch (e) {
|
|
22916
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
22917
|
+
}
|
|
22918
|
+
});
|
|
22919
|
+
}
|
|
22920
|
+
if (shouldRegisterTool("todos_cloud_pull")) {
|
|
22921
|
+
server.tool("todos_cloud_pull", "Pull cloud PostgreSQL data to local. Detects conflicts, merges by primary key with UPSERT, and logs conflict resolutions.", {
|
|
22922
|
+
tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
|
|
22923
|
+
}, async ({ tables: tablesStr }) => {
|
|
22924
|
+
try {
|
|
22925
|
+
const {
|
|
22926
|
+
getCloudConfig: getCloudConfig2,
|
|
22927
|
+
getConnectionString: getConnectionString2,
|
|
22928
|
+
syncPull: syncPull2,
|
|
22929
|
+
listPgTables: listPgTables2,
|
|
22930
|
+
SqliteAdapter: SqliteAdapter2,
|
|
22931
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
22932
|
+
getDbPath: getDbPath3
|
|
22933
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
22934
|
+
const config = getCloudConfig2();
|
|
22935
|
+
if (config.mode === "local") {
|
|
22936
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
22937
|
+
}
|
|
22938
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
22939
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("todos"));
|
|
22940
|
+
let tableList;
|
|
22941
|
+
if (tablesStr) {
|
|
22942
|
+
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
22943
|
+
} else {
|
|
22944
|
+
try {
|
|
22945
|
+
tableList = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
|
|
22946
|
+
} catch {
|
|
22947
|
+
local.close();
|
|
22948
|
+
await cloud.close();
|
|
22949
|
+
return { content: [{ type: "text", text: "Error: failed to list cloud tables." }], isError: true };
|
|
22950
|
+
}
|
|
22951
|
+
}
|
|
22952
|
+
let totalConflicts = 0;
|
|
22953
|
+
for (const table of tableList) {
|
|
22954
|
+
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
22955
|
+
}
|
|
22956
|
+
const results = await syncPull2(cloud, local, { tables: tableList });
|
|
22957
|
+
const syncTime = now();
|
|
22958
|
+
for (const result of results) {
|
|
22959
|
+
if (result.rowsWritten > 0) {
|
|
22960
|
+
try {
|
|
22961
|
+
local.run(`UPDATE "${result.table}" SET synced_at = ? WHERE synced_at IS NULL OR synced_at < ?`, syncTime, syncTime);
|
|
22962
|
+
} catch {}
|
|
22963
|
+
}
|
|
22964
|
+
}
|
|
22965
|
+
local.close();
|
|
22966
|
+
await cloud.close();
|
|
22967
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
22968
|
+
const errors2 = results.flatMap((r) => r.errors);
|
|
22969
|
+
const lines = [`Pulled ${total} rows across ${tableList.length} table(s).`];
|
|
22970
|
+
if (totalConflicts > 0)
|
|
22971
|
+
lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
|
|
22972
|
+
if (errors2.length > 0)
|
|
22973
|
+
lines.push(`Errors: ${errors2.join("; ")}`);
|
|
22974
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
22975
|
+
`) }] };
|
|
22976
|
+
} catch (e) {
|
|
22977
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
22978
|
+
}
|
|
22979
|
+
});
|
|
22980
|
+
}
|
|
22981
|
+
if (shouldRegisterTool("sync_all")) {
|
|
22982
|
+
server.tool("sync_all", "Bidirectional cloud sync \u2014 pull remote changes then push local changes. Detects and logs conflicts.", {
|
|
22983
|
+
tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
|
|
22984
|
+
}, async ({ tables: tablesStr }) => {
|
|
22985
|
+
try {
|
|
22986
|
+
const {
|
|
22987
|
+
getCloudConfig: getCloudConfig2,
|
|
22988
|
+
getConnectionString: getConnectionString2,
|
|
22989
|
+
syncPush: syncPush2,
|
|
22990
|
+
syncPull: syncPull2,
|
|
22991
|
+
listSqliteTables: listSqliteTables2,
|
|
22992
|
+
listPgTables: listPgTables2,
|
|
22993
|
+
SqliteAdapter: SqliteAdapter2,
|
|
22994
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
22995
|
+
getDbPath: getDbPath3
|
|
22996
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
22997
|
+
const config = getCloudConfig2();
|
|
22998
|
+
if (config.mode === "local") {
|
|
22999
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
23000
|
+
}
|
|
23001
|
+
const db = getDatabase();
|
|
23002
|
+
const machineId = getMachineId(db);
|
|
23003
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
23004
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("todos"));
|
|
23005
|
+
let tableList;
|
|
23006
|
+
if (tablesStr) {
|
|
23007
|
+
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
23008
|
+
} else {
|
|
23009
|
+
const localTables = new Set(listSqliteTables2(local).filter((t) => !t.startsWith("_")));
|
|
23010
|
+
const remoteTables = new Set((await listPgTables2(cloud)).filter((t) => !t.startsWith("_")));
|
|
23011
|
+
tableList = [...new Set([...localTables, ...remoteTables])];
|
|
23012
|
+
}
|
|
23013
|
+
let totalConflicts = 0;
|
|
23014
|
+
for (const table of tableList) {
|
|
23015
|
+
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
23016
|
+
}
|
|
23017
|
+
const pullResults = await syncPull2(cloud, local, { tables: tableList });
|
|
23018
|
+
const pullTotal = pullResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23019
|
+
for (const table of tableList) {
|
|
23020
|
+
try {
|
|
23021
|
+
local.run(`UPDATE "${table}" SET machine_id = ? WHERE machine_id IS NULL`, machineId);
|
|
23022
|
+
} catch {}
|
|
23023
|
+
}
|
|
23024
|
+
const pushResults = await syncPush2(local, cloud, { tables: tableList });
|
|
23025
|
+
const pushTotal = pushResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23026
|
+
const syncTime = now();
|
|
23027
|
+
for (const table of tableList) {
|
|
23028
|
+
try {
|
|
23029
|
+
local.run(`UPDATE "${table}" SET synced_at = ?`, syncTime);
|
|
23030
|
+
} catch {}
|
|
23031
|
+
}
|
|
23032
|
+
local.close();
|
|
23033
|
+
await cloud.close();
|
|
23034
|
+
const allErrors = [
|
|
23035
|
+
...pullResults.flatMap((r) => r.errors.map((e) => `pull: ${e}`)),
|
|
23036
|
+
...pushResults.flatMap((r) => r.errors.map((e) => `push: ${e}`))
|
|
23037
|
+
];
|
|
23038
|
+
const lines = [
|
|
23039
|
+
`Sync complete: pulled ${pullTotal} rows, pushed ${pushTotal} rows across ${tableList.length} table(s).`,
|
|
23040
|
+
`Machine: ${machineId}`
|
|
23041
|
+
];
|
|
23042
|
+
if (totalConflicts > 0)
|
|
23043
|
+
lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
|
|
23044
|
+
if (allErrors.length > 0)
|
|
23045
|
+
lines.push(`Errors: ${allErrors.join("; ")}`);
|
|
23046
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
23047
|
+
`) }] };
|
|
23048
|
+
} catch (e) {
|
|
23049
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
23050
|
+
}
|
|
23051
|
+
});
|
|
23052
|
+
}
|
|
23053
|
+
if (shouldRegisterTool("todos_cloud_conflicts")) {
|
|
23054
|
+
server.tool("todos_cloud_conflicts", "List sync conflicts detected during push/pull operations. Shows unresolved conflicts by default.", {
|
|
23055
|
+
resolved: exports_external.boolean().optional().describe("Filter by resolved status. Default: false (unresolved only)"),
|
|
23056
|
+
table: exports_external.string().optional().describe("Filter by table name"),
|
|
23057
|
+
limit: exports_external.number().optional().describe("Max conflicts to return. Default: 20")
|
|
23058
|
+
}, async ({ resolved, table, limit: maxResults }) => {
|
|
23059
|
+
try {
|
|
23060
|
+
const { listConflicts: listConflicts2, ensureConflictsTable: ensureConflictsTable2, SqliteAdapter: SqliteAdapter2, getDbPath: getDbPath3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23061
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
23062
|
+
ensureConflictsTable2(local);
|
|
23063
|
+
const conflicts = listConflicts2(local, {
|
|
23064
|
+
resolved: resolved ?? false,
|
|
23065
|
+
table
|
|
23066
|
+
});
|
|
23067
|
+
local.close();
|
|
23068
|
+
const shown = conflicts.slice(0, maxResults ?? 20);
|
|
23069
|
+
if (shown.length === 0) {
|
|
23070
|
+
return { content: [{ type: "text", text: resolved ? "No resolved conflicts." : "No unresolved conflicts." }] };
|
|
23071
|
+
}
|
|
23072
|
+
const lines = [`${conflicts.length} conflict(s) found${conflicts.length > shown.length ? ` (showing ${shown.length})` : ""}:
|
|
23073
|
+
`];
|
|
23074
|
+
for (const c of shown) {
|
|
23075
|
+
lines.push(`[${c.id}] ${c.table_name}/${c.row_id}`);
|
|
23076
|
+
lines.push(` Local updated: ${c.local_updated_at}`);
|
|
23077
|
+
lines.push(` Remote updated: ${c.remote_updated_at}`);
|
|
23078
|
+
if (c.resolution)
|
|
23079
|
+
lines.push(` Resolution: ${c.resolution} at ${c.resolved_at}`);
|
|
23080
|
+
lines.push("");
|
|
23081
|
+
}
|
|
23082
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
23083
|
+
`) }] };
|
|
23084
|
+
} catch (e) {
|
|
23085
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
23086
|
+
}
|
|
23087
|
+
});
|
|
23088
|
+
}
|
|
23089
|
+
if (shouldRegisterTool("todos_cloud_feedback")) {
|
|
23090
|
+
server.tool("todos_cloud_feedback", "Send feedback for this service", {
|
|
23091
|
+
message: exports_external.string().describe("Feedback message"),
|
|
23092
|
+
email: exports_external.string().optional().describe("Contact email")
|
|
23093
|
+
}, async ({ message, email }) => {
|
|
23094
|
+
try {
|
|
23095
|
+
const { sendFeedback: sendFeedback2, createDatabase: createDatabase2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23096
|
+
const db = createDatabase2({ service: "cloud" });
|
|
23097
|
+
const result = await sendFeedback2({ service: "todos", message, email }, db);
|
|
23098
|
+
db.close();
|
|
23099
|
+
return {
|
|
23100
|
+
content: [{
|
|
23101
|
+
type: "text",
|
|
23102
|
+
text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
|
|
23103
|
+
}]
|
|
23104
|
+
};
|
|
23105
|
+
} catch (e) {
|
|
23106
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
23107
|
+
}
|
|
23108
|
+
});
|
|
23109
|
+
}
|
|
23110
|
+
}
|
|
23111
|
+
var CONFLICT_TABLES;
|
|
23112
|
+
var init_cloud = __esm(() => {
|
|
23113
|
+
init_zod();
|
|
23114
|
+
init_database();
|
|
23115
|
+
init_machines();
|
|
23116
|
+
CONFLICT_TABLES = new Set([
|
|
23117
|
+
"projects",
|
|
23118
|
+
"tasks",
|
|
23119
|
+
"agents",
|
|
23120
|
+
"task_lists",
|
|
23121
|
+
"plans",
|
|
23122
|
+
"orgs",
|
|
23123
|
+
"task_templates",
|
|
23124
|
+
"webhooks",
|
|
23125
|
+
"project_sources",
|
|
23126
|
+
"task_checklists"
|
|
23127
|
+
]);
|
|
23128
|
+
});
|
|
23129
|
+
|
|
22606
23130
|
// src/mcp/tools/dispatch.ts
|
|
22607
23131
|
function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError }) {
|
|
22608
23132
|
if (shouldRegisterTool("dispatch_tasks")) {
|
|
@@ -25488,7 +26012,7 @@ async function main() {
|
|
|
25488
26012
|
var server, TODOS_PROFILE, MINIMAL_TOOLS, STANDARD_EXCLUDED, agentFocusMap;
|
|
25489
26013
|
var init_mcp = __esm(() => {
|
|
25490
26014
|
init_zod();
|
|
25491
|
-
|
|
26015
|
+
init_cloud();
|
|
25492
26016
|
init_tasks();
|
|
25493
26017
|
init_comments();
|
|
25494
26018
|
init_projects();
|
|
@@ -29040,7 +29564,7 @@ ${result.errors.join(`
|
|
|
29040
29564
|
});
|
|
29041
29565
|
}
|
|
29042
29566
|
registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError });
|
|
29043
|
-
|
|
29567
|
+
registerCloudSyncTools(server, { shouldRegisterTool, formatError });
|
|
29044
29568
|
main().catch((err) => {
|
|
29045
29569
|
console.error("MCP server error:", err);
|
|
29046
29570
|
process.exit(1);
|
|
@@ -35282,5 +35806,329 @@ dbCmd.command("migrate-pg").description("Apply PostgreSQL migrations to the conf
|
|
|
35282
35806
|
process.exit(1);
|
|
35283
35807
|
}
|
|
35284
35808
|
});
|
|
35809
|
+
var cloudCmd = program2.command("cloud").description("Cloud sync commands");
|
|
35810
|
+
cloudCmd.command("status").description("Show cloud config, connection health, machine registry, and sync status").option("--json", "Output as JSON").action(async (opts) => {
|
|
35811
|
+
const globalOpts = program2.opts();
|
|
35812
|
+
const useJson = opts.json || globalOpts.json;
|
|
35813
|
+
try {
|
|
35814
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, PgAdapterAsync: PgAdapterAsync2, SqliteAdapter: SqliteAdapter2, getDbPath: getDbPath3, listSqliteTables: listSqliteTables2, ensureConflictsTable: ensureConflictsTable2, listConflicts: listConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
35815
|
+
const { getMachineId: getMachineId2, listMachines: listMachines2 } = await Promise.resolve().then(() => (init_machines(), exports_machines));
|
|
35816
|
+
const config = getCloudConfig2();
|
|
35817
|
+
const machineId = getMachineId2();
|
|
35818
|
+
const machines = listMachines2();
|
|
35819
|
+
const info = {
|
|
35820
|
+
mode: config.mode,
|
|
35821
|
+
service: "todos",
|
|
35822
|
+
machine_id: machineId,
|
|
35823
|
+
rds_host: config.rds.host || "(not configured)",
|
|
35824
|
+
machines: machines.map((m) => ({ id: m.id, name: m.name, hostname: m.hostname, platform: m.platform, last_seen: m.last_seen_at }))
|
|
35825
|
+
};
|
|
35826
|
+
if (config.rds.host && config.rds.username) {
|
|
35827
|
+
try {
|
|
35828
|
+
const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
|
|
35829
|
+
await pg.get("SELECT 1 as ok");
|
|
35830
|
+
info.postgresql = "connected";
|
|
35831
|
+
await pg.close();
|
|
35832
|
+
} catch (err) {
|
|
35833
|
+
info.postgresql = `failed \u2014 ${err?.message}`;
|
|
35834
|
+
}
|
|
35835
|
+
}
|
|
35836
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
35837
|
+
const tables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
35838
|
+
const syncHealth = [];
|
|
35839
|
+
for (const table of tables) {
|
|
35840
|
+
try {
|
|
35841
|
+
const totalRow = local.get(`SELECT COUNT(*) as c FROM "${table}"`);
|
|
35842
|
+
const unsyncedRow = local.get(`SELECT COUNT(*) as c FROM "${table}" WHERE synced_at IS NULL`);
|
|
35843
|
+
const lastRow = local.get(`SELECT MAX(synced_at) as m FROM "${table}"`);
|
|
35844
|
+
syncHealth.push({
|
|
35845
|
+
table,
|
|
35846
|
+
total: totalRow?.c ?? 0,
|
|
35847
|
+
unsynced: unsyncedRow?.c ?? 0,
|
|
35848
|
+
last_synced: lastRow?.m ?? null
|
|
35849
|
+
});
|
|
35850
|
+
} catch {}
|
|
35851
|
+
}
|
|
35852
|
+
info.sync_health = syncHealth.filter((s) => s.total > 0);
|
|
35853
|
+
try {
|
|
35854
|
+
ensureConflictsTable2(local);
|
|
35855
|
+
const unresolved = listConflicts2(local, { resolved: false });
|
|
35856
|
+
info.conflicts_unresolved = unresolved.length;
|
|
35857
|
+
} catch {}
|
|
35858
|
+
local.close();
|
|
35859
|
+
if (useJson) {
|
|
35860
|
+
console.log(JSON.stringify(info, null, 2));
|
|
35861
|
+
} else {
|
|
35862
|
+
console.log(chalk3.bold("Cloud Status"));
|
|
35863
|
+
console.log(` Mode: ${info.mode}`);
|
|
35864
|
+
console.log(` Machine: ${machineId}`);
|
|
35865
|
+
console.log(` RDS Host: ${info.rds_host}`);
|
|
35866
|
+
if (info.postgresql)
|
|
35867
|
+
console.log(` PostgreSQL: ${info.postgresql}`);
|
|
35868
|
+
if (machines.length > 0) {
|
|
35869
|
+
console.log(chalk3.bold(`
|
|
35870
|
+
Machines`));
|
|
35871
|
+
for (const m of machines) {
|
|
35872
|
+
const current = m.id === machineId ? chalk3.green(" (this)") : "";
|
|
35873
|
+
console.log(` ${m.name}${current} \u2014 ${m.hostname || "?"} / ${m.platform || "?"} \u2014 last seen ${m.last_seen_at}`);
|
|
35874
|
+
}
|
|
35875
|
+
}
|
|
35876
|
+
const healthItems = info.sync_health.filter((s) => s.total > 0);
|
|
35877
|
+
if (healthItems.length > 0) {
|
|
35878
|
+
console.log(chalk3.bold(`
|
|
35879
|
+
Sync Health`));
|
|
35880
|
+
for (const s of healthItems) {
|
|
35881
|
+
const pct = s.total > 0 ? Math.round((s.total - s.unsynced) / s.total * 100) : 100;
|
|
35882
|
+
const color = pct === 100 ? chalk3.green : pct > 50 ? chalk3.yellow : chalk3.red;
|
|
35883
|
+
console.log(` ${s.table}: ${color(`${pct}%`)} synced (${s.unsynced} unsynced / ${s.total} total)${s.last_synced ? ` \u2014 last: ${s.last_synced}` : ""}`);
|
|
35884
|
+
}
|
|
35885
|
+
}
|
|
35886
|
+
if (info.conflicts_unresolved > 0) {
|
|
35887
|
+
console.log(chalk3.bold(`
|
|
35888
|
+
Conflicts`));
|
|
35889
|
+
console.log(` ${chalk3.yellow(`${info.conflicts_unresolved} unresolved`)} \u2014 run \`todos cloud conflicts\` to review`);
|
|
35890
|
+
}
|
|
35891
|
+
}
|
|
35892
|
+
} catch (e) {
|
|
35893
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
35894
|
+
if (useJson) {
|
|
35895
|
+
console.log(JSON.stringify({ error: msg }));
|
|
35896
|
+
} else {
|
|
35897
|
+
console.error(chalk3.red(msg));
|
|
35898
|
+
}
|
|
35899
|
+
}
|
|
35900
|
+
});
|
|
35901
|
+
cloudCmd.command("push").description("Push local data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
35902
|
+
const globalOpts = program2.opts();
|
|
35903
|
+
const useJson = opts.json || globalOpts.json;
|
|
35904
|
+
try {
|
|
35905
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, listSqliteTables: listSqliteTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
35906
|
+
const { getMachineId: getMachineId2 } = await Promise.resolve().then(() => (init_machines(), exports_machines));
|
|
35907
|
+
const { now: now2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
35908
|
+
const config = getCloudConfig2();
|
|
35909
|
+
if (config.mode === "local") {
|
|
35910
|
+
const msg = "Error: cloud mode not configured.";
|
|
35911
|
+
if (useJson) {
|
|
35912
|
+
console.log(JSON.stringify({ error: msg }));
|
|
35913
|
+
} else {
|
|
35914
|
+
console.error(chalk3.red(msg));
|
|
35915
|
+
}
|
|
35916
|
+
process.exit(1);
|
|
35917
|
+
}
|
|
35918
|
+
const machineId = getMachineId2();
|
|
35919
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
35920
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("todos"));
|
|
35921
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
35922
|
+
for (const table of tableList) {
|
|
35923
|
+
try {
|
|
35924
|
+
local.run(`UPDATE "${table}" SET machine_id = ? WHERE machine_id IS NULL`, machineId);
|
|
35925
|
+
} catch {}
|
|
35926
|
+
}
|
|
35927
|
+
const results = await syncPush2(local, cloud, {
|
|
35928
|
+
tables: tableList,
|
|
35929
|
+
onProgress: (p) => {
|
|
35930
|
+
if (!useJson && p.phase === "done") {
|
|
35931
|
+
console.log(` ${p.table}: ${p.rowsWritten} rows pushed`);
|
|
35932
|
+
}
|
|
35933
|
+
}
|
|
35934
|
+
});
|
|
35935
|
+
const syncTime = now2();
|
|
35936
|
+
for (const r of results) {
|
|
35937
|
+
if (r.rowsWritten > 0) {
|
|
35938
|
+
try {
|
|
35939
|
+
local.run(`UPDATE "${r.table}" SET synced_at = ? WHERE machine_id = ?`, syncTime, machineId);
|
|
35940
|
+
} catch {}
|
|
35941
|
+
}
|
|
35942
|
+
}
|
|
35943
|
+
local.close();
|
|
35944
|
+
await cloud.close();
|
|
35945
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
35946
|
+
if (useJson) {
|
|
35947
|
+
console.log(JSON.stringify({ total, machine_id: machineId, tables: results }));
|
|
35948
|
+
} else {
|
|
35949
|
+
console.log(chalk3.green(`Done. ${total} rows pushed (machine: ${machineId}).`));
|
|
35950
|
+
}
|
|
35951
|
+
} catch (e) {
|
|
35952
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
35953
|
+
if (useJson) {
|
|
35954
|
+
console.log(JSON.stringify({ error: msg }));
|
|
35955
|
+
} else {
|
|
35956
|
+
console.error(chalk3.red(msg));
|
|
35957
|
+
}
|
|
35958
|
+
process.exit(1);
|
|
35959
|
+
}
|
|
35960
|
+
});
|
|
35961
|
+
cloudCmd.command("pull").description("Pull cloud data to local \u2014 merges by primary key").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
35962
|
+
const globalOpts = program2.opts();
|
|
35963
|
+
const useJson = opts.json || globalOpts.json;
|
|
35964
|
+
try {
|
|
35965
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPull: syncPull2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
35966
|
+
const { now: now2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
35967
|
+
const config = getCloudConfig2();
|
|
35968
|
+
if (config.mode === "local") {
|
|
35969
|
+
const msg = "Error: cloud mode not configured.";
|
|
35970
|
+
if (useJson) {
|
|
35971
|
+
console.log(JSON.stringify({ error: msg }));
|
|
35972
|
+
} else {
|
|
35973
|
+
console.error(chalk3.red(msg));
|
|
35974
|
+
}
|
|
35975
|
+
process.exit(1);
|
|
35976
|
+
}
|
|
35977
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
35978
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("todos"));
|
|
35979
|
+
let tableList;
|
|
35980
|
+
if (opts.tables) {
|
|
35981
|
+
tableList = opts.tables.split(",").map((t) => t.trim());
|
|
35982
|
+
} else {
|
|
35983
|
+
tableList = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
|
|
35984
|
+
}
|
|
35985
|
+
const results = await syncPull2(cloud, local, {
|
|
35986
|
+
tables: tableList,
|
|
35987
|
+
onProgress: (p) => {
|
|
35988
|
+
if (!useJson && p.phase === "done") {
|
|
35989
|
+
console.log(` ${p.table}: ${p.rowsWritten} rows pulled`);
|
|
35990
|
+
}
|
|
35991
|
+
}
|
|
35992
|
+
});
|
|
35993
|
+
const syncTime = now2();
|
|
35994
|
+
for (const r of results) {
|
|
35995
|
+
if (r.rowsWritten > 0) {
|
|
35996
|
+
try {
|
|
35997
|
+
local.run(`UPDATE "${r.table}" SET synced_at = ? WHERE synced_at IS NULL OR synced_at < ?`, syncTime, syncTime);
|
|
35998
|
+
} catch {}
|
|
35999
|
+
}
|
|
36000
|
+
}
|
|
36001
|
+
local.close();
|
|
36002
|
+
await cloud.close();
|
|
36003
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
36004
|
+
if (useJson) {
|
|
36005
|
+
console.log(JSON.stringify({ total, tables: results }));
|
|
36006
|
+
} else {
|
|
36007
|
+
console.log(chalk3.green(`Done. ${total} rows pulled.`));
|
|
36008
|
+
}
|
|
36009
|
+
} catch (e) {
|
|
36010
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
36011
|
+
if (useJson) {
|
|
36012
|
+
console.log(JSON.stringify({ error: msg }));
|
|
36013
|
+
} else {
|
|
36014
|
+
console.error(chalk3.red(msg));
|
|
36015
|
+
}
|
|
36016
|
+
process.exit(1);
|
|
36017
|
+
}
|
|
36018
|
+
});
|
|
36019
|
+
cloudCmd.command("sync").description("Bidirectional sync \u2014 pull remote changes then push local changes").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
36020
|
+
const globalOpts = program2.opts();
|
|
36021
|
+
const useJson = opts.json || globalOpts.json;
|
|
36022
|
+
try {
|
|
36023
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, syncPull: syncPull2, listSqliteTables: listSqliteTables2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
36024
|
+
const { getMachineId: getMachineId2 } = await Promise.resolve().then(() => (init_machines(), exports_machines));
|
|
36025
|
+
const { now: now2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
36026
|
+
const config = getCloudConfig2();
|
|
36027
|
+
if (config.mode === "local") {
|
|
36028
|
+
const msg = "Error: cloud mode not configured.";
|
|
36029
|
+
if (useJson) {
|
|
36030
|
+
console.log(JSON.stringify({ error: msg }));
|
|
36031
|
+
} else {
|
|
36032
|
+
console.error(chalk3.red(msg));
|
|
36033
|
+
}
|
|
36034
|
+
process.exit(1);
|
|
36035
|
+
}
|
|
36036
|
+
const machineId = getMachineId2();
|
|
36037
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
36038
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("todos"));
|
|
36039
|
+
let tableList;
|
|
36040
|
+
if (opts.tables) {
|
|
36041
|
+
tableList = opts.tables.split(",").map((t) => t.trim());
|
|
36042
|
+
} else {
|
|
36043
|
+
const localTables = new Set(listSqliteTables2(local).filter((t) => !t.startsWith("_")));
|
|
36044
|
+
const remoteTables = new Set((await listPgTables2(cloud)).filter((t) => !t.startsWith("_")));
|
|
36045
|
+
tableList = [...new Set([...localTables, ...remoteTables])];
|
|
36046
|
+
}
|
|
36047
|
+
if (!useJson)
|
|
36048
|
+
console.log(chalk3.bold("Pulling..."));
|
|
36049
|
+
const pullResults = await syncPull2(cloud, local, {
|
|
36050
|
+
tables: tableList,
|
|
36051
|
+
onProgress: (p) => {
|
|
36052
|
+
if (!useJson && p.phase === "done" && p.rowsWritten > 0)
|
|
36053
|
+
console.log(` \u2193 ${p.table}: ${p.rowsWritten} rows`);
|
|
36054
|
+
}
|
|
36055
|
+
});
|
|
36056
|
+
const pullTotal = pullResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
36057
|
+
for (const table of tableList) {
|
|
36058
|
+
try {
|
|
36059
|
+
local.run(`UPDATE "${table}" SET machine_id = ? WHERE machine_id IS NULL`, machineId);
|
|
36060
|
+
} catch {}
|
|
36061
|
+
}
|
|
36062
|
+
if (!useJson)
|
|
36063
|
+
console.log(chalk3.bold("Pushing..."));
|
|
36064
|
+
const pushResults = await syncPush2(local, cloud, {
|
|
36065
|
+
tables: tableList,
|
|
36066
|
+
onProgress: (p) => {
|
|
36067
|
+
if (!useJson && p.phase === "done" && p.rowsWritten > 0)
|
|
36068
|
+
console.log(` \u2191 ${p.table}: ${p.rowsWritten} rows`);
|
|
36069
|
+
}
|
|
36070
|
+
});
|
|
36071
|
+
const pushTotal = pushResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
36072
|
+
const syncTime = now2();
|
|
36073
|
+
for (const table of tableList) {
|
|
36074
|
+
try {
|
|
36075
|
+
local.run(`UPDATE "${table}" SET synced_at = ?`, syncTime);
|
|
36076
|
+
} catch {}
|
|
36077
|
+
}
|
|
36078
|
+
local.close();
|
|
36079
|
+
await cloud.close();
|
|
36080
|
+
if (useJson) {
|
|
36081
|
+
console.log(JSON.stringify({ pulled: pullTotal, pushed: pushTotal, machine_id: machineId, tables: tableList.length }));
|
|
36082
|
+
} else {
|
|
36083
|
+
console.log(chalk3.green(`Done. Pulled ${pullTotal}, pushed ${pushTotal} rows across ${tableList.length} table(s) (machine: ${machineId}).`));
|
|
36084
|
+
}
|
|
36085
|
+
} catch (e) {
|
|
36086
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
36087
|
+
if (useJson) {
|
|
36088
|
+
console.log(JSON.stringify({ error: msg }));
|
|
36089
|
+
} else {
|
|
36090
|
+
console.error(chalk3.red(msg));
|
|
36091
|
+
}
|
|
36092
|
+
process.exit(1);
|
|
36093
|
+
}
|
|
36094
|
+
});
|
|
36095
|
+
cloudCmd.command("conflicts").description("List sync conflicts detected during push/pull").option("--resolved", "Show resolved conflicts instead of unresolved").option("--table <table>", "Filter by table name").option("--limit <n>", "Max conflicts to show", "20").option("--json", "Output as JSON").action(async (opts) => {
|
|
36096
|
+
const globalOpts = program2.opts();
|
|
36097
|
+
const useJson = opts.json || globalOpts.json;
|
|
36098
|
+
try {
|
|
36099
|
+
const { listConflicts: listConflicts2, ensureConflictsTable: ensureConflictsTable2, SqliteAdapter: SqliteAdapter2, getDbPath: getDbPath3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
36100
|
+
const local = new SqliteAdapter2(getDbPath3("todos"));
|
|
36101
|
+
ensureConflictsTable2(local);
|
|
36102
|
+
const conflicts = listConflicts2(local, { resolved: !!opts.resolved, table: opts.table });
|
|
36103
|
+
local.close();
|
|
36104
|
+
const maxResults = parseInt(opts.limit, 10) || 20;
|
|
36105
|
+
const shown = conflicts.slice(0, maxResults);
|
|
36106
|
+
if (useJson) {
|
|
36107
|
+
console.log(JSON.stringify({ total: conflicts.length, conflicts: shown }, null, 2));
|
|
36108
|
+
return;
|
|
36109
|
+
}
|
|
36110
|
+
if (shown.length === 0) {
|
|
36111
|
+
console.log(chalk3.dim(opts.resolved ? "No resolved conflicts." : "No unresolved conflicts."));
|
|
36112
|
+
return;
|
|
36113
|
+
}
|
|
36114
|
+
console.log(`${conflicts.length} conflict(s)${conflicts.length > shown.length ? ` (showing ${shown.length})` : ""}:
|
|
36115
|
+
`);
|
|
36116
|
+
for (const c of shown) {
|
|
36117
|
+
console.log(chalk3.yellow(`[${c.id}]`) + ` ${c.table_name}/${c.row_id}`);
|
|
36118
|
+
console.log(` Local: ${c.local_updated_at}`);
|
|
36119
|
+
console.log(` Remote: ${c.remote_updated_at}`);
|
|
36120
|
+
if (c.resolution)
|
|
36121
|
+
console.log(` Resolution: ${chalk3.green(c.resolution)} at ${c.resolved_at}`);
|
|
36122
|
+
console.log();
|
|
36123
|
+
}
|
|
36124
|
+
} catch (e) {
|
|
36125
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
36126
|
+
if (useJson) {
|
|
36127
|
+
console.log(JSON.stringify({ error: msg }));
|
|
36128
|
+
} else {
|
|
36129
|
+
console.error(chalk3.red(msg));
|
|
36130
|
+
}
|
|
36131
|
+
}
|
|
36132
|
+
});
|
|
35285
36133
|
registerDispatchCommands(program2);
|
|
35286
36134
|
program2.parse();
|