im-nobsidian 0.3.2 → 0.4.1

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/dist/index.js CHANGED
@@ -2,11 +2,16 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createRequire } from "module";
5
- import { Command as Command11 } from "commander";
5
+ import { Command as Command12 } from "commander";
6
6
 
7
7
  // src/commands/init.ts
8
8
  import { Command } from "commander";
9
- import { ConfigManager, NotionClient } from "@im-nobsidian/core";
9
+ import {
10
+ ConfigManager,
11
+ NOTION_TOKEN_PREFIX,
12
+ NotionClient,
13
+ notionIdFromInput
14
+ } from "@im-nobsidian/core";
10
15
  import { input, confirm } from "@inquirer/prompts";
11
16
  import ora from "ora";
12
17
  import chalk2 from "chalk";
@@ -17,6 +22,7 @@ var icons = {
17
22
  create: chalk.green("+"),
18
23
  update: chalk.yellow("~"),
19
24
  delete: chalk.red("-"),
25
+ move: chalk.cyan("\u2192"),
20
26
  success: chalk.green("\u2713"),
21
27
  fail: chalk.red("\u2715"),
22
28
  conflict: chalk.magenta("!"),
@@ -42,12 +48,15 @@ function operationLabel(op) {
42
48
  return chalk.yellow("updated");
43
49
  case "delete":
44
50
  return chalk.red("deleted");
51
+ case "move":
52
+ return chalk.cyan("moved");
45
53
  }
46
54
  }
