im-nobsidian 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -134,6 +134,40 @@ function restoredLines(paths, dryRun = false) {
134
134
  ...pathLines(paths)
135
135
  ];
136
136
  }
137
+ function relinkedLines(paths, pending) {
138
+ if (pending) {
139
+ return [
140
+ chalk.cyan(
141
+ "\u21BB \uC0C1\uD0DC \uAE30\uB85D\uC5D0 Notion \uACFC \uC774\uC740 \uB178\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uBC1B\uC73C\uBA74 \uAC19\uC740 \uC790\uB9AC\uC5D0 \uC788\uB294 \uB178\uD2B8\uB294 \uC0C8\uB85C \uB9CC\uB4E4\uC9C0 \uC54A\uACE0 \uADF8 \uD398\uC774\uC9C0\uC640 \uB2E4\uC2DC \uC787\uC2B5\uB2C8\uB2E4(\uAE00\uC774 \uB2E4\uB974\uBA74 \uCDA9\uB3CC). \uBBF8\uB9AC \uBCF4\uAE30\uB294 \uADF8 \uB178\uD2B8\uB4E4\uB3C4 \uC0DD\uC131\uC73C\uB85C \uC149\uB2C8\uB2E4"
142
+ )
143
+ ];
144
+ }
145
+ if (!paths || paths.length === 0) return [];
146
+ return [
147
+ chalk.cyan(
148
+ `\u21BB \uC0C1\uD0DC \uAE30\uB85D\uC5D0 \uC5C6\uB358 \uB178\uD2B8 ${paths.length}\uAC1C\uB97C \uAC19\uC740 \uC790\uB9AC\uC758 Notion \uD398\uC774\uC9C0\uC640 \uB2E4\uC2DC \uC774\uC5C8\uC2B5\uB2C8\uB2E4 \u2014 \uC0C8\uB85C \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4:`
149
+ ),
150
+ ...pathLines(paths)
151
+ ];
152
+ }
153
+ function mergedLines(paths, then, dryRun = false) {
154
+ if (!paths || paths.length === 0) return [];
155
+ return [
156
+ chalk.cyan(
157
+ `\u21BB \uC591\uCABD\uC774 \uB2E4\uB978 \uC904\uC744 \uACE0\uCE5C \uB178\uD2B8 ${paths.length}\uAC1C\uB97C ${dryRun ? "\uD569\uCE69\uB2C8\uB2E4" : "\uD569\uCCE4\uC2B5\uB2C8\uB2E4"} \u2014 ` + (then === "push" ? "push \uD558\uBA74 \uD569\uCE5C \uAE00\uC744 Notion \uC5D0 \uC62C\uB9BD\uB2C8\uB2E4:" : "\uD569\uCE5C \uAE00\uC744 Notion \uC5D0 \uC62C\uB9BD\uB2C8\uB2E4:")
158
+ ),
159
+ ...pathLines(paths)
160
+ ];
161
+ }
162
+ function plannedConflictLines(paths) {
163
+ if (paths.length === 0) return [];
164
+ return [
165
+ chalk.magenta(
166
+ `\u26A0 \uC591\uCABD\uC744 \uACE0\uCE5C \uB178\uD2B8 ${paths.length}\uAC1C\uB294 \uBC1B\uC73C\uBA74 \uCDA9\uB3CC\uB85C \uB0A8\uC2B5\uB2C8\uB2E4 \u2014 \uBC1B\uC740 \uB4A4 nobsi resolve \uB85C \uACE0\uB974\uC138\uC694:`
167
+ ),
168
+ ...pathLines(paths)
169
+ ];
170
+ }
137
171
  function schemaFollowedLines(followed) {
138
172
  const lines = [];
139
173
  for (const db of followed ?? []) {
@@ -169,6 +203,9 @@ function schemaEditLines(edits, dryRun = false) {
169
203
  ...edit.renamed.length > 0 ? [`\uC774\uB984 ${edit.renamed.map(([from, to]) => `${from} \u2192 ${to}`).join(", ")}`] : [],
170
204
  ...edit.retyped.length > 0 ? [`\uD0C0\uC785 ${edit.retyped.map(([name, type]) => `${name} \u2192 ${type}`).join(", ")}`] : [],
171
205
  ...edit.redefined.length > 0 ? [`\uC124\uC815 ${edit.redefined.join(", ")}`] : [],
206
+ ...(edit.renamedOptions ?? []).map(
207
+ ([name, from, to]) => `${name} \uC635\uC158 \uC774\uB984 ${from} \u2192 ${to}(\uADF8 \uAC12\uC744 \uAC00\uC9C4 \uD589\uB3C4 \uC62E\uAE41\uB2C8\uB2E4)`
208
+ ),
172
209
  ...(edit.droppedOptions ?? []).map(
173
210
  ([name, options]) => `${name} \uC635\uC158 \uC9C0\uC6C0 ${options.join(", ")}(\uADF8 \uAC12\uC744 \uAC00\uC9C4 \uD589\uC740 Notion \uC5D0\uC11C \uBE48 \uAC12\uC774 \uB429\uB2C8\uB2E4)`
174
211
  )
@@ -178,6 +215,12 @@ function schemaEditLines(edits, dryRun = false) {
178
215
  }
179
216
  function viewEditLines(edits, dryRun = false) {
180
217
  return (edits ?? []).map((edit) => {
218
+ if (edit.action === "created") {
219
+ return `${icons.success} .base \uC5D0\uC11C \uB354\uD55C \uBCF4\uAE30\uB97C Notion \uC5D0 ${dryRun ? "\uB9CC\uB4ED\uB2C8\uB2E4" : "\uB9CC\uB4E4\uC5C8\uC2B5\uB2C8\uB2E4"} \u2014 ${edit.path}: ${edit.view}`;
220
+ }
221
+ if (edit.action === "deleted") {
222
+ return `${icons.success} .base \uC5D0\uC11C \uC9C0\uC6B4 \uBCF4\uAE30\uB97C Notion \uC5D0\uC11C ${dryRun ? "\uC9C0\uC6C1\uB2C8\uB2E4" : "\uC9C0\uC6E0\uC2B5\uB2C8\uB2E4"} \u2014 ${edit.path}: ${edit.view}`;
223
+ }
181
224
  const name = edit.from ? `${edit.from} \u2192 ${edit.view}` : edit.view;
182
225
  return `${icons.success} .base \uC5D0\uC11C \uACE0\uCE5C \uBCF4\uAE30\uB97C Notion \uC5D0 ${dryRun ? "\uC62C\uB9BD\uB2C8\uB2E4" : "\uC62C\uB838\uC2B5\uB2C8\uB2E4"} \u2014 ${edit.path}: ${name} (${edit.changed.map((field) => VIEW_FIELD_LABELS[field]).join(" \xB7 ")})`;
183
226
  });
@@ -206,7 +249,7 @@ function heldBaseLines(paths) {
206
249
  if (!paths || paths.length === 0) return [];
207
250
  return [
208
251
  chalk.yellow(
209
- `\u26A0 \uBCF4\uAE30\uB97C \uACE0\uCE5C .base ${paths.length}\uAC1C\uB294 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 push \uD558\uBA74 \uACE0\uCE5C \uBCF4\uAE30 \uC774\uB984 \xB7 \uC5F4 \xB7 \uC815\uB82C \xB7 \uD544\uD130\uB97C Notion \uC5D0 \uC62C\uB9BD\uB2C8\uB2E4:`
252
+ `\u26A0 \uBCF4\uAE30\uB97C \uACE0\uCE5C .base ${paths.length}\uAC1C\uB294 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 push \uD558\uBA74 \uACE0\uCE5C \uBCF4\uAE30 \uC774\uB984 \xB7 \uC5F4 \xB7 \uC815\uB82C \xB7 \uD544\uD130\uC640 \uB354\uD558\uACE0 \uC9C0\uC6B4 \uBCF4\uAE30\uB97C Notion \uC5D0 \uC62C\uB9BD\uB2C8\uB2E4:`
210
253
  ),
211
254
  ...pathLines(paths)
212
255
  ];
@@ -232,7 +275,7 @@ function readOnlySkipLines(skips, dryRun = false) {
232
275
  }
233
276
 
234
277
  // src/utils/vault.ts
235
- import { existsSync } from "fs";
278
+ import { existsSync, readFileSync } from "fs";
236
279
  import { dirname, join, relative } from "path";
237
280
  import {
238
281
  ConfigManager,
@@ -241,26 +284,57 @@ import {
241
284
  matchesPathScope,
242
285
  ScopedVaultFS,
243
286
  findConnectionFolders,
244
- OBSIDIAN_CONFIG_DIR
287
+ OBSIDIAN_CONFIG_DIR,
288
+ IGNORE_FILE,
289
+ obsidianOnlyExcludes,
290
+ templateFolders
245
291
  } from "@im-nobsidian/core";
246
292
  async function nestedConnections(cwd) {
247
293
  return (await findConnectionFolders(cwd)).filter((folder) => folder !== "");
248
294
  }
249
295
  async function openVaultFs(cwd, paths) {
250
- const base = new NodeVaultFS(cwd, paths);
296
+ const base = new NodeVaultFS(cwd, {
297
+ ...paths,
298
+ exclude: [...paths.exclude ?? [], ...obsidianOnlyPatterns(cwd, paths)]
299
+ });
251
300
  const nested = await nestedConnections(cwd);
252
301
  return nested.length === 0 ? base : new ScopedVaultFS(base, "", ["", ...nested]);
253
302
  }
303
+ async function outsideVault(cwd, paths) {
304
+ const root = vaultRoot(cwd);
305
+ if (root === null) return void 0;
306
+ const folder = relative(root, cwd).split("\\").join("/");
307
+ const whole = folder === "" && (paths.include ?? []).length === 0 && (paths.exclude ?? []).length === 0 && paths.excludeObsidianOnly !== true && !existsSync(join(cwd, IGNORE_FILE)) && (await nestedConnections(cwd)).length === 0;
308
+ return whole ? void 0 : { fs: new NodeVaultFS(root, {}, false), folder };
309
+ }
310
+ function obsidianOnlyPatterns(cwd, paths) {
311
+ if (paths.excludeObsidianOnly !== true) return [];
312
+ const root = vaultRoot(cwd);
313
+ if (root === null) return obsidianOnlyExcludes([], "");
314
+ const read = (file) => {
315
+ try {
316
+ return readFileSync(join(root, OBSIDIAN_CONFIG_DIR, file), "utf-8");
317
+ } catch {
318
+ return null;
319
+ }
320
+ };
321
+ return obsidianOnlyExcludes(
322
+ templateFolders(read),
323
+ relative(root, cwd).split("\\").join("/").normalize("NFC")
324
+ );
325
+ }
326
+ function vaultRoot(cwd) {
327
+ for (let dir = cwd; ; dir = dirname(dir)) {
328
+ if (existsSync(join(dir, OBSIDIAN_CONFIG_DIR))) return dir;
329
+ if (dirname(dir) === dir) return null;
330
+ }
331
+ }
254
332
  function vaultArgPath(path, platform = process.platform) {
255
333
  return platform === "win32" ? path.replace(/\\/g, "/") : path;
256
334
  }
257
335
  function obsidianConfigDir(cwd) {
258
- for (let dir = cwd; ; dir = dirname(dir)) {
259
- if (existsSync(join(dir, OBSIDIAN_CONFIG_DIR))) {
260
- return relative(cwd, join(dir, OBSIDIAN_CONFIG_DIR)).split("\\").join("/");
261
- }
262
- if (dirname(dir) === dir) return OBSIDIAN_CONFIG_DIR;
263
- }
336
+ const root = vaultRoot(cwd);
337
+ return root === null ? OBSIDIAN_CONFIG_DIR : relative(cwd, join(root, OBSIDIAN_CONFIG_DIR)).split("\\").join("/");
264
338
  }
265
339
  function outerConnectionTracking(cwd) {
266
340
  for (let dir = dirname(cwd); ; dir = dirname(dir)) {
@@ -403,12 +477,31 @@ function extractPageTitle(page) {
403
477
  import { Command as Command2 } from "commander";
404
478
  import { ConfigManager as ConfigManager3, StateDB as StateDB2, NotionClient as NotionClient2, SyncOrchestrator } from "@im-nobsidian/core";
405
479
 
406
- // src/utils/json-output.ts
480
+ // src/utils/log-level.ts
407
481
  import { setLogger } from "@im-nobsidian/core";
482
+ var level = "normal";
483
+ var noop = () => {
484
+ };
485
+ function applyLogLevel(next, sink) {
486
+ level = next;
487
+ const out = (fallback) => sink ?? fallback;
488
+ setLogger({
489
+ error: out((message, ...args) => console.error(message, ...args)),
490
+ warn: level === "quiet" ? noop : out((message, ...args) => console.warn(message, ...args)),
491
+ info: level === "verbose" ? out((message, ...args) => console.info(message, ...args)) : noop,
492
+ debug: level === "verbose" ? out((message, ...args) => console.debug(message, ...args)) : noop
493
+ });
494
+ }
495
+ function currentLogLevel() {
496
+ return level;
497
+ }
498
+ function logLevelOf(options) {
499
+ return options.verbose ? "verbose" : options.quiet ? "quiet" : "normal";
500
+ }
501
+
502
+ // src/utils/json-output.ts
408
503
  function enableJsonMode() {
409
- const toStderr = (message, ...args) => console.error(message, ...args);
410
- setLogger({ warn: toStderr, error: toStderr, info: toStderr, debug: () => {
411
- } });
504
+ applyLogLevel(currentLogLevel(), (message, ...args) => console.error(message, ...args));
412
505
  }
413
506
  function pushDbChangeCount(result) {
414
507
  return (result.createdDatabases ?? 0) + (result.renamedProperties?.length ?? 0) + (result.schemaEdits?.length ?? 0) + (result.viewEdits?.length ?? 0);
@@ -439,6 +532,9 @@ function pullJson(result) {
439
532
  deleted: result.deleted,
440
533
  restored: result.restored,
441
534
  restoredPaths: result.restoredPaths ?? [],
535
+ mergedPaths: result.mergedPaths ?? [],
536
+ relinkedPaths: result.relinkedPaths ?? [],
537
+ relinkPending: result.relinkPending ?? false,
442
538
  schemaFollowed: result.schemaFollowed ?? [],
443
539
  heldSidecars: result.heldSidecars ?? [],
444
540
  heldBases: result.heldBases ?? [],
@@ -463,8 +559,41 @@ function printJson(value) {
463
559
  `);
464
560
  }
465
561
 
562
+ // src/utils/base-filter-prompt.ts
563
+ import { confirm as confirm2 } from "@inquirer/prompts";
564
+
565
+ // src/utils/interactive.ts
566
+ function isInteractive() {
567
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
568
+ }
569
+
570
+ // src/utils/base-filter-prompt.ts
571
+ async function askUserBaseFilterFixes(orchestrator, options) {
572
+ if (options.dryRun) return;
573
+ const fixes = orchestrator.userBaseFilterFixes();
574
+ if (fixes.length === 0) return;
575
+ console.log(`
576
+ ${icons.conflict} \uC9C1\uC811 \uB9CC\uB4E0 .base \uC758 \uD3F4\uB354 \uD544\uD130\uAC00 \uC62E\uAE34 \uD3F4\uB354\uB97C \uAC00\uB9AC\uD0B5\uB2C8\uB2E4:`);
577
+ for (const fix of fixes) console.log(` ${dimText(`\xB7 ${fix.path}: ${fix.from} \u2192 ${fix.to}`)}`);
578
+ let accept;
579
+ if (options.yes) {
580
+ accept = true;
581
+ } else if (!isInteractive()) {
582
+ console.log(
583
+ ` ${dimText("\uACE0\uCE58\uB824\uBA74 --yes \uB97C \uBD99\uC5EC \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694 \u2014 \uB2E4\uC74C \uC2E4\uD589\uC5D0\uC11C \uB2E4\uC2DC \uBB3B\uC2B5\uB2C8\uB2E4")}`
584
+ );
585
+ return;
586
+ } else {
587
+ accept = await confirm2({ message: "\uC0C8 \uD3F4\uB354\uB85C \uACE0\uCE60\uAE4C\uC694?", default: true });
588
+ }
589
+ const fixed = await orchestrator.resolveUserBaseFilterFixes(accept);
590
+ console.log(
591
+ accept ? ` ${icons.success} .base ${fixed.length}\uAC1C\uC758 \uD3F4\uB354 \uD544\uD130\uB97C \uACE0\uCCE4\uC2B5\uB2C8\uB2E4` : ` ${dimText("\uADF8\uB300\uB85C \uB461\uB2C8\uB2E4 \u2014 \uB2E4\uC2DC \uBB3B\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4")}`
592
+ );
593
+ }
594
+
466
595
  // src/commands/push.ts
467
- 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) => {
596
+ 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)").option("-y, --yes", "\uC9C1\uC811 \uB9CC\uB4E0 .base \uC758 \uD3F4\uB354 \uD544\uD130\uB97C \uBB3B\uC9C0 \uC54A\uACE0 \uACE0\uCE68 (\uBE44\uB300\uD654\uD615/CI)").action(async (options) => {
468
597
  const json = options.json === true;
469
598
  if (json) enableJsonMode();
470
599
  const cwd = process.cwd();
@@ -485,6 +614,7 @@ ${header(" Pushing to Notion...")}`);
485
614
  dryRun: options.dryRun,
486
615
  paths: options.path?.map((path) => vaultArgPath(path)),
487
616
  onProgress: json ? void 0 : (current, total, item) => {
617
+ if (item.unchanged) return;
488
618
  const icon = operationIcon(item.operation);
489
619
  const label = operationLabel(item.operation);
490
620
  const prog = progress(current, total);
@@ -533,6 +663,7 @@ ${header(" Pushing to Notion...")}`);
533
663
  }
534
664
  process.exitCode = 1;
535
665
  }
666
+ await askUserBaseFilterFixes(orchestrator, options);
536
667
  } finally {
537
668
  stateDb.close();
538
669
  }
@@ -545,7 +676,7 @@ import {
545
676
  StateDB as StateDB3,
546
677
  NotionClient as NotionClient3,
547
678
  SyncOrchestrator as SyncOrchestrator2,
548
- IGNORE_FILE
679
+ IGNORE_FILE as IGNORE_FILE2
549
680
  } from "@im-nobsidian/core";
550
681
  import chalk3 from "chalk";
551
682
  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(
@@ -556,7 +687,7 @@ var pullCommand = new Command3("pull").description("Notion \uBCC0\uACBD\uC0AC\uD
556
687
  "--rerender",
557
688
  "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)"
558
689
  ).conflicts("dryRun")
559
- ).option("--json", "\uACB0\uACFC\uB97C JSON \uD55C \uC904\uB85C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9 \u2014 \uB85C\uADF8\uB294 stderr)").action(async (options) => {
690
+ ).option("--json", "\uACB0\uACFC\uB97C JSON \uD55C \uC904\uB85C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9 \u2014 \uB85C\uADF8\uB294 stderr)").option("-y, --yes", "\uC9C1\uC811 \uB9CC\uB4E0 .base \uC758 \uD3F4\uB354 \uD544\uD130\uB97C \uBB3B\uC9C0 \uC54A\uACE0 \uACE0\uCE68 (\uBE44\uB300\uD654\uD615/CI)").action(async (options) => {
560
691
  const json = options.json === true;
561
692
  if (json) enableJsonMode();
562
693
  const cwd = process.cwd();
@@ -566,7 +697,14 @@ var pullCommand = new Command3("pull").description("Notion \uBCC0\uACBD\uC0AC\uD
566
697
  try {
567
698
  const client = NotionClient3.fromConfig(config);
568
699
  const vaultFs = await openVaultFs(cwd, config.paths);
569
- const orchestrator = new SyncOrchestrator2(config, stateDb, client, vaultFs);
700
+ const orchestrator = new SyncOrchestrator2(
701
+ config,
702
+ stateDb,
703
+ client,
704
+ vaultFs,
705
+ void 0,
706
+ await outsideVault(cwd, config.paths)
707
+ );
570
708
  if (!json) {
571
709
  console.log(`
572
710
  ${header(" Pulling from Notion...")}`);
@@ -620,6 +758,12 @@ ${header(" Pulling from Notion...")}`);
620
758
  for (const line of restoredLines(result.restoredPaths, options.dryRun)) {
621
759
  console.log(` ${line}`);
622
760
  }
761
+ for (const line of relinkedLines(result.relinkedPaths, result.relinkPending)) {
762
+ console.log(` ${line}`);
763
+ }
764
+ for (const line of mergedLines(result.mergedPaths, "push", options.dryRun)) {
765
+ console.log(` ${line}`);
766
+ }
623
767
  for (const move of result.moved ?? []) {
624
768
  const verb = move.renamed ? "rename" : "move";
625
769
  console.log(
@@ -629,7 +773,7 @@ ${header(" Pulling from Notion...")}`);
629
773
  const excluded = result.excluded ?? [];
630
774
  if (excluded.length > 0) {
631
775
  console.log(
632
- ` ${icons.conflict} ${dimText(`${excluded.length} new in Notion ${options.dryRun ? "would not be" : "not"} pulled \u2014 excluded by paths / ${IGNORE_FILE}:`)}`
776
+ ` ${icons.conflict} ${dimText(`${excluded.length} new in Notion ${options.dryRun ? "would not be" : "not"} pulled \u2014 excluded by paths / ${IGNORE_FILE2}:`)}`
633
777
  );
634
778
  for (const path of excluded.slice(0, LISTED_ITEMS))
635
779
  console.log(` ${dimText(`\xB7 ${path}`)}`);
@@ -648,7 +792,13 @@ ${header(" Pulling from Notion...")}`);
648
792
  if (result.remoteScan) {
649
793
  for (const line of remoteScanLines(result.remoteScan)) console.log(` ${line}`);
650
794
  }
651
- if (result.conflicts.length > 0) {
795
+ if (options.dryRun) {
796
+ const planned = plannedConflictLines(
797
+ result.conflicts.map((conflict) => conflict.syncRecord.obsidianPath)
798
+ );
799
+ if (planned.length > 0) console.log("");
800
+ for (const line of planned) console.log(` ${line}`);
801
+ } else if (result.conflicts.length > 0) {
652
802
  console.log(
653
803
  `
654
804
  ${icons.conflict} ${chalk3.magenta(`${result.conflicts.length} conflicts`)} \u2014 run ${chalk3.cyan("nobsi resolve")}`
@@ -661,6 +811,7 @@ ${header(" Pulling from Notion...")}`);
661
811
  }
662
812
  process.exitCode = 1;
663
813
  }
814
+ await askUserBaseFilterFixes(orchestrator, options);
664
815
  } finally {
665
816
  stateDb.close();
666
817
  }
@@ -670,7 +821,7 @@ ${header(" Pulling from Notion...")}`);
670
821
  import { Command as Command4 } from "commander";
671
822
  import { ConfigManager as ConfigManager5, StateDB as StateDB4, NotionClient as NotionClient4, SyncOrchestrator as SyncOrchestrator3 } from "@im-nobsidian/core";
672
823
  import chalk4 from "chalk";
673
- 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) => {
824
+ 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)").option("-y, --yes", "\uC9C1\uC811 \uB9CC\uB4E0 .base \uC758 \uD3F4\uB354 \uD544\uD130\uB97C \uBB3B\uC9C0 \uC54A\uACE0 \uACE0\uCE68 (\uBE44\uB300\uD654\uD615/CI)").action(async (options) => {
674
825
  const json = options.json === true;
675
826
  if (json) enableJsonMode();
676
827
  const log = json ? () => {
@@ -682,7 +833,14 @@ var syncCommand = new Command4("sync").description("\uC591\uBC29\uD5A5 \uB3D9\uA
682
833
  try {
683
834
  const client = NotionClient4.fromConfig(config);
684
835
  const vaultFs = await openVaultFs(cwd, config.paths);
685
- const orchestrator = new SyncOrchestrator3(config, stateDb, client, vaultFs);
836
+ const orchestrator = new SyncOrchestrator3(
837
+ config,
838
+ stateDb,
839
+ client,
840
+ vaultFs,
841
+ void 0,
842
+ await outsideVault(cwd, config.paths)
843
+ );
686
844
  log(`
687
845
  ${header(" Bidirectional Sync")}`);
688
846
  log(` ${separator()}`);
@@ -745,6 +903,12 @@ ${header(" Bidirectional Sync")}`);
745
903
  for (const line of restoredLines(pullResult.restoredPaths, options.dryRun)) {
746
904
  console.log(` ${line}`);
747
905
  }
906
+ for (const line of relinkedLines(pullResult.relinkedPaths, pullResult.relinkPending)) {
907
+ console.log(` ${line}`);
908
+ }
909
+ for (const line of mergedLines(pullResult.mergedPaths, "pushed", options.dryRun)) {
910
+ console.log(` ${line}`);
911
+ }
748
912
  for (const line of unsentDeletionLines(pushResult.unsentDeletions, options.dryRun)) {
749
913
  console.log(` ${line}`);
750
914
  }
@@ -780,7 +944,13 @@ ${header(" Bidirectional Sync")}`);
780
944
  if (pullResult.remoteScan) {
781
945
  for (const line of remoteScanLines(pullResult.remoteScan)) console.log(` ${line}`);
782
946
  }
783
- if (result.conflicts.length > 0) {
947
+ if (options.dryRun) {
948
+ const planned = plannedConflictLines(
949
+ result.conflicts.map((conflict) => conflict.syncRecord.obsidianPath)
950
+ );
951
+ if (planned.length > 0) console.log("");
952
+ for (const line of planned) console.log(` ${line}`);
953
+ } else if (result.conflicts.length > 0) {
784
954
  console.log(
785
955
  `
786
956
  ${icons.conflict} ${chalk4.magenta(`${result.conflicts.length} conflicts`)} \u2014 run ${chalk4.cyan("nobsi resolve")}`
@@ -792,6 +962,7 @@ ${header(" Bidirectional Sync")}`);
792
962
  for (const f of pushResult.failed) failedItem(f.path, f.error, "\u25B2");
793
963
  process.exitCode = 1;
794
964
  }
965
+ await askUserBaseFilterFixes(orchestrator, options);
795
966
  } finally {
796
967
  stateDb.close();
797
968
  }
@@ -804,7 +975,7 @@ import {
804
975
  StateDB as StateDB5,
805
976
  NotionClient as NotionClient5,
806
977
  SyncOrchestrator as SyncOrchestrator4,
807
- IGNORE_FILE as IGNORE_FILE2,
978
+ IGNORE_FILE as IGNORE_FILE3,
808
979
  isRemoteDeletion,
809
980
  shortNotionId
810
981
  } from "@im-nobsidian/core";
@@ -979,7 +1150,7 @@ ${header(" Sync Status")}`);
979
1150
  if (remoteExcluded.length > 0) {
980
1151
  console.log(
981
1152
  `
982
- ${header(`New in Notion, not pulled (excluded by paths / ${IGNORE_FILE2}): ${remoteExcluded.length}`)}`
1153
+ ${header(`New in Notion, not pulled (excluded by paths / ${IGNORE_FILE3}): ${remoteExcluded.length}`)}`
983
1154
  );
984
1155
  for (const path of remoteExcluded.slice(0, LISTED_ITEMS)) {
985
1156
  console.log(` ${dimText("\xB7")} ${path}`);
@@ -1214,7 +1385,7 @@ ${header(" Remote Scan")}`);
1214
1385
 
1215
1386
  // src/commands/resolve.ts
1216
1387
  import { Command as Command9 } from "commander";
1217
- import { select, confirm as confirm2 } from "@inquirer/prompts";
1388
+ import { select, confirm as confirm3 } from "@inquirer/prompts";
1218
1389
  import {
1219
1390
  ConfigManager as ConfigManager10,
1220
1391
  StateDB as StateDB9,
@@ -1225,14 +1396,23 @@ import {
1225
1396
  } from "@im-nobsidian/core";
1226
1397
 
1227
1398
  // src/utils/state-db-failure.ts
1228
- import { StateDbUnavailableError } from "@im-nobsidian/core";
1399
+ import {
1400
+ ConnectionBoundaryError,
1401
+ ConnectionScopeError,
1402
+ StateDbUnavailableError,
1403
+ SyncBusyError,
1404
+ UnlinkedVaultError
1405
+ } from "@im-nobsidian/core";
1229
1406
  function describeFailure(error) {
1230
1407
  const message = error instanceof Error ? error.message : String(error);
1231
1408
  if (!(error instanceof StateDbUnavailableError)) return message;
1232
1409
  return `${message} \u2014 ${error.guidance("\uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694")}`;
1233
1410
  }
1234
- function reportStateDbFailure(error, proc) {
1235
- if (!(error instanceof StateDbUnavailableError)) throw error;
1411
+ function isUserFailure(error) {
1412
+ return error instanceof StateDbUnavailableError || error instanceof UnlinkedVaultError || error instanceof SyncBusyError || error instanceof ConnectionBoundaryError || error instanceof ConnectionScopeError;
1413
+ }
1414
+ function reportUserFailure(error, proc) {
1415
+ if (!isUserFailure(error)) throw error;
1236
1416
  console.error(`\uC624\uB958: ${describeFailure(error)}`);
1237
1417
  proc.exitCode = 1;
1238
1418
  }
@@ -1256,9 +1436,6 @@ function choiceName(conflict, choice, uploads) {
1256
1436
  const deleted = isRemoteDeletion2(conflict);
1257
1437
  return (deleted && !uploads ? PULL_ONLY_REMOTE_DELETION_CHOICE_NAMES[choice] : void 0) ?? (deleted ? REMOTE_DELETION_CHOICE_NAMES[choice] : void 0) ?? CHOICE_NAMES[choice] ?? choice;
1258
1438
  }
1259
- function isInteractive() {
1260
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
1261
- }
1262
1439
  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) => {
1263
1440
  if (options.strategy && !STRATEGIES.includes(options.strategy)) {
1264
1441
  console.error(`\uC54C \uC218 \uC5C6\uB294 \uC804\uB7B5 '${options.strategy}' \u2014 \uC0AC\uC6A9 \uAC00\uB2A5: ${STRATEGIES.join(", ")}`);
@@ -1314,7 +1491,7 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
1314
1491
  process.exitCode = 1;
1315
1492
  return;
1316
1493
  }
1317
- const shouldProceed = await confirm2({
1494
+ const shouldProceed = await confirm3({
1318
1495
  message: `${pending.length}\uAC74\uC758 \uCDA9\uB3CC\uC744 '${strategy}' \uC804\uB7B5\uC73C\uB85C \uC77C\uAD04 \uD574\uACB0\uD560\uAE4C\uC694?`
1319
1496
  });
1320
1497
  if (!shouldProceed) return;
@@ -1441,7 +1618,14 @@ var watchCommand = new Command10("watch").description("\uD30C\uC77C \uBCC0\uACBD
1441
1618
  const stateDb = StateDB10.open(configManager.dbPath);
1442
1619
  const client = NotionClient10.fromConfig(config);
1443
1620
  const vaultFs = await openVaultFs(cwd, config.paths);
1444
- const orchestrator = new SyncOrchestrator9(config, stateDb, client, vaultFs);
1621
+ const orchestrator = new SyncOrchestrator9(
1622
+ config,
1623
+ stateDb,
1624
+ client,
1625
+ vaultFs,
1626
+ void 0,
1627
+ await outsideVault(cwd, config.paths)
1628
+ );
1445
1629
  const service = new WatchSyncService(cwd, orchestrator, {
1446
1630
  debounceMs,
1447
1631
  onFileChange: (event, path) => {
@@ -1682,7 +1866,7 @@ function scheduleForcedExit(proc, schedule = setTimeout) {
1682
1866
  var requireJson = createRequire(import.meta.url);
1683
1867
  var { version } = requireJson("../package.json");
1684
1868
  var program = new Command13();
1685
- 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");
1869
+ program.name("im-nobsidian").description("Im-Nobsidian: Obsidian \u2194 Notion \uC591\uBC29\uD5A5 \uB3D9\uAE30\uD654 CLI").version(version).option("--verbose", "\uC5D4\uC9C4\uC758 \uC815\uBCF4 \xB7 \uB514\uBC84\uADF8 \uB85C\uADF8\uB3C4 \uCD9C\uB825").option("--quiet", "\uACBD\uACE0 \uB85C\uADF8\uB97C \uC228\uAE30\uACE0 \uC624\uB958 \uB85C\uADF8\uB9CC \uCD9C\uB825").hook("preAction", () => applyLogLevel(logLevelOf(program.opts())));
1686
1870
  program.addCommand(initCommand);
1687
1871
  program.addCommand(pushCommand);
1688
1872
  program.addCommand(pullCommand);
@@ -1700,7 +1884,7 @@ program.parseAsync().then(
1700
1884
  scheduleForcedExit(process);
1701
1885
  },
1702
1886
  (error) => {
1703
- reportStateDbFailure(error, process);
1887
+ reportUserFailure(error, process);
1704
1888
  scheduleForcedExit(process);
1705
1889
  }
1706
1890
  );