stackmem 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +253 -2
  2. package/package.json +1 -1
  3. package/src/cli.ts +269 -2
package/dist/cli.js CHANGED
@@ -36,6 +36,10 @@ function usage() {
36
36
  cm init <project> <path>
37
37
  cm context [--project <name>] <task>
38
38
  cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
39
+ cm doctor
40
+ cm undo [project]
41
+ cm export [filename]
42
+ cm import [project] <filename>
39
43
  cm help
40
44
 
41
45
  [project] is optional if a .stackmem file exists in the current
@@ -107,6 +111,26 @@ function cmdHelp() {
107
111
  (project, summary, errors_encountered, files_touched,
108
112
  approaches_tried) when stdin is piped rather than a terminal.
109
113
 
114
+ cm doctor
115
+ Check every integration point in the current directory — backend
116
+ connection, MCP registration, .stackmem, git hook, and memory
117
+ count — and report what's working and what's broken.
118
+
119
+ cm undo [project]
120
+ Delete the most recently saved unresolved memory (and any
121
+ memory_links referencing it) for a project.
122
+
123
+ cm export [filename]
124
+ Export all unresolved memories, execution_log entries, and
125
+ memory_links for a project to a JSON file. Defaults to
126
+ <project>-memories.json.
127
+
128
+ cm import [project] <filename>
129
+ Import memories and execution_log entries from a JSON file
130
+ produced by 'cm export'. Inserted under the current device_id
131
+ and project; memory_links are skipped and get rebuilt by
132
+ semantic linking the next time cm save runs.
133
+
110
134
  cm help
111
135
  Show this help message.`);
112
136
  }
@@ -696,7 +720,7 @@ for (const line of isFirstCommit ? [] : lines) {
696
720
  savedItems.push(\`discovery: \${content}\`);
697
721
  }
698
722
 
699
- if (/TODO:|FIXME:|HACK:|XXX:/.test(codeLine)) {
723
+ if (/^\\+\\s*(\\/\\/|#)\\s*(TODO:|FIXME:|HACK:|XXX:)/.test(line)) {
700
724
  const truncated = codeLine.trim().slice(0, 100);
701
725
  const content = \`open issue from commit: \${truncated}\`;
702
726
  cmSave("discovery", content);
@@ -859,7 +883,7 @@ async function cmdInit(project, targetPath) {
859
883
  console.log(`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`);
860
884
  }
861
885
  else {
862
- console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
886
+ console.log("✗ Backend connection failed — run 'cm doctor'");
863
887
  }
864
888
  // Written last and deliberately: if a commit (and hence the post-commit
865
889
  // hook) fires anywhere during init, it must still see whatever project
@@ -1054,6 +1078,204 @@ async function cmdSessionEndEntry(args) {
1054
1078
  approaches: input.approaches,
1055
1079
  });
1056
1080
  }
1081
+ // --- cm doctor ---------------------------------------------------------
1082
+ async function cmdDoctor() {
1083
+ let issues = 0;
1084
+ const project = readProjectFromFile();
1085
+ // 1. Backend connection
1086
+ const backendOk = await verifyBackendConnection(project ?? "");
1087
+ if (backendOk) {
1088
+ console.log("✓ Backend connection verified");
1089
+ }
1090
+ else {
1091
+ console.log("✗ Backend connection failed — check your network");
1092
+ issues++;
1093
+ }
1094
+ // 2. MCP server registration
1095
+ const claudeConfigPath = nodePath.join(nodeOs.homedir(), ".claude.json");
1096
+ let mcpRegistered = false;
1097
+ if (nodeFs.existsSync(claudeConfigPath)) {
1098
+ try {
1099
+ const config = JSON.parse(nodeFs.readFileSync(claudeConfigPath, "utf-8"));
1100
+ mcpRegistered = Boolean(config?.mcpServers?.stackmem);
1101
+ }
1102
+ catch {
1103
+ mcpRegistered = false;
1104
+ }
1105
+ }
1106
+ if (mcpRegistered) {
1107
+ console.log("✓ MCP server registered with Claude Code");
1108
+ }
1109
+ else {
1110
+ console.log("✗ MCP server not registered — run 'cm init <project> .'");
1111
+ issues++;
1112
+ }
1113
+ // 3. Project file
1114
+ if (project) {
1115
+ console.log(`✓ Project file found: ${project}`);
1116
+ }
1117
+ else {
1118
+ console.log("✗ No .stackmem found — run 'cm init <project> .'");
1119
+ issues++;
1120
+ }
1121
+ // 4. Git hook
1122
+ const gitDir = nodePath.join(process.cwd(), ".git");
1123
+ if (!nodeFs.existsSync(gitDir)) {
1124
+ console.log("✗ No git repo found in current directory");
1125
+ issues++;
1126
+ }
1127
+ else {
1128
+ const hookPath = nodePath.join(gitDir, "hooks", "post-commit");
1129
+ let hookExecutable = false;
1130
+ if (nodeFs.existsSync(hookPath)) {
1131
+ try {
1132
+ nodeFs.accessSync(hookPath, nodeFs.constants.X_OK);
1133
+ hookExecutable = true;
1134
+ }
1135
+ catch {
1136
+ hookExecutable = false;
1137
+ }
1138
+ }
1139
+ if (hookExecutable) {
1140
+ console.log("✓ Git hook installed");
1141
+ }
1142
+ else {
1143
+ console.log("✗ Git hook not installed — run 'cm init <project> .'");
1144
+ issues++;
1145
+ }
1146
+ }
1147
+ // 5. Memory count
1148
+ if (project) {
1149
+ const { count, error } = await supabase
1150
+ .from("memories")
1151
+ .select("id", { count: "exact", head: true })
1152
+ .eq("project", project)
1153
+ .eq("resolved", false);
1154
+ if (!error && (count ?? 0) > 0) {
1155
+ console.log(`✓ ${count} memories stored for "${project}"`);
1156
+ }
1157
+ else {
1158
+ console.log(`✗ No memories yet — make a commit to start capturing`);
1159
+ issues++;
1160
+ }
1161
+ }
1162
+ else {
1163
+ console.log(`✗ No memories yet — make a commit to start capturing`);
1164
+ issues++;
1165
+ }
1166
+ console.log("");
1167
+ if (issues === 0) {
1168
+ console.log("All systems operational.");
1169
+ }
1170
+ else {
1171
+ console.log(`${issues} issue(s) found. See above.`);
1172
+ }
1173
+ }
1174
+ // --- cm undo -------------------------------------------------------------
1175
+ async function cmdUndo(project) {
1176
+ const { data, error } = await supabase
1177
+ .from("memories")
1178
+ .select("*")
1179
+ .eq("project", project)
1180
+ .eq("resolved", false)
1181
+ .order("created_at", { ascending: false })
1182
+ .limit(1);
1183
+ if (error)
1184
+ fail(`Failed to load most recent memory: ${error.message}`);
1185
+ const memory = (data ?? [])[0];
1186
+ if (!memory) {
1187
+ console.log("Nothing to undo.");
1188
+ return;
1189
+ }
1190
+ const { error: linksError } = await supabase
1191
+ .from("memory_links")
1192
+ .delete()
1193
+ .or(`source_id.eq.${memory.id},target_id.eq.${memory.id}`);
1194
+ if (linksError)
1195
+ fail(`Failed to delete memory links: ${linksError.message}`);
1196
+ const { error: deleteError } = await supabase.from("memories").delete().eq("id", memory.id);
1197
+ if (deleteError)
1198
+ fail(`Failed to delete memory: ${deleteError.message}`);
1199
+ console.log(`Undone: [${memory.type}] ${memory.content}`);
1200
+ }
1201
+ // --- cm export -------------------------------------------------------------
1202
+ async function cmdExport(project, filename) {
1203
+ const { data: memories, error: memoriesError } = await supabase
1204
+ .from("memories")
1205
+ .select("*")
1206
+ .eq("project", project)
1207
+ .eq("resolved", false);
1208
+ if (memoriesError)
1209
+ fail(`Failed to load memories: ${memoriesError.message}`);
1210
+ const { data: executionLog, error: executionLogError } = await supabase
1211
+ .from("execution_log")
1212
+ .select("*")
1213
+ .eq("project", project);
1214
+ if (executionLogError)
1215
+ fail(`Failed to load execution log: ${executionLogError.message}`);
1216
+ const memoryResults = memories ?? [];
1217
+ const memoryIds = memoryResults.map((m) => m.id);
1218
+ let memoryLinks = [];
1219
+ if (memoryIds.length > 0) {
1220
+ const idList = memoryIds.join(",");
1221
+ const { data: links, error: linksError } = await supabase
1222
+ .from("memory_links")
1223
+ .select("*")
1224
+ .or(`source_id.in.(${idList}),target_id.in.(${idList})`);
1225
+ if (linksError)
1226
+ fail(`Failed to load memory links: ${linksError.message}`);
1227
+ memoryLinks = links ?? [];
1228
+ }
1229
+ const exportData = {
1230
+ project,
1231
+ exported_at: new Date().toISOString(),
1232
+ memories: memoryResults,
1233
+ execution_log: executionLog ?? [],
1234
+ memory_links: memoryLinks,
1235
+ };
1236
+ const outputFilename = filename ?? `${project}-memories.json`;
1237
+ nodeFs.writeFileSync(outputFilename, JSON.stringify(exportData, null, 2), "utf-8");
1238
+ console.log(`Exported ${memoryResults.length} memories to ${outputFilename}`);
1239
+ }
1240
+ // --- cm import -------------------------------------------------------------
1241
+ async function cmdImport(project, filename) {
1242
+ if (!nodeFs.existsSync(filename)) {
1243
+ console.log(`File not found: ${filename}`);
1244
+ return;
1245
+ }
1246
+ let parsed;
1247
+ try {
1248
+ parsed = JSON.parse(nodeFs.readFileSync(filename, "utf-8"));
1249
+ }
1250
+ catch (error) {
1251
+ fail(`Failed to parse ${filename}: ${error instanceof Error ? error.message : String(error)}`);
1252
+ }
1253
+ const memories = Array.isArray(parsed.memories) ? parsed.memories : [];
1254
+ const executionLogEntries = Array.isArray(parsed.execution_log) ? parsed.execution_log : [];
1255
+ // memory_links are intentionally skipped — they get rebuilt by semantic
1256
+ // linking the next time cm save runs against the imported memories.
1257
+ let importedCount = 0;
1258
+ for (const memory of memories) {
1259
+ const { error } = await supabase.from("memories").insert({
1260
+ project,
1261
+ type: memory.type,
1262
+ content: memory.content,
1263
+ device_id: getDeviceId(),
1264
+ });
1265
+ if (!error)
1266
+ importedCount++;
1267
+ }
1268
+ for (const entry of executionLogEntries) {
1269
+ await supabase.from("execution_log").insert({
1270
+ project,
1271
+ problem: entry.problem,
1272
+ solution: entry.solution ?? null,
1273
+ resolved: entry.resolved ?? false,
1274
+ device_id: getDeviceId(),
1275
+ });
1276
+ }
1277
+ console.log(`Imported ${importedCount} memories from ${filename}`);
1278
+ }
1057
1279
  // --- main --------------------------------------------------------------
1058
1280
  async function main() {
1059
1281
  const [, , command, ...args] = process.argv;
@@ -1160,6 +1382,35 @@ async function main() {
1160
1382
  await cmdSessionEndEntry(args);
1161
1383
  break;
1162
1384
  }
1385
+ case "doctor": {
1386
+ await cmdDoctor();
1387
+ break;
1388
+ }
1389
+ case "undo": {
1390
+ const split = splitOptionalProject(args, 0);
1391
+ if (!split)
1392
+ usage();
1393
+ const project = requireProject(split.project);
1394
+ await cmdUndo(project);
1395
+ break;
1396
+ }
1397
+ case "export": {
1398
+ if (args.length > 1)
1399
+ usage();
1400
+ const [filename] = args;
1401
+ const project = requireProject(null);
1402
+ await cmdExport(project, filename);
1403
+ break;
1404
+ }
1405
+ case "import": {
1406
+ const split = splitOptionalProject(args, 1);
1407
+ if (!split)
1408
+ usage();
1409
+ const project = requireProject(split.project);
1410
+ const [filename] = split.rest;
1411
+ await cmdImport(project, filename);
1412
+ break;
1413
+ }
1163
1414
  case "help": {
1164
1415
  cmdHelp();
1165
1416
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackmem",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -54,6 +54,10 @@ function usage(): never {
54
54
  cm init <project> <path>
55
55
  cm context [--project <name>] <task>
56
56
  cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
57
+ cm doctor
58
+ cm undo [project]
59
+ cm export [filename]
60
+ cm import [project] <filename>
57
61
  cm help
58
62
 
59
63
  [project] is optional if a .stackmem file exists in the current
@@ -126,6 +130,26 @@ function cmdHelp() {
126
130
  (project, summary, errors_encountered, files_touched,
127
131
  approaches_tried) when stdin is piped rather than a terminal.
128
132
 
133
+ cm doctor
134
+ Check every integration point in the current directory — backend
135
+ connection, MCP registration, .stackmem, git hook, and memory
136
+ count — and report what's working and what's broken.
137
+
138
+ cm undo [project]
139
+ Delete the most recently saved unresolved memory (and any
140
+ memory_links referencing it) for a project.
141
+
142
+ cm export [filename]
143
+ Export all unresolved memories, execution_log entries, and
144
+ memory_links for a project to a JSON file. Defaults to
145
+ <project>-memories.json.
146
+
147
+ cm import [project] <filename>
148
+ Import memories and execution_log entries from a JSON file
149
+ produced by 'cm export'. Inserted under the current device_id
150
+ and project; memory_links are skipped and get rebuilt by
151
+ semantic linking the next time cm save runs.
152
+
129
153
  cm help
130
154
  Show this help message.`);
131
155
  }