47
- function summary(created, updated, deleted) {
55
+ function summary(created, updated, deleted, moved = 0) {
48
56
  const parts = [];
49
57
  if (created > 0) parts.push(chalk.green(`${created} created`));
50
58
  if (updated > 0) parts.push(chalk.yellow(`${updated} updated`));
59
+ if (moved > 0) parts.push(chalk.cyan(`${moved} moved`));
51
60
  if (deleted > 0) parts.push(chalk.red(`${deleted} deleted`));
52
61
  if (parts.length === 0) parts.push(chalk.dim("no changes"));
53
62
  return parts.join(" ");
@@ -61,18 +70,50 @@ function failedCountText(count) {
61
70
  function duration(ms) {
62
71
  return chalk.dim(`Done in ${(ms / 1e3).toFixed(1)}s`);
63
72
  }
73
+ function localDateTime(iso) {
74
+ const date = new Date(iso);
75
+ if (Number.isNaN(date.getTime())) return iso;
76
+ const pad = (n) => String(n).padStart(2, "0");
77
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
78
+ }
79
+ var FULL_SCAN_REASON = {
80
+ first: "first pull",
81
+ forced: "--force",
82
+ "database-mode": "database mode",
83
+ "every-pull": "every pull",
84
+ due: "scheduled"
85
+ };
86
+ function remoteScanLines(scan) {
87
+ if (scan.kind === "full") {
88
+ return [dimText(scan.reason ? `Full scan (${FULL_SCAN_REASON[scan.reason]})` : "Full scan")];
89
+ }
90
+ const skipped = scan.skippedDatabases ?? 0;
91
+ const skippedText = skipped > 0 ? ` \xB7 ${skipped} unchanged ${skipped === 1 ? "database" : "databases"} skipped` : "";
92
+ const lines = [dimText(`Changes-only scan${skippedText}`)];
93
+ if (scan.deletionsDeferred) lines.push(remoteDeletionHint(scan));
94
+ return lines;
95
+ }
96
+ function remoteDeletionHint(scan) {
97
+ const when = scan.nextFullAt ? `the next full scan (${localDateTime(scan.nextFullAt)})` : "the next pull (full scan due)";
98
+ return `${dimText(`Deletions in Notion apply at ${when} \u2014 or run`)} ${chalk.cyan("nobsi pull --force")}`;
99
+ }
100
+ function lastFullScanText(lastFullScanAt) {
101
+ return lastFullScanAt ? localDateTime(lastFullScanAt) : chalk.yellow("never");
102
+ }
64
103
  function progress(current, total) {
65
104
  return chalk.dim(`[${current}/${total}]`);
66
105
  }
67
106
  function dimText(text) {
68
107
  return chalk.dim(text);
69
108
  }
70
- function failedItem(path, error) {
71
- console.log(` ${icons.fail} ${path}: ${chalk.red(error)}`);
109
+ function failedItem(path, error, direction) {
110
+ const where = `${direction ? `${direction} ` : ""}${path || "/"}`;
111
+ console.log(` ${icons.fail} ${where}: ${chalk.red(error)}`);
72
112
  }
73
113
 
74
114
  // src/commands/init.ts
75
- var initCommand = new Command("init").description("Im-Nobsidian \uCD08\uAE30 \uC124\uC815").option("--token <token>", "Notion Integration Token").option("--root-page-id <id>", "\uB8E8\uD2B8 \uD398\uC774\uC9C0 ID").option("--non-interactive", "\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC").action(async (options) => {
115
+ var ROOT_PAGE_UNREADABLE = "\uB8E8\uD2B8 \uD398\uC774\uC9C0\uC5D0\uC11C Notion \uD398\uC774\uC9C0 ID(32\uC790\uB9AC)\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uD398\uC774\uC9C0 \uC8FC\uC18C\uB098 ID \uB97C \uB123\uC73C\uC138\uC694.";
116
+ var initCommand = new Command("init").description("Im-Nobsidian \uCD08\uAE30 \uC124\uC815").option("--token <token>", "Notion Integration Token").option("--root-page-id <id>", "\uB8E8\uD2B8 \uD398\uC774\uC9C0 ID \uB610\uB294 \uD398\uC774\uC9C0 \uC8FC\uC18C").option("--non-interactive", "\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC").action(async (options) => {
76
117
  const cwd = process.cwd();
77
118
  const configManager = new ConfigManager(cwd);
78
119
  console.log(`
@@ -84,8 +125,14 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
84
125
  process.exitCode = 1;
85
126
  return;
86
127
  }
87
- if (!options.token.startsWith("ntn_")) {
88
- console.error("\uD1A0\uD070\uC740 ntn_\uC73C\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.");
128
+ if (!options.token.startsWith(NOTION_TOKEN_PREFIX)) {
129
+ console.error(`\uD1A0\uD070\uC740 ${NOTION_TOKEN_PREFIX}\uC73C\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.`);
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ const rootPageId = notionIdFromInput(options.rootPageId);
134
+ if (rootPageId === null) {
135
+ console.error(ROOT_PAGE_UNREADABLE);
89
136
  process.exitCode = 1;
90
137
  return;
91
138
  }
@@ -95,10 +142,9 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
95
142
  await client.search({ pageSize: 1, filter: { property: "object", value: "page" } });
96
143
  spinner2.stop();
97
144
  console.log(` ${icons.success} Token validated`);
98
- await configManager.init({ token: options.token, rootPageId: options.rootPageId });
145
+ await configManager.init({ token: options.token, rootPageId });
99
146
  console.log(`
100
147
  ${icons.success} Config saved to ${dimText(configManager.configPath)}`);
101
- console.log(` ${icons.success} State DB initialized`);
102
148
  console.log(
103
149
  `
104
150
  ${header(chalk2.green("Ready!"))} Run ${chalk2.cyan("nobsi sync")} to start syncing.`
@@ -120,7 +166,7 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
120
166
  }
121
167
  const token = await input({
122
168
  message: "Notion Integration Token:",
123
- validate: (v) => v.startsWith("ntn_") || "ntn_\uC73C\uB85C \uC2DC\uC791\uD558\uB294 \uD1A0\uD070\uC744 \uC785\uB825\uD558\uC138\uC694"
169
+ validate: (v) => v.startsWith(NOTION_TOKEN_PREFIX) || `${NOTION_TOKEN_PREFIX}\uC73C\uB85C \uC2DC\uC791\uD558\uB294 \uD1A0\uD070\uC744 \uC785\uB825\uD558\uC138\uC694`
124
170
  });
125
171
  const spinner = ora("Notion \uC5F0\uACB0 \uD655\uC778 \uC911...").start();
126
172
  try {
@@ -144,14 +190,15 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
144
190
  const title = extractPageTitle(page);
145
191
  console.log(` ${dimText(`${i + 1}.`)} ${title}`);
146
192
  }
147
- const rootPageId = await input({
148
- message: "\uB8E8\uD2B8 \uD398\uC774\uC9C0 ID (\uC704\uC5D0\uC11C \uC120\uD0DD\uD558\uAC70\uB098 \uC9C1\uC811 \uC785\uB825):",
149
- default: result.results[0]?.id
193
+ const rootPageInput = await input({
194
+ message: "\uB8E8\uD2B8 \uD398\uC774\uC9C0 ID \uB610\uB294 \uC8FC\uC18C (\uC704\uC5D0\uC11C \uC120\uD0DD\uD558\uAC70\uB098 \uC9C1\uC811 \uC785\uB825):",
195
+ default: result.results[0]?.id,
196
+ validate: (value) => notionIdFromInput(value) !== null || ROOT_PAGE_UNREADABLE
150
197
  });
198
+ const rootPageId = notionIdFromInput(rootPageInput);
151
199
  await configManager.init({ token, rootPageId });
152
200
  console.log(`
153
201
  ${icons.success} Config saved to ${dimText(configManager.configPath)}`);
154
- console.log(` ${icons.success} State DB initialized`);
155
202
  console.log(
156
203
  `
157
204
  ${header(chalk2.green("Ready!"))} Run ${chalk2.cyan("nobsi sync")} to start syncing.`
@@ -183,7 +230,51 @@ import {
183
230
  SyncOrchestrator,
184
231
  NodeVaultFS
185
232
  } from "@im-nobsidian/core";
186
- var pushCommand = new Command2("push").description("\uB85C\uCEEC \uBCC0\uACBD\uC0AC\uD56D\uC744 Notion\uC5D0 \uBC18\uC601").option("--dry-run", "\uC2E4\uC81C \uBC18\uC601 \uC5C6\uC774 \uBCC0\uACBD\uC0AC\uD56D\uB9CC \uD45C\uC2DC").option("-p, --path <paths...>", "\uD2B9\uC815 \uACBD\uB85C\uB9CC push").action(async (options) => {
233
+
234
+ // src/utils/json-output.ts
235
+ import { setLogger } from "@im-nobsidian/core";
236
+ function enableJsonMode() {
237
+ const toStderr = (message, ...args) => console.error(message, ...args);
238
+ setLogger({ warn: toStderr, error: toStderr, info: toStderr, debug: () => {
239
+ } });
240
+ }
241
+ function pushJson(result) {
242
+ return {
243
+ created: result.created,
244
+ updated: result.updated,
245
+ deleted: result.deleted,
246
+ moved: result.moved,
247
+ churn: result.created + result.updated + result.deleted + result.moved,
248
+ failed: result.failed,
249
+ durationMs: result.duration
250
+ };
251
+ }
252
+ function pullJson(result) {
253
+ return {
254
+ created: result.created,
255
+ updated: result.updated,
256
+ deleted: result.deleted,
257
+ restored: result.restored,
258
+ churn: result.created + result.updated + result.deleted + result.restored,
259
+ conflicts: result.conflicts.length,
260
+ failed: result.failed,
261
+ writtenPaths: result.writtenPaths.length,
262
+ images: result.imageCount,
263
+ files: result.fileCount,
264
+ links: result.linkCount,
265
+ durationMs: result.duration,
266
+ remoteScan: result.remoteScan ?? null
267
+ };
268
+ }
269
+ function printJson(value) {
270
+ process.stdout.write(`${JSON.stringify(value)}
271
+ `);
272
+ }
273
+
274
+ // src/commands/push.ts
275
+ var pushCommand = new Command2("push").description("\uB85C\uCEEC \uBCC0\uACBD\uC0AC\uD56D\uC744 Notion\uC5D0 \uBC18\uC601").option("--dry-run", "\uC2E4\uC81C \uBC18\uC601 \uC5C6\uC774 \uBCC0\uACBD\uC0AC\uD56D\uB9CC \uD45C\uC2DC").option("-p, --path <paths...>", "\uD2B9\uC815 \uACBD\uB85C\uB9CC push").option("--json", "\uACB0\uACFC\uB97C JSON \uD55C \uC904\uB85C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9 \u2014 \uB85C\uADF8\uB294 stderr)").action(async (options) => {
276
+ const json = options.json === true;
277
+ if (json) enableJsonMode();
187
278
  const cwd = process.cwd();
188
279
  const configManager = new ConfigManager2(cwd);
189
280
  const config = await configManager.load();
@@ -192,25 +283,32 @@ var pushCommand = new Command2("push").description("\uB85C\uCEEC \uBCC0\uACBD\uC
192
283
  const client = NotionClient2.fromConfig(config);
193
284
  const vaultFs = new NodeVaultFS(cwd, config.paths);
194
285
  const orchestrator = new SyncOrchestrator(config, stateDb, client, vaultFs);
195
- console.log(`
286
+ if (!json) {
287
+ console.log(`
196
288
  ${header(" Pushing to Notion...")}`);
197
- if (options.dryRun) console.log(dimText(" (dry-run mode)"));
198
- console.log("");
289
+ if (options.dryRun) console.log(dimText(" (dry-run mode)"));
290
+ console.log("");
291
+ }
199
292
  const result = await orchestrator.push({
200
293
  dryRun: options.dryRun,
201
294
  paths: options.path,
202
- onProgress: (current, total, item) => {
295
+ onProgress: json ? void 0 : (current, total, item) => {
203
296
  const icon = operationIcon(item.operation);
204
297
  const label = operationLabel(item.operation);
205
298
  const prog = progress(current, total);
206
299
  console.log(` ${icon} ${item.path} ${prog} ${label}`);
207
300
  }
208
301
  });
302
+ if (json) {
303
+ printJson(pushJson(result));
304
+ if (result.failed.length > 0) process.exitCode = 1;
305
+ return;
306
+ }
209
307
  const failedCount = result.failed.length;
210
308
  console.log(`
211
309
  ${completionHeader("Push", failedCount)}`);
212
310
  console.log(
213
- ` ${summary(result.created, result.updated, result.deleted)} ${failedCountText(failedCount)}`
311
+ ` ${summary(result.created, result.updated, result.deleted, result.moved)} ${failedCountText(failedCount)}`
214
312
  );
215
313
  console.log(` ${duration(result.duration)}`);
216
314
  if (failedCount > 0) {
@@ -226,7 +324,7 @@ ${header(" Pushing to Notion...")}`);
226
324
  });
227
325
 
228
326
  // src/commands/pull.ts
229
- import { Command as Command3 } from "commander";
327
+ import { Command as Command3, Option } from "commander";
230
328
  import {
231
329
  ConfigManager as ConfigManager3,
232
330
  StateDB as StateDB2,
@@ -238,7 +336,14 @@ import chalk3 from "chalk";
238
336
  var pullCommand = new Command3("pull").description("Notion \uBCC0\uACBD\uC0AC\uD56D\uC744 \uB85C\uCEEC\uC5D0 \uBC18\uC601").option("--dry-run", "\uC2E4\uC81C \uBC18\uC601 \uC5C6\uC774 \uBCC0\uACBD\uC0AC\uD56D\uB9CC \uD45C\uC2DC").option("-p, --path <paths...>", "\uD2B9\uC815 \uACBD\uB85C\uB9CC pull").option(
239
337
  "--force",
240
338
  "\uC99D\uBD84 \uAC10\uC9C0\uB97C \uAC74\uB108\uB6F0\uACE0 \uC804\uCCB4 \uC2A4\uCE94 (search \uC778\uB371\uC2F1 \uC9C0\uC5F0\uC73C\uB85C \uB204\uB77D\uB41C \uC2E0\uADDC \uD398\uC774\uC9C0\xB7DB \uBCF5\uAD6C)"
241
- ).action(async (options) => {
339
+ ).addOption(
340
+ new Option(
341
+ "--rerender",
342
+ "Notion \uC5D0\uC11C \uBC14\uB00C\uC9C0 \uC54A\uC740 \uBC1B\uC544 \uB454 \uB178\uD2B8\uB3C4 \uC9C0\uAE08 \uBCC0\uD658\uAE30\uB85C \uB2E4\uC2DC \uADF8\uB9BC (\uBCFC\uD2B8\uC5D0\uC11C \uACE0\uCE5C \uB178\uD2B8\uB294 \uADF8\uB300\uB85C)"
343
+ ).conflicts("dryRun")
344
+ ).option("--json", "\uACB0\uACFC\uB97C JSON \uD55C \uC904\uB85C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9 \u2014 \uB85C\uADF8\uB294 stderr)").action(async (options) => {
345
+ const json = options.json === true;
346
+ if (json) enableJsonMode();
242
347
  const cwd = process.cwd();
243
348
  const configManager = new ConfigManager3(cwd);
244
349
  const config = await configManager.load();
@@ -247,29 +352,40 @@ var pullCommand = new Command3("pull").description("Notion \uBCC0\uACBD\uC0AC\uD
247
352
  const client = NotionClient3.fromConfig(config);
248
353
  const vaultFs = new NodeVaultFS2(cwd, config.paths);
249
354
  const orchestrator = new SyncOrchestrator2(config, stateDb, client, vaultFs);
250
- console.log(`
355
+ if (!json) {
356
+ console.log(`
251
357
  ${header(" Pulling from Notion...")}`);
252
- if (options.dryRun) console.log(dimText(" (dry-run mode)"));
253
- console.log("");
358
+ if (options.dryRun) console.log(dimText(" (dry-run mode)"));
359
+ console.log("");
360
+ }
254
361
  const result = await orchestrator.pull({
255
362
  dryRun: options.dryRun,
256
363
  paths: options.path,
257
364
  force: options.force,
258
- onProgress: (current, total, item) => {
365
+ rerender: options.rerender,
366
+ onProgress: json ? void 0 : (current, total, item) => {
367
+ if (item.unchanged) return;
259
368
  const icon = operationIcon(item.operation);
260
369
  const label = operationLabel(item.operation);
261
370
  const prog = progress(current, total);
262
371
  console.log(` ${icon} ${item.path} ${prog} ${label}`);
263
372
  }
264
373
  });
374
+ if (json) {
375
+ printJson(pullJson(result));
376
+ if (result.failed.length > 0) process.exitCode = 1;
377
+ return;
378
+ }
265
379
  if (result.imageCount > 0) {
266
380
  console.log(
267
381
  `
268
- ${icons.success} ${result.imageCount} images \u2192 ${dimText("attachments/")}`
382
+ ${icons.success} ${result.imageCount} images \u2192 ${dimText(`${config.paths.attachments}/`)}`
269
383
  );
270
384
  }
271
385
  if (result.fileCount > 0) {
272
- console.log(` ${icons.success} ${result.fileCount} files \u2192 ${dimText("attachments/")}`);
386
+ console.log(
387
+ ` ${icons.success} ${result.fileCount} files \u2192 ${dimText(`${config.paths.attachments}/`)}`
388
+ );
273
389
  }
274
390
  if (result.linkCount > 0) {
275
391
  console.log(` ${icons.success} ${result.linkCount} links resolved`);
@@ -284,6 +400,9 @@ ${header(" Pulling from Notion...")}`);
284
400
  console.log(` ${icons.success} ${chalk3.yellow(`${result.restored} restored`)}`);
285
401
  }
286
402
  console.log(` ${duration(result.duration)}`);
403
+ if (result.remoteScan) {
404
+ for (const line of remoteScanLines(result.remoteScan)) console.log(` ${line}`);
405
+ }
287
406
  if (result.conflicts.length > 0) {
288
407
  console.log(
289
408
  `
@@ -312,7 +431,11 @@ import {
312
431
  NodeVaultFS as NodeVaultFS3
313
432
  } from "@im-nobsidian/core";
314
433
  import chalk4 from "chalk";
315
- var syncCommand = new Command4("sync").description("\uC591\uBC29\uD5A5 \uB3D9\uAE30\uD654 (Pull \u2192 Push)").option("--dry-run", "\uC2E4\uC81C \uBC18\uC601 \uC5C6\uC774 \uBCC0\uACBD\uC0AC\uD56D\uB9CC \uD45C\uC2DC").action(async (options) => {
434
+ var syncCommand = new Command4("sync").description("\uC591\uBC29\uD5A5 \uB3D9\uAE30\uD654 (Pull \u2192 Push)").option("--dry-run", "\uC2E4\uC81C \uBC18\uC601 \uC5C6\uC774 \uBCC0\uACBD\uC0AC\uD56D\uB9CC \uD45C\uC2DC").option("--json", "\uACB0\uACFC\uB97C JSON \uD55C \uC904\uB85C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9 \u2014 \uB85C\uADF8\uB294 stderr)").action(async (options) => {
435
+ const json = options.json === true;
436
+ if (json) enableJsonMode();
437
+ const log = json ? () => {
438
+ } : (line) => console.log(line);
316
439
  const cwd = process.cwd();
317
440
  const configManager = new ConfigManager4(cwd);
318
441
  const config = await configManager.load();
@@ -321,37 +444,44 @@ var syncCommand = new Command4("sync").description("\uC591\uBC29\uD5A5 \uB3D9\uA
321
444
  const client = NotionClient4.fromConfig(config);
322
445
  const vaultFs = new NodeVaultFS3(cwd, config.paths);
323
446
  const orchestrator = new SyncOrchestrator3(config, stateDb, client, vaultFs);
324
- console.log(`
447
+ log(`
325
448
  ${header(" Bidirectional Sync")}`);
326
- console.log(` ${separator()}`);
327
- if (options.dryRun) console.log(dimText(" (dry-run mode)"));
328
- console.log(`
449
+ log(` ${separator()}`);
450
+ if (options.dryRun) log(dimText(" (dry-run mode)"));
451
+ log(`
329
452
  ${header(chalk4.blue("\u25BC Pull"))} ${dimText("(Notion \u2192 Obsidian)")}`);
330
- const pullResult = await orchestrator.pull({
331
- dryRun: options.dryRun,
332
- onProgress: (_current, _total, item) => {
333
- const icon = operationIcon(item.operation);
334
- const label = operationLabel(item.operation);
335
- console.log(` ${icon} ${item.path} ${label}`);
336
- }
337
- });
338
- if (pullResult.imageCount > 0) {
339
- console.log(` ${icons.success} ${pullResult.imageCount} images downloaded`);
340
- }
341
- if (pullResult.linkCount > 0) {
342
- console.log(` ${icons.success} ${pullResult.linkCount} links resolved`);
343
- }
344
- console.log(`
453
+ let pushShown = false;
454
+ const showPush = () => {
455
+ if (pushShown) return;
456
+ pushShown = true;
457
+ log(`
345
458
  ${header(chalk4.magenta("\u25B2 Push"))} ${dimText("(Obsidian \u2192 Notion)")}`);
346
- const pushResult = await orchestrator.push({
459
+ };
460
+ const result = await orchestrator.sync({
347
461
  dryRun: options.dryRun,
348
462
  onProgress: (_current, _total, item) => {
463
+ if (item.direction === "push") showPush();
464
+ if (item.unchanged) return;
349
465
  const icon = operationIcon(item.operation);
350
466
  const label = operationLabel(item.operation);
351
- console.log(` ${icon} ${item.path} ${label}`);
467
+ log(` ${icon} ${item.path} ${label}`);
352
468
  }
353
469
  });
354
- const totalDuration = pullResult.duration + pushResult.duration;
470
+ showPush();
471
+ const { pull: pullResult, push: pushResult } = result;
472
+ if (json) {
473
+ const pull = pullJson(pullResult);
474
+ const push = pushJson(pushResult);
475
+ printJson({
476
+ pull,
477
+ push,
478
+ churn: pull.churn + push.churn,
479
+ conflicts: pull.conflicts,
480
+ durationMs: result.duration
481
+ });
482
+ if (pullResult.failed.length + pushResult.failed.length > 0) process.exitCode = 1;
483
+ return;
484
+ }
355
485
  const pullFailed = pullResult.failed.length;
356
486
  const pushFailed = pushResult.failed.length;
357
487
  console.log(`
@@ -360,20 +490,29 @@ ${header(" Bidirectional Sync")}`);
360
490
  ` ${chalk4.blue("Pull:")} ${summary(pullResult.created, pullResult.updated, pullResult.deleted)}` + // 사라졌던 파일이 되살아난 경우에만 덧붙인다 — 평상시 요약을 어지럽히지 않는다.
361
491
  (pullResult.restored > 0 ? ` ${chalk4.yellow(`${pullResult.restored} restored`)}` : "") + (pullFailed > 0 ? ` ${chalk4.red(`${pullFailed} failed`)}` : "")
362
492
  );
493
+ if (pullResult.imageCount > 0) {
494
+ console.log(` ${icons.success} ${pullResult.imageCount} images downloaded`);
495
+ }
496
+ if (pullResult.linkCount > 0) {
497
+ console.log(` ${icons.success} ${pullResult.linkCount} links resolved`);
498
+ }
363
499
  console.log(
364
- ` ${chalk4.magenta("Push:")} ${summary(pushResult.created, pushResult.updated, pushResult.deleted)}` + (pushFailed > 0 ? ` ${chalk4.red(`${pushFailed} failed`)}` : "")
500
+ ` ${chalk4.magenta("Push:")} ${summary(pushResult.created, pushResult.updated, pushResult.deleted, pushResult.moved)}` + (pushFailed > 0 ? ` ${chalk4.red(`${pushFailed} failed`)}` : "")
365
501
  );
366
- console.log(` ${duration(totalDuration)}`);
367
- if (pullResult.conflicts.length > 0) {
502
+ console.log(` ${duration(result.duration)}`);
503
+ if (pullResult.remoteScan) {
504
+ for (const line of remoteScanLines(pullResult.remoteScan)) console.log(` ${line}`);
505
+ }
506
+ if (result.conflicts.length > 0) {
368
507
  console.log(
369
508
  `
370
- ${icons.conflict} ${chalk4.magenta(`${pullResult.conflicts.length} conflicts`)} \u2014 run ${chalk4.cyan("nobsi resolve")}`
509
+ ${icons.conflict} ${chalk4.magenta(`${result.conflicts.length} conflicts`)} \u2014 run ${chalk4.cyan("nobsi resolve")}`
371
510
  );
372
511
  }
373
512
  if (pullFailed + pushFailed > 0) {
374
513
  console.log("");
375
- for (const f of pullResult.failed) failedItem(`\u25BC ${f.path}`, f.error);
376
- for (const f of pushResult.failed) failedItem(`\u25B2 ${f.path}`, f.error);
514
+ for (const f of pullResult.failed) failedItem(f.path, f.error, "\u25BC");
515
+ for (const f of pushResult.failed) failedItem(f.path, f.error, "\u25B2");
377
516
  process.exitCode = 1;
378
517
  }
379
518
  } finally {
@@ -388,7 +527,8 @@ import {
388
527
  StateDB as StateDB4,
389
528
  NotionClient as NotionClient5,
390
529
  SyncOrchestrator as SyncOrchestrator4,
391
- NodeVaultFS as NodeVaultFS4
530
+ NodeVaultFS as NodeVaultFS4,
531
+ isRemoteDeletion
392
532
  } from "@im-nobsidian/core";
393
533
  import chalk5 from "chalk";
394
534
  var statusCommand = new Command5("status").description("\uB3D9\uAE30\uD654 \uC0C1\uD0DC \uD655\uC778 (--full: \uC6D0\uACA9 \uBCC0\uACBD \uD3EC\uD568)").option("--full", "Notion \uC6D0\uACA9 \uBCC0\uACBD\uAE4C\uC9C0 \uC591\uBC29\uD5A5 \uD655\uC778 (API \uD638\uCD9C, \uB290\uB9BC)").action(async (opts) => {
@@ -409,21 +549,22 @@ ${header(" Sync Status")}`);
409
549
  console.log(` ${dimText("Root page:")} ${rootId.slice(0, 8)}...`);
410
550
  console.log(` ${dimText("Direction:")} ${direction}`);
411
551
  if (status.lastSyncAt) {
412
- const date = new Date(status.lastSyncAt);
413
- const iso = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") + " " + String(date.getHours()).padStart(2, "0") + ":" + String(date.getMinutes()).padStart(2, "0") + ":" + String(date.getSeconds()).padStart(2, "0");
414
- console.log(` ${dimText("Last sync:")} ${iso}`);
552
+ console.log(` ${dimText("Last sync:")} ${localDateTime(status.lastSyncAt)}`);
415
553
  } else {
416
554
  console.log(` ${dimText("Last sync:")} ${chalk5.yellow("never")}`);
417
555
  }
556
+ console.log(` ${dimText("Full scan:")} ${lastFullScanText(status.lastFullScanAt)}`);
418
557
  const allRecords = stateDb.getAll();
419
558
  const total = allRecords.length;
420
559
  const conflictCount = status.conflictRecords.length;
421
560
  const createdChanges = status.localChanges.filter((c) => c.type === "created");
422
561
  const modifiedChanges = status.localChanges.filter((c) => c.type === "modified");
423
562
  const deletedChanges = status.localChanges.filter((c) => c.type === "deleted");
563
+ const movedChanges = status.localChanges.filter((c) => c.type === "moved");
424
564
  const dirtyPaths = /* @__PURE__ */ new Set([
425
565
  ...modifiedChanges.map((c) => c.path),
426
566
  ...deletedChanges.map((c) => c.path),
567
+ ...movedChanges.flatMap((c) => [c.path, c.movedFrom ?? c.path]),
427
568
  ...status.conflictRecords.map((r) => r.obsidianPath)
428
569
  ]);
429
570
  const synced = allRecords.filter(
@@ -432,12 +573,17 @@ ${header(" Sync Status")}`);
432
573
  const remoteCreated = status.remoteChanges.filter((c) => c.type === "created");
433
574
  const remoteModified = status.remoteChanges.filter((c) => c.type === "modified");
434
575
  const remoteDeleted = status.remoteChanges.filter((c) => c.type === "deleted");
576
+ const remoteMoved = status.remoteChanges.filter((c) => c.type === "moved");
577
+ const { folderMoves } = status;
435
578
  console.log(`
436
579
  ${header(`Tracked files: ${total}`)}`);
437
580
  console.log(` ${icons.synced} ${chalk5.green("synced")} ${synced}`);
438
581
  if (modifiedChanges.length > 0) {
439
582
  console.log(` ${icons.modified} ${chalk5.yellow("modified")} ${modifiedChanges.length}`);
440
583
  }
584
+ if (movedChanges.length > 0) {
585
+ console.log(` ${icons.move} ${chalk5.cyan("moved")} ${movedChanges.length}`);
586
+ }
441
587
  if (deletedChanges.length > 0) {
442
588
  console.log(` ${chalk5.red("-")} ${chalk5.red("deleted")} ${deletedChanges.length}`);
443
589
  }
@@ -461,6 +607,29 @@ ${header(" Sync Status")}`);
461
607
  console.log(dimText(` ... and ${modifiedChanges.length - 10} more`));
462
608
  }
463
609
  }
610
+ if (folderMoves.length + movedChanges.length > 0) {
611
+ console.log(`
612
+ ${header("Moved:")}`);
613
+ for (const move of folderMoves) {
614
+ console.log(` ${icons.move} ${move.from}/ \u2192 ${move.to}/ ${dimText("(folder)")}`);
615
+ }
616
+ for (const c of movedChanges.slice(0, 10)) {
617
+ console.log(` ${icons.move} ${c.movedFrom ?? "?"} \u2192 ${c.path}`);
618
+ }
619
+ if (movedChanges.length > 10) {
620
+ console.log(dimText(` ... and ${movedChanges.length - 10} more`));
621
+ }
622
+ }
623
+ if (deletedChanges.length > 0) {
624
+ console.log(`
625
+ ${header("Deleted files:")}`);
626
+ for (const c of deletedChanges.slice(0, 10)) {
627
+ console.log(` ${chalk5.red("-")} ${c.path}`);
628
+ }
629
+ if (deletedChanges.length > 10) {
630
+ console.log(dimText(` ... and ${deletedChanges.length - 10} more`));
631
+ }
632
+ }
464
633
  if (createdChanges.length > 0) {
465
634
  console.log(`
466
635
  ${header("New files:")}`);
@@ -482,12 +651,18 @@ ${header(" Sync Status")}`);
482
651
  ` ${chalk5.yellow("~")} ${chalk5.yellow("modified")} ${remoteModified.length}`
483
652
  );
484
653
  }
654
+ if (remoteMoved.length > 0) {
655
+ console.log(` ${icons.move} ${chalk5.cyan("moved")} ${remoteMoved.length}`);
656
+ }
485
657
  if (remoteDeleted.length > 0) {
486
658
  console.log(` ${chalk5.red("-")} ${chalk5.red("deleted")} ${remoteDeleted.length}`);
487
659
  }
488
660
  for (const c of status.remoteChanges.slice(0, 10)) {
489
- const icon = c.type === "created" ? chalk5.cyan("+") : c.type === "modified" ? chalk5.yellow("~") : chalk5.red("-");
490
- console.log(` ${icon} ${c.pageId.slice(0, 8)}... ${dimText(`(${c.type})`)}`);
661
+ const icon = c.type === "created" ? chalk5.cyan("+") : c.type === "modified" ? chalk5.yellow("~") : c.type === "moved" ? icons.move : chalk5.red("-");
662
+ const name = c.path ?? (c.title ? `${c.title} ${dimText("(new page)")}` : void 0);
663
+ console.log(
664
+ ` ${icon} ${name ?? `${c.pageId.slice(0, 8)}...`} ${dimText(`(${c.type})`)}`
665
+ );
491
666
  }
492
667
  if (status.remoteChanges.length > 10) {
493
668
  console.log(dimText(` ... and ${status.remoteChanges.length - 10} more`));
@@ -496,13 +671,15 @@ ${header(" Sync Status")}`);
496
671
  if (conflictCount > 0) {
497
672
  console.log(`
498
673
  ${header(chalk5.magenta("Conflicts:"))}`);
674
+ const remoteDeleted2 = new Set(
675
+ status.conflicts.filter(isRemoteDeletion).map((c) => c.syncRecord.obsidianPath)
676
+ );
499
677
  for (const r of status.conflictRecords) {
500
- console.log(
501
- ` ${icons.conflict} ${r.obsidianPath.padEnd(30)} ${dimText("(both sides changed)")}`
502
- );
678
+ const reason = remoteDeleted2.has(r.obsidianPath) ? "(deleted in Notion, local edits not pushed)" : "(both sides changed)";
679
+ console.log(` ${icons.conflict} ${r.obsidianPath.padEnd(30)} ${dimText(reason)}`);
503
680
  }
504
681
  }
505
- const hasChanges = status.localChanges.length > 0 || status.remoteChanges.length > 0 || conflictCount > 0;
682
+ const hasChanges = status.localChanges.length > 0 || folderMoves.length > 0 || status.remoteChanges.length > 0 || conflictCount > 0;
506
683
  if (hasChanges) {
507
684
  console.log("");
508
685
  if (conflictCount > 0) {
@@ -513,6 +690,11 @@ ${header(" Sync Status")}`);
513
690
  console.log(
514
691
  ` ${dimText("Run")} ${chalk5.cyan("nobsi sync")} ${dimText("to push/pull changes")}`
515
692
  );
693
+ if (modifiedChanges.length + deletedChanges.length > 0) {
694
+ console.log(
695
+ ` ${dimText("Run")} ${chalk5.cyan("nobsi discard <path>")} ${dimText("to discard local edits")}`
696
+ );
697
+ }
516
698
  } else {
517
699
  console.log(`
518
700
  ${chalk5.green("Everything up to date")} \u2713`);
@@ -522,6 +704,9 @@ ${header(" Sync Status")}`);
522
704
  `
523
705
  ${dimText("Run")} ${chalk5.cyan("nobsi status --full")} ${dimText("to check Notion remote changes")}`
524
706
  );
707
+ } else if (status.remoteScan?.deletionsDeferred) {
708
+ console.log(`
709
+ ${remoteDeletionHint(status.remoteScan)}`);
525
710
  }
526
711
  } finally {
527
712
  stateDb.close();
@@ -536,9 +721,31 @@ import {
536
721
  NotionClient as NotionClient6,
537
722
  SyncOrchestrator as SyncOrchestrator5,
538
723
  NodeVaultFS as NodeVaultFS5,
724
+ formatUnifiedDiff,
725
+ lineDiff,
539
726
  matchesPathScope
540
727
  } from "@im-nobsidian/core";
541
- import { createTwoFilesPatch } from "diff";
728
+
729
+ // src/utils/diff-output.ts
730
+ import { DIFF_SIGN } from "@im-nobsidian/core";
731
+ var BOLD = "\x1B[1m";
732
+ var RED = "\x1B[31m";
733
+ var GREEN = "\x1B[32m";
734
+ var CYAN = "\x1B[36m";
735
+ var RESET = "\x1B[0m";
736
+ var LINE_COLOR = {
737
+ [DIFF_SIGN.added]: GREEN,
738
+ [DIFF_SIGN.removed]: RED,
739
+ "@": CYAN
740
+ };
741
+ function printUnifiedDiff(lines, color) {
742
+ lines.forEach((line, index) => {
743
+ const code = index < 2 ? BOLD : LINE_COLOR[line.charAt(0)];
744
+ console.log(color && code ? `${code}${line}${RESET}` : line);
745
+ });
746
+ }
747
+
748
+ // src/commands/diff.ts
542
749
  var diffCommand = new Command6("diff").description(
543
750
  "\uB85C\uCEEC \uBCC0\uACBD\uC0AC\uD56D \uD45C\uC2DC (\uAE30\uBCF8: \uB9C8\uC9C0\uB9C9 \uB3D9\uAE30\uD654 \uC2DC\uC810\uACFC \uBE44\uAD50, --remote: Notion \uD604\uC7AC\uBCF8\uACFC \uBE44\uAD50)"
544
751
  ).argument("[path]", "\uD2B9\uC815 \uD30C\uC77C \uACBD\uB85C (\uC0DD\uB7B5 \uC2DC \uBCC0\uACBD\uB41C \uBAA8\uB4E0 \uD30C\uC77C)").option("--color", "\uCEEC\uB7EC \uCD9C\uB825", true).option("--remote", "Notion \uD604\uC7AC \uBCF8\uBB38\uACFC \uBE44\uAD50 (API \uD638\uCD9C, \uB290\uB9BC)").action(async (targetPath, options) => {
@@ -556,10 +763,15 @@ var diffCommand = new Command6("diff").description(
556
763
  console.log("\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C");
557
764
  return;
558
765
  }
766
+ let failed = 0;
559
767
  for (const change of changes) {
560
- const record = stateDb.getByPath(change.path);
561
768
  if (options.remote && change.type !== "deleted") {
562
- const remote = await orchestrator.renderRemoteSnapshot(change.path);
769
+ let remotePath = change.path;
770
+ let remote = await orchestrator.renderRemoteSnapshot(remotePath);
771
+ if (remote === null && change.movedFrom) {
772
+ remotePath = change.movedFrom;
773
+ remote = await orchestrator.renderRemoteSnapshot(remotePath);
774
+ }
563
775
  if (remote === null) {
564
776
  console.log(`--- (\uC6D0\uACA9 \uC5C6\uC74C) ${change.path}`);
565
777
  console.log(`+++ b/${change.path}`);
@@ -568,76 +780,49 @@ var diffCommand = new Command6("diff").description(
568
780
  continue;
569
781
  }
570
782
  const currentContent = await vaultFs.readFile(change.path);
571
- const patch = createTwoFilesPatch(
572
- `notion/${change.path}`,
573
- `b/${change.path}`,
574
- remote,
575
- currentContent
576
- );
577
- printPatch(patch, options.color, `notion/${change.path}`, `b/${change.path}`);
783
+ const hunks = lineDiff(remote, currentContent);
784
+ const lines = formatUnifiedDiff(hunks, `notion/${remotePath}`, `b/${change.path}`);
785
+ if (hunks.length === 0) lines.push("(Notion \uACFC \uB0B4\uC6A9\uC774 \uAC19\uC2B5\uB2C8\uB2E4)");
786
+ printUnifiedDiff(lines, options.color);
578
787
  console.log("");
579
788
  continue;
580
789
  }
581
- if (change.type === "created") {
582
- const content = await vaultFs.readFile(change.path);
583
- const patch = createTwoFilesPatch("/dev/null", `b/${change.path}`, "", content);
584
- printPatch(patch, options.color, "/dev/null", `b/${change.path}`);
585
- } else if (change.type === "deleted") {
586
- if (record?.baseSnapshot) {
587
- const oldContent = record.baseSnapshot.toString("utf-8");
588
- const patch = createTwoFilesPatch(`a/${change.path}`, "/dev/null", oldContent, "");
589
- printPatch(patch, options.color, `a/${change.path}`, "/dev/null");
590
- } else {
591
- console.log(`--- a/${change.path}`);
592
- console.log(`+++ /dev/null`);
593
- console.log(`(\uC774\uC804 \uC2A4\uB0C5\uC0F7 \uC5C6\uC74C)`);
594
- }
595
- } else if (change.type === "modified" && record?.baseSnapshot) {
596
- const currentContent = await vaultFs.readFile(change.path);
597
- const baseContent = record.baseSnapshot.toString("utf-8");
598
- const patch = createTwoFilesPatch(
599
- `a/${change.path}`,
600
- `b/${change.path}`,
601
- baseContent,
602
- currentContent
603
- );
604
- printPatch(patch, options.color, `a/${change.path}`, `b/${change.path}`);
790
+ let diff;
791
+ try {
792
+ diff = await orchestrator.localChangeDiff(change);
793
+ } catch (error) {
794
+ failed++;
795
+ console.log(`--- a/${change.path}`);
796
+ console.log(`(${error instanceof Error ? error.message : String(error)})`);
797
+ console.log("");
798
+ continue;
605
799
  }
800
+ printChangeDiff(diff, options.color);
606
801
  console.log("");
607
802
  }
803
+ if (failed > 0) process.exitCode = 1;
608
804
  } finally {
609
805
  stateDb.close();
610
806
  }
611
807
  });
612
- function printPatch(patch, color, oldPath, newPath) {
613
- for (const line of patch.split("\n")) {
614
- let output = line;
615
- if (line.startsWith("===")) {
616
- output = "===================================================================";
617
- } else if (line.startsWith("--- ")) {
618
- output = `--- ${oldPath}`;
619
- } else if (line.startsWith("+++ ")) {
620
- output = `+++ ${newPath}`;
621
- }
622
- if (!color) {
623
- console.log(output);
624
- continue;
625
- }
626
- if (output.startsWith("+++") || output.startsWith("---")) {
627
- console.log(`\x1B[1m${output}\x1B[0m`);
628
- } else if (output.startsWith("+")) {
629
- console.log(`\x1B[32m${output}\x1B[0m`);
630
- } else if (output.startsWith("-")) {
631
- console.log(`\x1B[31m${output}\x1B[0m`);
632
- } else if (output.startsWith("@@")) {
633
- console.log(`\x1B[36m${output}\x1B[0m`);
634
- } else {
635
- console.log(output);
808
+ function printChangeDiff(diff, color) {
809
+ const oldPath = diff.before === null ? "/dev/null" : `a/${diff.movedFrom ?? diff.path}`;
810
+ const newPath = diff.after === null ? "/dev/null" : `b/${diff.path}`;
811
+ if (diff.movedFrom) {
812
+ console.log(`rename from ${diff.movedFrom}`);
813
+ console.log(`rename to ${diff.path}`);
814
+ if (diff.before === diff.after) {
815
+ console.log("(\uB0B4\uC6A9\uC740 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4 \u2014 \uC790\uB9AC\uB9CC \uC62E\uACBC\uC2B5\uB2C8\uB2E4)");
816
+ return;
636
817
  }
637
818
  }
819
+ printUnifiedDiff(
820
+ formatUnifiedDiff(lineDiff(diff.before ?? "", diff.after ?? ""), oldPath, newPath),
821
+ color
822
+ );
638
823
  }
639
824
 
640
- // src/commands/fetch.ts
825
+ // src/commands/discard.ts
641
826
  import { Command as Command7 } from "commander";
642
827
  import {
643
828
  ConfigManager as ConfigManager7,
@@ -646,8 +831,7 @@ import {
646
831
  SyncOrchestrator as SyncOrchestrator6,
647
832
  NodeVaultFS as NodeVaultFS6
648
833
  } from "@im-nobsidian/core";
649
- import chalk6 from "chalk";
650
- var fetchCommand = new Command7("fetch").description("Notion \uB9AC\uBAA8\uD2B8 \uC0C1\uD0DC \uC2A4\uCE94 (\uC0AD\uC81C \uAC10\uC9C0 \uD3EC\uD568)").action(async () => {
834
+ var discardCommand = new Command7("discard").description("\uB85C\uCEEC \uBCC0\uACBD\uC744 \uC9C0\uB09C \uB3D9\uAE30\uD654 \uB54C\uC758 \uAE00\uB85C \uB418\uB3CC\uB9AC\uAE30 (Notion \uC740 \uADF8\uB300\uB85C)").argument("<paths...>", "\uB418\uB3CC\uB9B4 \uB178\uD2B8 \uACBD\uB85C \u2014 status \uAC00 \uBCF4\uC778 \uBCFC\uD2B8 \uAE30\uC900 \uACBD\uB85C").action(async (paths) => {
651
835
  const cwd = process.cwd();
652
836
  const configManager = new ConfigManager7(cwd);
653
837
  const config = await configManager.load();
@@ -656,6 +840,41 @@ var fetchCommand = new Command7("fetch").description("Notion \uB9AC\uBAA8\uD2B8
656
840
  const client = NotionClient7.fromConfig(config);
657
841
  const vaultFs = new NodeVaultFS6(cwd, config.paths);
658
842
  const orchestrator = new SyncOrchestrator6(config, stateDb, client, vaultFs);
843
+ let failed = 0;
844
+ for (const path of paths) {
845
+ try {
846
+ await orchestrator.discardLocalChange(path);
847
+ console.log(` ${icons.success} ${path}`);
848
+ } catch (error) {
849
+ failed++;
850
+ failedItem(path, error instanceof Error ? error.message : String(error));
851
+ }
852
+ }
853
+ if (failed > 0) process.exitCode = 1;
854
+ } finally {
855
+ stateDb.close();
856
+ }
857
+ });
858
+
859
+ // src/commands/fetch.ts
860
+ import { Command as Command8 } from "commander";
861
+ import {
862
+ ConfigManager as ConfigManager8,
863
+ StateDB as StateDB7,
864
+ NotionClient as NotionClient8,
865
+ SyncOrchestrator as SyncOrchestrator7,
866
+ NodeVaultFS as NodeVaultFS7
867
+ } from "@im-nobsidian/core";
868
+ import chalk6 from "chalk";
869
+ var fetchCommand = new Command8("fetch").description("Notion \uB9AC\uBAA8\uD2B8 \uC0C1\uD0DC \uC2A4\uCE94 (\uC0AD\uC81C \uAC10\uC9C0 \uD3EC\uD568)").action(async () => {
870
+ const cwd = process.cwd();
871
+ const configManager = new ConfigManager8(cwd);
872
+ const config = await configManager.load();
873
+ const stateDb = StateDB7.open(configManager.dbPath);
874
+ try {
875
+ const client = NotionClient8.fromConfig(config);
876
+ const vaultFs = new NodeVaultFS7(cwd, config.paths);
877
+ const orchestrator = new SyncOrchestrator7(config, stateDb, client, vaultFs);
659
878
  console.log(dimText(" Scanning remote pages..."));
660
879
  const result = await orchestrator.fetch();
661
880
  console.log(`
@@ -687,34 +906,68 @@ ${header(" Remote Scan")}`);
687
906
  });
688
907
 
689
908
  // src/commands/resolve.ts
690
- import { Command as Command8 } from "commander";
909
+ import { Command as Command9 } from "commander";
691
910
  import { select, confirm as confirm2 } from "@inquirer/prompts";
692
911
  import {
693
- ConfigManager as ConfigManager8,
694
- StateDB as StateDB7,
695
- NotionClient as NotionClient8,
696
- SyncOrchestrator as SyncOrchestrator7,
697
- NodeVaultFS as NodeVaultFS7
912
+ ConfigManager as ConfigManager9,
913
+ StateDB as StateDB8,
914
+ NotionClient as NotionClient9,
915
+ SyncOrchestrator as SyncOrchestrator8,
916
+ NodeVaultFS as NodeVaultFS8,
917
+ applicableChoices,
918
+ isRemoteDeletion as isRemoteDeletion2
698
919
  } from "@im-nobsidian/core";
920
+
921
+ // src/utils/state-db-failure.ts
922
+ import { StateDbUnavailableError } from "@im-nobsidian/core";
923
+ function describeFailure(error) {
924
+ const message = error instanceof Error ? error.message : String(error);
925
+ if (!(error instanceof StateDbUnavailableError)) return message;
926
+ return `${message} \u2014 ${error.guidance("\uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694")}`;
927
+ }
928
+ function reportStateDbFailure(error, proc) {
929
+ if (!(error instanceof StateDbUnavailableError)) throw error;
930
+ console.error(`\uC624\uB958: ${describeFailure(error)}`);
931
+ proc.exitCode = 1;
932
+ }
933
+
934
+ // src/commands/resolve.ts
699
935
  var STRATEGIES = ["local-first", "remote-first", "duplicate"];
936
+ var CHOICE_NAMES = {
937
+ local: "\uB85C\uCEEC \uC720\uC9C0 (Obsidian \uBC84\uC804 \uC720\uC9C0)",
938
+ remote: "\uC6D0\uACA9 \uC720\uC9C0 (Notion \uBC84\uC804\uC73C\uB85C \uB36E\uC5B4\uC4F0\uAE30)",
939
+ merge: "\uC790\uB3D9 \uBCD1\uD569 (3-way merge)",
940
+ duplicate: "\uBCF5\uC81C (\uC6D0\uBCF8 \uC720\uC9C0 + \uC62C\uB9AC\uC9C0 \uC54A\uB294 .conflict \uC0AC\uBCF8 \uC0DD\uC131)"
941
+ };
942
+ var REMOTE_DELETION_CHOICE_NAMES = {
943
+ local: "\uB85C\uCEEC \uC720\uC9C0 (Notion \uC5D0 \uC0C8 \uD398\uC774\uC9C0\uB85C \uB2E4\uC2DC \uB9CC\uB4E4\uAE30)",
944
+ remote: "\uC0AD\uC81C \uB530\uB974\uAE30 (Obsidian \uD30C\uC77C\uB3C4 \uC9C0\uC6B0\uAE30)"
945
+ };
946
+ var PULL_ONLY_REMOTE_DELETION_CHOICE_NAMES = {
947
+ local: "\uB85C\uCEEC \uC720\uC9C0 (Obsidian \uD30C\uC77C\uB9CC \uB450\uAE30 \u2014 \uBC1B\uAE30 \uC804\uC6A9\uC774\uB77C Notion \uC5D0 \uB9CC\uB4E4\uC9C0 \uC54A\uC74C)"
948
+ };
949
+ function choiceName(conflict, choice, uploads) {
950
+ const deleted = isRemoteDeletion2(conflict);
951
+ return (deleted && !uploads ? PULL_ONLY_REMOTE_DELETION_CHOICE_NAMES[choice] : void 0) ?? (deleted ? REMOTE_DELETION_CHOICE_NAMES[choice] : void 0) ?? CHOICE_NAMES[choice] ?? choice;
952
+ }
700
953
  function isInteractive() {
701
954
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
702
955
  }
703
- var resolveCommand = new Command8("resolve").description("\uCDA9\uB3CC\uC744 \uD574\uACB0\uD569\uB2C8\uB2E4").option("-s, --strategy <strategy>", `\uC790\uB3D9 \uD574\uACB0 \uC804\uB7B5 (${STRATEGIES.join(" | ")})`).option("-y, --yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC5C6\uC774 \uC9C4\uD589 (\uBE44\uB300\uD654\uD615/CI)").action(async (options) => {
956
+ var resolveCommand = new Command9("resolve").description("\uCDA9\uB3CC\uC744 \uD574\uACB0\uD569\uB2C8\uB2E4").option("-s, --strategy <strategy>", `\uC790\uB3D9 \uD574\uACB0 \uC804\uB7B5 (${STRATEGIES.join(" | ")})`).option("-y, --yes", "\uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC5C6\uC774 \uC9C4\uD589 (\uBE44\uB300\uD654\uD615/CI)").action(async (options) => {
704
957
  if (options.strategy && !STRATEGIES.includes(options.strategy)) {
705
958
  console.error(`\uC54C \uC218 \uC5C6\uB294 \uC804\uB7B5 '${options.strategy}' \u2014 \uC0AC\uC6A9 \uAC00\uB2A5: ${STRATEGIES.join(", ")}`);
706
959
  process.exitCode = 1;
707
960
  return;
708
961
  }
709
962
  const cwd = process.cwd();
710
- const configManager = new ConfigManager8(cwd);
963
+ const configManager = new ConfigManager9(cwd);
711
964
  let stateDb;
712
965
  try {
713
966
  const config = await configManager.load();
714
- stateDb = StateDB7.open(configManager.dbPath);
715
- const client = NotionClient8.fromConfig(config);
716
- const vaultFs = new NodeVaultFS7(cwd, config.paths);
717
- const orchestrator = new SyncOrchestrator7(config, stateDb, client, vaultFs);
967
+ stateDb = StateDB8.open(configManager.dbPath);
968
+ const client = NotionClient9.fromConfig(config);
969
+ const vaultFs = new NodeVaultFS8(cwd, config.paths);
970
+ const orchestrator = new SyncOrchestrator8(config, stateDb, client, vaultFs);
718
971
  const conflicts = await orchestrator.listConflicts();
719
972
  if (conflicts.length === 0) {
720
973
  console.log("\uCDA9\uB3CC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
@@ -733,8 +986,20 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
733
986
  console.log(`
734
987
  \uCDA9\uB3CC ${pending.length}\uAC74 \uBC1C\uACAC
735
988
  `);
989
+ const uploads = config.sync.direction !== "pull";
990
+ if (!uploads) {
991
+ console.log(
992
+ " \uBC1B\uAE30 \uC804\uC6A9(direction: pull) \u2014 \uD574\uACB0 \uACB0\uACFC\uB294 Obsidian \uC5D0\uB9CC \uB450\uACE0 Notion \uC5D0\uB294 \uC62C\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4\n"
993
+ );
994
+ }
736
995
  if (options.strategy) {
737
996
  const strategy = options.strategy;
997
+ const deletions = pending.filter(isRemoteDeletion2).length;
998
+ if (deletions > 0) {
999
+ console.log(
1000
+ ` Notion \uC5D0\uC11C \uC0AD\uC81C\uB41C \uB178\uD2B8 ${deletions}\uAC74 \u2014 ${strategy === "remote-first" ? "Obsidian \uD30C\uC77C\uB3C4 \uC9C0\uC6C1\uB2C8\uB2E4" : uploads ? "Notion \uC5D0 \uC0C8 \uD398\uC774\uC9C0\uB85C \uB2E4\uC2DC \uB9CC\uB4ED\uB2C8\uB2E4" : "Obsidian \uD30C\uC77C\uC744 \uADF8\uB300\uB85C \uB461\uB2C8\uB2E4"}`
1001
+ );
1002
+ }
738
1003
  if (!options.yes) {
739
1004
  if (!isInteractive()) {
740
1005
  console.error(
@@ -750,17 +1015,20 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
750
1015
  }
751
1016
  const results = await orchestrator.resolveAllConflicts(pending, strategy);
752
1017
  let failed2 = 0;
753
- for (const result of results) {
1018
+ for (const [index, result] of results.entries()) {
754
1019
  if (!result.success) failed2++;
755
1020
  const icon = result.success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
756
- console.log(` ${icon} ${result.path} \u2192 ${result.choice}`);
1021
+ const reason = result.error ? ` \u2014 ${result.error}` : "";
1022
+ console.log(
1023
+ ` ${icon} ${result.path} \u2192 ${choiceName(pending[index], result.choice, uploads)}${reason}`
1024
+ );
757
1025
  }
758
1026
  report(results.length - failed2, failed2);
759
1027
  return;
760
1028
  }
761
1029
  if (!isInteractive()) {
762
1030
  console.error(
763
- "\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C\uB294 \uB300\uD654\uD615 \uD574\uACB0\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 --strategy <local-first|remote-first|duplicate> --yes \uB97C \uC9C0\uC815\uD558\uC138\uC694."
1031
+ `\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C\uB294 \uB300\uD654\uD615 \uD574\uACB0\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 --strategy <${STRATEGIES.join("|")}> --yes \uB97C \uC9C0\uC815\uD558\uC138\uC694.`
764
1032
  );
765
1033
  process.exitCode = 1;
766
1034
  return;
@@ -768,12 +1036,12 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
768
1036
  let resolved = 0;
769
1037
  let failed = 0;
770
1038
  for (const conflict of pending) {
771
- if (await resolveInteractive(conflict, orchestrator)) resolved++;
1039
+ if (await resolveInteractive(conflict, orchestrator, uploads)) resolved++;
772
1040
  else failed++;
773
1041
  }
774
1042
  report(resolved, failed);
775
1043
  } catch (error) {
776
- console.error("\uC624\uB958:", error instanceof Error ? error.message : String(error));
1044
+ console.error("\uC624\uB958:", describeFailure(error));
777
1045
  process.exitCode = 1;
778
1046
  } finally {
779
1047
  stateDb?.close();
@@ -789,37 +1057,29 @@ function report(resolved, failed) {
789
1057
  \x1B[33m\uCDA9\uB3CC ${resolved}\uAC74 \uD574\uACB0 \xB7 ${failed}\uAC74 \uBBF8\uD574\uACB0\x1B[0m`);
790
1058
  process.exitCode = 1;
791
1059
  }
792
- async function resolveInteractive(conflict, orchestrator) {
1060
+ async function resolveInteractive(conflict, orchestrator, uploads) {
793
1061
  const path = conflict.syncRecord.obsidianPath;
794
1062
  console.log(`
795
1063
  ${"\u2500".repeat(60)}`);
796
1064
  console.log(`\uD30C\uC77C: \x1B[1m${path}\x1B[0m`);
797
1065
  console.log(`${"\u2500".repeat(60)}`);
798
- const diff = orchestrator.generateConflictDiff(conflict);
799
- const diffLines = diff.split("\n");
800
- for (const line of diffLines) {
801
- if (line.startsWith("- ")) {
802
- console.log(`\x1B[31m${line}\x1B[0m`);
803
- } else if (line.startsWith("+ ")) {
804
- console.log(`\x1B[32m${line}\x1B[0m`);
805
- } else if (line.startsWith("---") || line.startsWith("+++")) {
806
- console.log(`\x1B[1m${line}\x1B[0m`);
807
- } else {
808
- console.log(line);
809
- }
1066
+ if (isRemoteDeletion2(conflict)) {
1067
+ console.log(
1068
+ "\x1B[33mNotion \uC5D0\uC11C \uC0AD\uC81C\uB41C \uB178\uD2B8\uC785\uB2C8\uB2E4 \u2014 Notion \uC5D0 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uB85C\uCEEC \uD3B8\uC9D1\uC774 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4.\x1B[0m"
1069
+ );
1070
+ } else {
1071
+ printUnifiedDiff(orchestrator.generateConflictDiff(conflict).split("\n"), true);
810
1072
  }
811
1073
  const choice = await select({
812
1074
  message: "\uC5B4\uB5BB\uAC8C \uD574\uACB0\uD560\uAE4C\uC694?",
813
- choices: [
814
- { name: "\uB85C\uCEEC \uC720\uC9C0 (Obsidian \uBC84\uC804 \uC720\uC9C0)", value: "local" },
815
- { name: "\uC6D0\uACA9 \uC720\uC9C0 (Notion \uBC84\uC804\uC73C\uB85C \uB36E\uC5B4\uC4F0\uAE30)", value: "remote" },
816
- { name: "\uC790\uB3D9 \uBCD1\uD569 (3-way merge)", value: "merge" },
817
- { name: "\uBCF5\uC81C (\uC6D0\uBCF8 \uC720\uC9C0 + .conflict \uD30C\uC77C \uC0DD\uC131)", value: "duplicate" }
818
- ]
1075
+ choices: applicableChoices(conflict).map((value) => ({
1076
+ name: choiceName(conflict, value, uploads),
1077
+ value
1078
+ }))
819
1079
  });
820
1080
  const result = await orchestrator.resolveConflict(conflict, choice);
821
1081
  if (result.success) {
822
- console.log(`\x1B[32m\u2713 ${path} \u2192 ${choice}\uB85C \uD574\uACB0\x1B[0m`);
1082
+ console.log(`\x1B[32m\u2713 ${path} \u2192 ${choiceName(conflict, choice, uploads)}\x1B[0m`);
823
1083
  return true;
824
1084
  }
825
1085
  if (result.mergeHadConflicts) {
@@ -831,18 +1091,20 @@ ${"\u2500".repeat(60)}`);
831
1091
  }
832
1092
 
833
1093
  // src/commands/watch.ts
834
- import { Command as Command9 } from "commander";
1094
+ import { Command as Command10 } from "commander";
835
1095
  import {
836
- ConfigManager as ConfigManager9,
837
- StateDB as StateDB8,
838
- NotionClient as NotionClient9,
839
- SyncOrchestrator as SyncOrchestrator8,
840
- NodeVaultFS as NodeVaultFS8,
841
- WatchSyncService
1096
+ ConfigManager as ConfigManager10,
1097
+ StateDB as StateDB9,
1098
+ NotionClient as NotionClient10,
1099
+ SyncOrchestrator as SyncOrchestrator9,
1100
+ NodeVaultFS as NodeVaultFS9,
1101
+ WatchSyncService,
1102
+ DEFAULT_WATCH_DEBOUNCE_MS
842
1103
  } from "@im-nobsidian/core";
843
1104
  function formatResult(result) {
844
1105
  const pull = `Pull: +${result.pull.created} ~${result.pull.updated} -${result.pull.deleted}`;
845
- const push = `Push: +${result.push.created} ~${result.push.updated} -${result.push.deleted}`;
1106
+ const moved = result.push.moved > 0 ? ` \u2192${result.push.moved}` : "";
1107
+ const push = `Push: +${result.push.created} ~${result.push.updated} -${result.push.deleted}${moved}`;
846
1108
  const parts = [pull, push];
847
1109
  if (result.conflicts.length > 0) {
848
1110
  parts.push(`\uCDA9\uB3CC ${result.conflicts.length}\uAC74`);
@@ -852,35 +1114,59 @@ function formatResult(result) {
852
1114
  function timestamp() {
853
1115
  return (/* @__PURE__ */ new Date()).toLocaleTimeString("ko-KR", { hour12: false });
854
1116
  }
855
- var watchCommand = new Command9("watch").description("\uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0 + \uC790\uB3D9 \uB3D9\uAE30\uD654").option("-d, --debounce <ms>", "\uB514\uBC14\uC6B4\uC2A4 \uB300\uAE30 \uC2DC\uAC04(ms)", "2000").option("-i, --interval <seconds>", "\uD480 \uB3D9\uAE30\uD654 \uC8FC\uAE30(\uCD08, 0\uC774\uBA74 \uBE44\uD65C\uC131)", "0").action(async (options) => {
1117
+ function syncLabel(scope) {
1118
+ return scope === "full" ? "\uC8FC\uAE30\uC801 \uB3D9\uAE30\uD654" : "\uB3D9\uAE30\uD654";
1119
+ }
1120
+ var watchCommand = new Command10("watch").description("\uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0 + \uC790\uB3D9 \uB3D9\uAE30\uD654").option("-d, --debounce <ms>", "\uB514\uBC14\uC6B4\uC2A4 \uB300\uAE30 \uC2DC\uAC04(ms)", String(DEFAULT_WATCH_DEBOUNCE_MS)).option("-i, --interval <seconds>", "\uD480 \uB3D9\uAE30\uD654 \uC8FC\uAE30(\uCD08, 0\uC774\uBA74 \uBE44\uD65C\uC131)", "0").action(async (options) => {
1121
+ const debounceMs = Number(options.debounce);
1122
+ const intervalSec = Number(options.interval);
1123
+ if (!Number.isInteger(debounceMs) || debounceMs < 0) {
1124
+ console.error(`--debounce \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218(ms)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${options.debounce}`);
1125
+ process.exitCode = 1;
1126
+ return;
1127
+ }
1128
+ if (!Number.isInteger(intervalSec) || intervalSec < 0) {
1129
+ console.error(`--interval \uC740 0 \uC774\uC0C1\uC758 \uC815\uC218(\uCD08)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${options.interval}`);
1130
+ process.exitCode = 1;
1131
+ return;
1132
+ }
856
1133
  const cwd = process.cwd();
857
- const configManager = new ConfigManager9(cwd);
1134
+ const configManager = new ConfigManager10(cwd);
858
1135
  const config = await configManager.load();
859
- const stateDb = StateDB8.open(configManager.dbPath);
860
- const client = NotionClient9.fromConfig(config);
861
- const vaultFs = new NodeVaultFS8(cwd, config.paths);
862
- const orchestrator = new SyncOrchestrator8(config, stateDb, client, vaultFs);
863
- const debounceMs = parseInt(options.debounce, 10);
864
- const intervalSec = parseInt(options.interval, 10);
1136
+ const stateDb = StateDB9.open(configManager.dbPath);
1137
+ const client = NotionClient10.fromConfig(config);
1138
+ const vaultFs = new NodeVaultFS9(cwd, config.paths);
1139
+ const orchestrator = new SyncOrchestrator9(config, stateDb, client, vaultFs);
865
1140
  const service = new WatchSyncService(cwd, orchestrator, {
866
1141
  debounceMs,
867
1142
  onFileChange: (event, path) => {
868
1143
  console.log(`[${timestamp()}] ${event}: ${path}`);
869
1144
  },
870
- onSyncStart: () => {
871
- console.log(`[${timestamp()}] \uB3D9\uAE30\uD654 \uC2DC\uC791...`);
1145
+ onSyncStart: (scope) => {
1146
+ console.log(`[${timestamp()}] ${syncLabel(scope)} \uC2DC\uC791...`);
1147
+ },
1148
+ onSyncComplete: (result, scope) => {
1149
+ console.log(`[${timestamp()}] ${syncLabel(scope)} \uC644\uB8CC \u2014 ${formatResult(result)}`);
872
1150
  },
873
- onSyncComplete: (result) => {
874
- console.log(`[${timestamp()}] \uB3D9\uAE30\uD654 \uC644\uB8CC \u2014 ${formatResult(result)}`);
1151
+ onSyncCancelled: (result, scope) => {
1152
+ console.log(
1153
+ `[${timestamp()}] ${syncLabel(scope)} \uCDE8\uC18C \u2014 \uBA48\uCD94\uAE30 \uC804\uAE4C\uC9C0 ${formatResult(result)}`
1154
+ );
875
1155
  },
876
- onSyncError: (error) => {
877
- console.error(`[${timestamp()}] \uB3D9\uAE30\uD654 \uC2E4\uD328: ${error.message}`);
1156
+ onSyncError: (error, scope) => {
1157
+ console.error(`[${timestamp()}] ${syncLabel(scope)} \uC2E4\uD328: ${error.message}`);
878
1158
  }
879
1159
  });
880
1160
  let pullTimer = null;
1161
+ let shuttingDown = false;
881
1162
  const shutdown = async () => {
1163
+ if (shuttingDown) {
1164
+ console.log(`[${timestamp()}] \uAC15\uC81C \uC885\uB8CC`);
1165
+ return process.exit(1);
1166
+ }
1167
+ shuttingDown = true;
882
1168
  console.log(`
883
- [${timestamp()}] \uAC10\uC2DC \uC885\uB8CC \uC911...`);
1169
+ [${timestamp()}] \uAC10\uC2DC \uC885\uB8CC \uC911... (\uB3C4\uB294 \uB3D9\uAE30\uD654\uB97C \uBA48\uCD94\uACE0 \uAE30\uB2E4\uB9BD\uB2C8\uB2E4)`);
884
1170
  if (pullTimer) clearInterval(pullTimer);
885
1171
  await service.stop();
886
1172
  stateDb.close();
@@ -893,30 +1179,20 @@ var watchCommand = new Command9("watch").description("\uD30C\uC77C \uBCC0\uACBD
893
1179
  console.log(`[${timestamp()}] \uD30C\uC77C \uAC10\uC2DC \uC2DC\uC791 (debounce: ${debounceMs}ms)`);
894
1180
  if (intervalSec > 0) {
895
1181
  console.log(`[${timestamp()}] \uD480 \uB3D9\uAE30\uD654 \uC8FC\uAE30: ${intervalSec}\uCD08`);
896
- pullTimer = setInterval(async () => {
897
- if (service.isSyncing()) return;
898
- try {
899
- console.log(`[${timestamp()}] \uC8FC\uAE30\uC801 \uB3D9\uAE30\uD654 \uC2DC\uC791...`);
900
- const result = await orchestrator.sync();
901
- console.log(`[${timestamp()}] \uC8FC\uAE30\uC801 \uB3D9\uAE30\uD654 \uC644\uB8CC \u2014 ${formatResult(result)}`);
902
- } catch (error) {
903
- const msg = error instanceof Error ? error.message : String(error);
904
- console.error(`[${timestamp()}] \uC8FC\uAE30\uC801 \uB3D9\uAE30\uD654 \uC2E4\uD328: ${msg}`);
905
- }
906
- }, intervalSec * 1e3);
1182
+ pullTimer = setInterval(() => service.requestFullSync(), intervalSec * 1e3);
907
1183
  }
908
1184
  await new Promise(() => {
909
1185
  });
910
1186
  });
911
1187
 
912
1188
  // src/commands/verify.ts
913
- import { Command as Command10 } from "commander";
1189
+ import { Command as Command11 } from "commander";
914
1190
  import {
915
- ConfigManager as ConfigManager10,
916
- StateDB as StateDB9,
917
- NotionClient as NotionClient10,
918
- SyncOrchestrator as SyncOrchestrator9,
919
- NodeVaultFS as NodeVaultFS9
1191
+ ConfigManager as ConfigManager11,
1192
+ StateDB as StateDB10,
1193
+ NotionClient as NotionClient11,
1194
+ SyncOrchestrator as SyncOrchestrator10,
1195
+ NodeVaultFS as NodeVaultFS10
920
1196
  } from "@im-nobsidian/core";
921
1197
  import chalk7 from "chalk";
922
1198
  var SAMPLE_LIMIT = 10;
@@ -1006,15 +1282,15 @@ function printReport(report2) {
1006
1282
  );
1007
1283
  }
1008
1284
  }
1009
- var verifyCommand = new Command10("verify").description("\uC644\uACB0\uC131 \uAC80\uC99D \u2014 \uC6D0\uACA9 DB \uD589\xB7\uD398\uC774\uC9C0\uAC00 \uBCFC\uD2B8\uC5D0 \uBE60\uC9D0\uC5C6\uC774 \uC788\uB294\uC9C0 \uB300\uC870 (API \uD638\uCD9C)").option("--json", "\uACB0\uACFC\uB97C JSON \uC73C\uB85C \uCD9C\uB825").action(async (opts) => {
1285
+ var verifyCommand = new Command11("verify").description("\uC644\uACB0\uC131 \uAC80\uC99D \u2014 \uC6D0\uACA9 DB \uD589\xB7\uD398\uC774\uC9C0\uAC00 \uBCFC\uD2B8\uC5D0 \uBE60\uC9D0\uC5C6\uC774 \uC788\uB294\uC9C0 \uB300\uC870 (API \uD638\uCD9C)").option("--json", "\uACB0\uACFC\uB97C JSON \uC73C\uB85C \uCD9C\uB825").action(async (opts) => {
1010
1286
  const cwd = process.cwd();
1011
- const configManager = new ConfigManager10(cwd);
1287
+ const configManager = new ConfigManager11(cwd);
1012
1288
  const config = await configManager.load();
1013
- const stateDb = StateDB9.open(configManager.dbPath);
1289
+ const stateDb = StateDB10.open(configManager.dbPath);
1014
1290
  try {
1015
- const client = NotionClient10.fromConfig(config);
1016
- const vaultFs = new NodeVaultFS9(cwd, config.paths);
1017
- const orchestrator = new SyncOrchestrator9(config, stateDb, client, vaultFs);
1291
+ const client = NotionClient11.fromConfig(config);
1292
+ const vaultFs = new NodeVaultFS10(cwd, config.paths);
1293
+ const orchestrator = new SyncOrchestrator10(config, stateDb, client, vaultFs);
1018
1294
  const report2 = await orchestrator.verifyCompleteness();
1019
1295
  if (opts.json) {
1020
1296
  console.log(JSON.stringify(report2, null, 2));
@@ -1044,7 +1320,7 @@ function scheduleForcedExit(proc, schedule = setTimeout) {
1044
1320
  // src/index.ts
1045
1321
  var requireJson = createRequire(import.meta.url);
1046
1322
  var { version } = requireJson("../package.json");
1047
- var program = new Command11();
1323
+ var program = new Command12();
1048
1324
  program.name("im-nobsidian").description("Im-Nobsidian: Obsidian \u2194 Notion \uC591\uBC29\uD5A5 \uB3D9\uAE30\uD654 CLI").version(version).option("--verbose", "\uC0C1\uC138 \uB85C\uADF8 \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825");
1049
1325
  program.addCommand(initCommand);
1050
1326
  program.addCommand(pushCommand);
@@ -1052,11 +1328,18 @@ program.addCommand(pullCommand);
1052
1328
  program.addCommand(syncCommand);
1053
1329
  program.addCommand(statusCommand);
1054
1330
  program.addCommand(diffCommand);
1331
+ program.addCommand(discardCommand);
1055
1332
  program.addCommand(fetchCommand);
1056
1333
  program.addCommand(resolveCommand);
1057
1334
  program.addCommand(watchCommand);
1058
1335
  program.addCommand(verifyCommand);
1059
- program.parseAsync().then(() => {
1060
- scheduleForcedExit(process);
1061
- });
1336
+ program.parseAsync().then(
1337
+ () => {
1338
+ scheduleForcedExit(process);
1339
+ },
1340
+ (error) => {
1341
+ reportStateDbFailure(error, process);
1342
+ scheduleForcedExit(process);
1343
+ }
1344
+ );
1062
1345
  //# sourceMappingURL=index.js.map