@@ -794,7 +818,7 @@ for (const line of isFirstCommit ? [] : lines) {
794
818
  savedItems.push(\`discovery: \${content}\`);
795
819
  }
796
820
 
797
- if (/TODO:|FIXME:|HACK:|XXX:/.test(codeLine)) {
821
+ if (/^\\+\\s*(\\/\\/|#)\\s*(TODO:|FIXME:|HACK:|XXX:)/.test(line)) {
798
822
  const truncated = codeLine.trim().slice(0, 100);
799
823
  const content = \`open issue from commit: \${truncated}\`;
800
824
  cmSave("discovery", content);
@@ -984,7 +1008,7 @@ async function cmdInit(project: string, targetPath: string) {
984
1008
  `stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`
985
1009
  );
986
1010
  } else {
987
- console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
1011
+ console.log("✗ Backend connection failed — run 'cm doctor'");
988
1012
  }
989
1013
 
990
1014
  // Written last and deliberately: if a commit (and hence the post-commit
@@ -1208,6 +1232,223 @@ async function cmdSessionEndEntry(args: string[]) {
1208
1232
  });
1209
1233
  }
1210
1234
 
1235
+ // --- cm doctor ---------------------------------------------------------
1236
+
1237
+ async function cmdDoctor() {
1238
+ let issues = 0;
1239
+ const project = readProjectFromFile();
1240
+
1241
+ // 1. Backend connection
1242
+ const backendOk = await verifyBackendConnection(project ?? "");
1243
+ if (backendOk) {
1244
+ console.log("✓ Backend connection verified");
1245
+ } else {
1246
+ console.log("✗ Backend connection failed — check your network");
1247
+ issues++;
1248
+ }
1249
+
1250
+ // 2. MCP server registration
1251
+ const claudeConfigPath = nodePath.join(nodeOs.homedir(), ".claude.json");
1252
+ let mcpRegistered = false;
1253
+ if (nodeFs.existsSync(claudeConfigPath)) {
1254
+ try {
1255
+ const config = JSON.parse(nodeFs.readFileSync(claudeConfigPath, "utf-8"));
1256
+ mcpRegistered = Boolean(config?.mcpServers?.stackmem);
1257
+ } catch {
1258
+ mcpRegistered = false;
1259
+ }
1260
+ }
1261
+ if (mcpRegistered) {
1262
+ console.log("✓ MCP server registered with Claude Code");
1263
+ } else {
1264
+ console.log("✗ MCP server not registered — run 'cm init <project> .'");
1265
+ issues++;
1266
+ }
1267
+
1268
+ // 3. Project file
1269
+ if (project) {
1270
+ console.log(`✓ Project file found: ${project}`);
1271
+ } else {
1272
+ console.log("✗ No .stackmem found — run 'cm init <project> .'");
1273
+ issues++;
1274
+ }
1275
+
1276
+ // 4. Git hook
1277
+ const gitDir = nodePath.join(process.cwd(), ".git");
1278
+ if (!nodeFs.existsSync(gitDir)) {
1279
+ console.log("✗ No git repo found in current directory");
1280
+ issues++;
1281
+ } else {
1282
+ const hookPath = nodePath.join(gitDir, "hooks", "post-commit");
1283
+ let hookExecutable = false;
1284
+ if (nodeFs.existsSync(hookPath)) {
1285
+ try {
1286
+ nodeFs.accessSync(hookPath, nodeFs.constants.X_OK);
1287
+ hookExecutable = true;
1288
+ } catch {
1289
+ hookExecutable = false;
1290
+ }
1291
+ }
1292
+ if (hookExecutable) {
1293
+ console.log("✓ Git hook installed");
1294
+ } else {
1295
+ console.log("✗ Git hook not installed — run 'cm init <project> .'");
1296
+ issues++;
1297
+ }
1298
+ }
1299
+
1300
+ // 5. Memory count
1301
+ if (project) {
1302
+ const { count, error } = await supabase
1303
+ .from("memories")
1304
+ .select("id", { count: "exact", head: true })
1305
+ .eq("project", project)
1306
+ .eq("resolved", false);
1307
+
1308
+ if (!error && (count ?? 0) > 0) {
1309
+ console.log(`✓ ${count} memories stored for "${project}"`);
1310
+ } else {
1311
+ console.log(`✗ No memories yet — make a commit to start capturing`);
1312
+ issues++;
1313
+ }
1314
+ } else {
1315
+ console.log(`✗ No memories yet — make a commit to start capturing`);
1316
+ issues++;
1317
+ }
1318
+
1319
+ console.log("");
1320
+ if (issues === 0) {
1321
+ console.log("All systems operational.");
1322
+ } else {
1323
+ console.log(`${issues} issue(s) found. See above.`);
1324
+ }
1325
+ }
1326
+
1327
+ // --- cm undo -------------------------------------------------------------
1328
+
1329
+ async function cmdUndo(project: string) {
1330
+ const { data, error } = await supabase
1331
+ .from("memories")
1332
+ .select("*")
1333
+ .eq("project", project)
1334
+ .eq("resolved", false)
1335
+ .order("created_at", { ascending: false })
1336
+ .limit(1);
1337
+
1338
+ if (error) fail(`Failed to load most recent memory: ${error.message}`);
1339
+
1340
+ const memory = (data ?? [])[0];
1341
+ if (!memory) {
1342
+ console.log("Nothing to undo.");
1343
+ return;
1344
+ }
1345
+
1346
+ const { error: linksError } = await supabase
1347
+ .from("memory_links")
1348
+ .delete()
1349
+ .or(`source_id.eq.${memory.id},target_id.eq.${memory.id}`);
1350
+
1351
+ if (linksError) fail(`Failed to delete memory links: ${linksError.message}`);
1352
+
1353
+ const { error: deleteError } = await supabase.from("memories").delete().eq("id", memory.id);
1354
+
1355
+ if (deleteError) fail(`Failed to delete memory: ${deleteError.message}`);
1356
+
1357
+ console.log(`Undone: [${memory.type}] ${memory.content}`);
1358
+ }
1359
+
1360
+ // --- cm export -------------------------------------------------------------
1361
+
1362
+ async function cmdExport(project: string, filename?: string) {
1363
+ const { data: memories, error: memoriesError } = await supabase
1364
+ .from("memories")
1365
+ .select("*")
1366
+ .eq("project", project)
1367
+ .eq("resolved", false);
1368
+
1369
+ if (memoriesError) fail(`Failed to load memories: ${memoriesError.message}`);
1370
+
1371
+ const { data: executionLog, error: executionLogError } = await supabase
1372
+ .from("execution_log")
1373
+ .select("*")
1374
+ .eq("project", project);
1375
+
1376
+ if (executionLogError) fail(`Failed to load execution log: ${executionLogError.message}`);
1377
+
1378
+ const memoryResults = memories ?? [];
1379
+ const memoryIds = memoryResults.map((m) => m.id);
1380
+
1381
+ let memoryLinks: unknown[] = [];
1382
+ if (memoryIds.length > 0) {
1383
+ const idList = memoryIds.join(",");
1384
+ const { data: links, error: linksError } = await supabase
1385
+ .from("memory_links")
1386
+ .select("*")
1387
+ .or(`source_id.in.(${idList}),target_id.in.(${idList})`);
1388
+
1389
+ if (linksError) fail(`Failed to load memory links: ${linksError.message}`);
1390
+ memoryLinks = links ?? [];
1391
+ }
1392
+
1393
+ const exportData = {
1394
+ project,
1395
+ exported_at: new Date().toISOString(),
1396
+ memories: memoryResults,
1397
+ execution_log: executionLog ?? [],
1398
+ memory_links: memoryLinks,
1399
+ };
1400
+
1401
+ const outputFilename = filename ?? `${project}-memories.json`;
1402
+ nodeFs.writeFileSync(outputFilename, JSON.stringify(exportData, null, 2), "utf-8");
1403
+
1404
+ console.log(`Exported ${memoryResults.length} memories to ${outputFilename}`);
1405
+ }
1406
+
1407
+ // --- cm import -------------------------------------------------------------
1408
+
1409
+ async function cmdImport(project: string, filename: string) {
1410
+ if (!nodeFs.existsSync(filename)) {
1411
+ console.log(`File not found: ${filename}`);
1412
+ return;
1413
+ }
1414
+
1415
+ let parsed: any;
1416
+ try {
1417
+ parsed = JSON.parse(nodeFs.readFileSync(filename, "utf-8"));
1418
+ } catch (error) {
1419
+ fail(`Failed to parse ${filename}: ${error instanceof Error ? error.message : String(error)}`);
1420
+ }
1421
+
1422
+ const memories = Array.isArray(parsed.memories) ? parsed.memories : [];
1423
+ const executionLogEntries = Array.isArray(parsed.execution_log) ? parsed.execution_log : [];
1424
+
1425
+ // memory_links are intentionally skipped — they get rebuilt by semantic
1426
+ // linking the next time cm save runs against the imported memories.
1427
+
1428
+ let importedCount = 0;
1429
+ for (const memory of memories) {
1430
+ const { error } = await supabase.from("memories").insert({
1431
+ project,
1432
+ type: memory.type,
1433
+ content: memory.content,
1434
+ device_id: getDeviceId(),
1435
+ });
1436
+ if (!error) importedCount++;
1437
+ }
1438
+
1439
+ for (const entry of executionLogEntries) {
1440
+ await supabase.from("execution_log").insert({
1441
+ project,
1442
+ problem: entry.problem,
1443
+ solution: entry.solution ?? null,
1444
+ resolved: entry.resolved ?? false,
1445
+ device_id: getDeviceId(),
1446
+ });
1447
+ }
1448
+
1449
+ console.log(`Imported ${importedCount} memories from ${filename}`);
1450
+ }
1451
+
1211
1452
  // --- main --------------------------------------------------------------
1212
1453
 
1213
1454
  async function main() {
@@ -1307,6 +1548,32 @@ async function main() {
1307
1548
  await cmdSessionEndEntry(args);
1308
1549
  break;
1309
1550
  }
1551
+ case "doctor": {
1552
+ await cmdDoctor();
1553
+ break;
1554
+ }
1555
+ case "undo": {
1556
+ const split = splitOptionalProject(args, 0);
1557
+ if (!split) usage();
1558
+ const project = requireProject(split.project);
1559
+ await cmdUndo(project);
1560
+ break;
1561
+ }
1562
+ case "export": {
1563
+ if (args.length > 1) usage();
1564
+ const [filename] = args;
1565
+ const project = requireProject(null);
1566
+ await cmdExport(project, filename);
1567
+ break;
1568
+ }
1569
+ case "import": {
1570
+ const split = splitOptionalProject(args, 1);
1571
+ if (!split) usage();
1572
+ const project = requireProject(split.project);
1573
+ const [filename] = split.rest;
1574
+ await cmdImport(project, filename);
1575
+ break;
1576
+ }
1310
1577
  case "help": {
1311
1578
  cmdHelp();
1312
1579
  break;