im-nobsidian 0.3.2 → 0.9.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 +611 -272
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
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
|
|
5
|
+
import { Command as Command13 } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ConfigManager as ConfigManager2,
|
|
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,31 +70,124 @@ 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
|
-
|
|
109
|
+
function failedItem(path, error, direction) {
|
|
110
|
+
const where = `${direction ? `${direction} ` : ""}${path || "/"}`;
|
|
111
|
+
console.log(` ${icons.fail} ${where}: ${chalk.red(error)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/utils/vault.ts
|
|
115
|
+
import { existsSync } from "fs";
|
|
116
|
+
import { dirname, join, relative } from "path";
|
|
117
|
+
import {
|
|
118
|
+
ConfigManager,
|
|
119
|
+
NodeVaultFS,
|
|
120
|
+
StateDB,
|
|
121
|
+
matchesPathScope,
|
|
122
|
+
ScopedVaultFS,
|
|
123
|
+
findConnectionFolders,
|
|
124
|
+
OBSIDIAN_CONFIG_DIR
|
|
125
|
+
} from "@im-nobsidian/core";
|
|
126
|
+
async function nestedConnections(cwd) {
|
|
127
|
+
return (await findConnectionFolders(cwd)).filter((folder) => folder !== "");
|
|
128
|
+
}
|
|
129
|
+
async function openVaultFs(cwd, paths) {
|
|
130
|
+
const base = new NodeVaultFS(cwd, paths);
|
|
131
|
+
const nested = await nestedConnections(cwd);
|
|
132
|
+
return nested.length === 0 ? base : new ScopedVaultFS(base, "", ["", ...nested]);
|
|
133
|
+
}
|
|
134
|
+
function obsidianConfigDir(cwd) {
|
|
135
|
+
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
136
|
+
if (existsSync(join(dir, OBSIDIAN_CONFIG_DIR))) {
|
|
137
|
+
return relative(cwd, join(dir, OBSIDIAN_CONFIG_DIR)).split("\\").join("/");
|
|
138
|
+
}
|
|
139
|
+
if (dirname(dir) === dir) return OBSIDIAN_CONFIG_DIR;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function outerConnectionTracking(cwd) {
|
|
143
|
+
for (let dir = dirname(cwd); ; dir = dirname(dir)) {
|
|
144
|
+
const manager = new ConfigManager(dir);
|
|
145
|
+
if (existsSync(manager.dbPath)) {
|
|
146
|
+
const scope = relative(dir, cwd).split("\\").join("/");
|
|
147
|
+
const db = StateDB.open(manager.dbPath);
|
|
148
|
+
try {
|
|
149
|
+
const notes = db.getAll().filter(
|
|
150
|
+
(r) => r.obsidianPath !== scope && matchesPathScope(r.obsidianPath, scope)
|
|
151
|
+
).length;
|
|
152
|
+
return notes > 0 ? { folder: dir, notes } : null;
|
|
153
|
+
} finally {
|
|
154
|
+
db.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (dirname(dir) === dir) return null;
|
|
158
|
+
}
|
|
72
159
|
}
|
|
73
160
|
|
|
74
161
|
// src/commands/init.ts
|
|
75
|
-
var
|
|
162
|
+
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.";
|
|
163
|
+
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
164
|
const cwd = process.cwd();
|
|
77
|
-
const configManager = new
|
|
165
|
+
const configManager = new ConfigManager2(cwd);
|
|
78
166
|
console.log(`
|
|
79
167
|
${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
|
|
80
168
|
console.log(` ${separator()}`);
|
|
169
|
+
const outer = outerConnectionTracking(cwd);
|
|
170
|
+
if (outer) {
|
|
171
|
+
console.error(
|
|
172
|
+
`\uC774 \uD3F4\uB354\uB294 \uC774\uBBF8 ${outer.folder} \uC758 \uC5F0\uACB0\uC774 \uB178\uD2B8 ${outer.notes}\uAC1C\uB97C \uB3D9\uAE30\uD654\uD558\uACE0 \uC788\uC5B4 \uB530\uB85C \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
173
|
+
);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
81
177
|
if (options.nonInteractive) {
|
|
82
178
|
if (!options.token || !options.rootPageId) {
|
|
83
179
|
console.error("\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC\uC5D0\uC11C\uB294 --token\uACFC --root-page-id\uAC00 \uD544\uC218\uC785\uB2C8\uB2E4.");
|
|
84
180
|
process.exitCode = 1;
|
|
85
181
|
return;
|
|
86
182
|
}
|
|
87
|
-
if (!options.token.startsWith(
|
|
88
|
-
console.error(
|
|
183
|
+
if (!options.token.startsWith(NOTION_TOKEN_PREFIX)) {
|
|
184
|
+
console.error(`\uD1A0\uD070\uC740 ${NOTION_TOKEN_PREFIX}\uC73C\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.`);
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const rootPageId = notionIdFromInput(options.rootPageId);
|
|
189
|
+
if (rootPageId === null) {
|
|
190
|
+
console.error(ROOT_PAGE_UNREADABLE);
|
|
89
191
|
process.exitCode = 1;
|
|
90
192
|
return;
|
|
91
193
|
}
|
|
@@ -95,10 +197,9 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
|
|
|
95
197
|
await client.search({ pageSize: 1, filter: { property: "object", value: "page" } });
|
|
96
198
|
spinner2.stop();
|
|
97
199
|
console.log(` ${icons.success} Token validated`);
|
|
98
|
-
await configManager.init({ token: options.token, rootPageId
|
|
200
|
+
await configManager.init({ token: options.token, rootPageId });
|
|
99
201
|
console.log(`
|
|
100
202
|
${icons.success} Config saved to ${dimText(configManager.configPath)}`);
|
|
101
|
-
console.log(` ${icons.success} State DB initialized`);
|
|
102
203
|
console.log(
|
|
103
204
|
`
|
|
104
205
|
${header(chalk2.green("Ready!"))} Run ${chalk2.cyan("nobsi sync")} to start syncing.`
|
|
@@ -120,7 +221,7 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
|
|
|
120
221
|
}
|
|
121
222
|
const token = await input({
|
|
122
223
|
message: "Notion Integration Token:",
|
|
123
|
-
validate: (v) => v.startsWith(
|
|
224
|
+
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
225
|
});
|
|
125
226
|
const spinner = ora("Notion \uC5F0\uACB0 \uD655\uC778 \uC911...").start();
|
|
126
227
|
try {
|
|
@@ -144,14 +245,15 @@ ${header(chalk2.cyan(" Im-Nobsidian Setup"))}`);
|
|
|
144
245
|
const title = extractPageTitle(page);
|
|
145
246
|
console.log(` ${dimText(`${i + 1}.`)} ${title}`);
|
|
146
247
|
}
|
|
147
|
-
const
|
|
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
|
|
248
|
+
const rootPageInput = await input({
|
|
249
|
+
message: "\uB8E8\uD2B8 \uD398\uC774\uC9C0 ID \uB610\uB294 \uC8FC\uC18C (\uC704\uC5D0\uC11C \uC120\uD0DD\uD558\uAC70\uB098 \uC9C1\uC811 \uC785\uB825):",
|
|
250
|
+
default: result.results[0]?.id,
|
|
251
|
+
validate: (value) => notionIdFromInput(value) !== null || ROOT_PAGE_UNREADABLE
|
|
150
252
|
});
|
|
253
|
+
const rootPageId = notionIdFromInput(rootPageInput);
|
|
151
254
|
await configManager.init({ token, rootPageId });
|
|
152
255
|
console.log(`
|
|
153
256
|
${icons.success} Config saved to ${dimText(configManager.configPath)}`);
|
|
154
|
-
console.log(` ${icons.success} State DB initialized`);
|
|
155
257
|
console.log(
|
|
156
258
|
`
|
|
157
259
|
${header(chalk2.green("Ready!"))} Run ${chalk2.cyan("nobsi sync")} to start syncing.`
|
|
@@ -176,42 +278,91 @@ function extractPageTitle(page) {
|
|
|
176
278
|
|
|
177
279
|
// src/commands/push.ts
|
|
178
280
|
import { Command as Command2 } from "commander";
|
|
179
|
-
import {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
281
|
+
import { ConfigManager as ConfigManager3, StateDB as StateDB2, NotionClient as NotionClient2, SyncOrchestrator } from "@im-nobsidian/core";
|
|
282
|
+
|
|
283
|
+
// src/utils/json-output.ts
|
|
284
|
+
import { setLogger } from "@im-nobsidian/core";
|
|
285
|
+
function enableJsonMode() {
|
|
286
|
+
const toStderr = (message, ...args) => console.error(message, ...args);
|
|
287
|
+
setLogger({ warn: toStderr, error: toStderr, info: toStderr, debug: () => {
|
|
288
|
+
} });
|
|
289
|
+
}
|
|
290
|
+
function pushJson(result) {
|
|
291
|
+
return {
|
|
292
|
+
created: result.created,
|
|
293
|
+
updated: result.updated,
|
|
294
|
+
deleted: result.deleted,
|
|
295
|
+
moved: result.moved,
|
|
296
|
+
churn: result.created + result.updated + result.deleted + result.moved + (result.createdDatabases ?? 0),
|
|
297
|
+
createdDatabases: result.createdDatabases ?? 0,
|
|
298
|
+
failed: result.failed,
|
|
299
|
+
durationMs: result.duration
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function pullJson(result) {
|
|
303
|
+
return {
|
|
304
|
+
created: result.created,
|
|
305
|
+
updated: result.updated,
|
|
306
|
+
deleted: result.deleted,
|
|
307
|
+
restored: result.restored,
|
|
308
|
+
churn: result.created + result.updated + result.deleted + result.restored,
|
|
309
|
+
conflicts: result.conflicts.length,
|
|
310
|
+
failed: result.failed,
|
|
311
|
+
writtenPaths: result.writtenPaths.length,
|
|
312
|
+
images: result.imageCount,
|
|
313
|
+
files: result.fileCount,
|
|
314
|
+
links: result.linkCount,
|
|
315
|
+
durationMs: result.duration,
|
|
316
|
+
remoteScan: result.remoteScan ?? null
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function printJson(value) {
|
|
320
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/commands/push.ts
|
|
325
|
+
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) => {
|
|
326
|
+
const json = options.json === true;
|
|
327
|
+
if (json) enableJsonMode();
|
|
187
328
|
const cwd = process.cwd();
|
|
188
|
-
const configManager = new
|
|
329
|
+
const configManager = new ConfigManager3(cwd);
|
|
189
330
|
const config = await configManager.load();
|
|
190
|
-
const stateDb =
|
|
331
|
+
const stateDb = StateDB2.open(configManager.dbPath);
|
|
191
332
|
try {
|
|
192
333
|
const client = NotionClient2.fromConfig(config);
|
|
193
|
-
const vaultFs =
|
|
334
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
194
335
|
const orchestrator = new SyncOrchestrator(config, stateDb, client, vaultFs);
|
|
195
|
-
|
|
336
|
+
if (!json) {
|
|
337
|
+
console.log(`
|
|
196
338
|
${header(" Pushing to Notion...")}`);
|
|
197
|
-
|
|
198
|
-
|
|
339
|
+
if (options.dryRun) console.log(dimText(" (dry-run mode)"));
|
|
340
|
+
console.log("");
|
|
341
|
+
}
|
|
199
342
|
const result = await orchestrator.push({
|
|
200
343
|
dryRun: options.dryRun,
|
|
201
344
|
paths: options.path,
|
|
202
|
-
onProgress: (current, total, item) => {
|
|
345
|
+
onProgress: json ? void 0 : (current, total, item) => {
|
|
203
346
|
const icon = operationIcon(item.operation);
|
|
204
347
|
const label = operationLabel(item.operation);
|
|
205
348
|
const prog = progress(current, total);
|
|
206
349
|
console.log(` ${icon} ${item.path} ${prog} ${label}`);
|
|
207
350
|
}
|
|
208
351
|
});
|
|
352
|
+
if (json) {
|
|
353
|
+
printJson(pushJson(result));
|
|
354
|
+
if (result.failed.length > 0) process.exitCode = 1;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
209
357
|
const failedCount = result.failed.length;
|
|
210
358
|
console.log(`
|
|
211
359
|
${completionHeader("Push", failedCount)}`);
|
|
212
360
|
console.log(
|
|
213
|
-
` ${summary(result.created, result.updated, result.deleted)} ${failedCountText(failedCount)}`
|
|
361
|
+
` ${summary(result.created, result.updated, result.deleted, result.moved)} ${failedCountText(failedCount)}`
|
|
214
362
|
);
|
|
363
|
+
if (result.createdDatabases) {
|
|
364
|
+
console.log(` ${options.dryRun ? "\uB9CC\uB4E4 DB" : "\uB9CC\uB4E0 DB"} ${result.createdDatabases}`);
|
|
365
|
+
}
|
|
215
366
|
console.log(` ${duration(result.duration)}`);
|
|
216
367
|
if (failedCount > 0) {
|
|
217
368
|
console.log("");
|
|
@@ -226,50 +377,62 @@ ${header(" Pushing to Notion...")}`);
|
|
|
226
377
|
});
|
|
227
378
|
|
|
228
379
|
// src/commands/pull.ts
|
|
229
|
-
import { Command as Command3 } from "commander";
|
|
230
|
-
import {
|
|
231
|
-
ConfigManager as ConfigManager3,
|
|
232
|
-
StateDB as StateDB2,
|
|
233
|
-
NotionClient as NotionClient3,
|
|
234
|
-
SyncOrchestrator as SyncOrchestrator2,
|
|
235
|
-
NodeVaultFS as NodeVaultFS2
|
|
236
|
-
} from "@im-nobsidian/core";
|
|
380
|
+
import { Command as Command3, Option } from "commander";
|
|
381
|
+
import { ConfigManager as ConfigManager4, StateDB as StateDB3, NotionClient as NotionClient3, SyncOrchestrator as SyncOrchestrator2 } from "@im-nobsidian/core";
|
|
237
382
|
import chalk3 from "chalk";
|
|
238
383
|
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
384
|
"--force",
|
|
240
385
|
"\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
|
-
).
|
|
386
|
+
).addOption(
|
|
387
|
+
new Option(
|
|
388
|
+
"--rerender",
|
|
389
|
+
"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)"
|
|
390
|
+
).conflicts("dryRun")
|
|
391
|
+
).option("--json", "\uACB0\uACFC\uB97C JSON \uD55C \uC904\uB85C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9 \u2014 \uB85C\uADF8\uB294 stderr)").action(async (options) => {
|
|
392
|
+
const json = options.json === true;
|
|
393
|
+
if (json) enableJsonMode();
|
|
242
394
|
const cwd = process.cwd();
|
|
243
|
-
const configManager = new
|
|
395
|
+
const configManager = new ConfigManager4(cwd);
|
|
244
396
|
const config = await configManager.load();
|
|
245
|
-
const stateDb =
|
|
397
|
+
const stateDb = StateDB3.open(configManager.dbPath);
|
|
246
398
|
try {
|
|
247
399
|
const client = NotionClient3.fromConfig(config);
|
|
248
|
-
const vaultFs =
|
|
400
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
249
401
|
const orchestrator = new SyncOrchestrator2(config, stateDb, client, vaultFs);
|
|
250
|
-
|
|
402
|
+
if (!json) {
|
|
403
|
+
console.log(`
|
|
251
404
|
${header(" Pulling from Notion...")}`);
|
|
252
|
-
|
|
253
|
-
|
|
405
|
+
if (options.dryRun) console.log(dimText(" (dry-run mode)"));
|
|
406
|
+
console.log("");
|
|
407
|
+
}
|
|
254
408
|
const result = await orchestrator.pull({
|
|
255
409
|
dryRun: options.dryRun,
|
|
256
410
|
paths: options.path,
|
|
257
411
|
force: options.force,
|
|
258
|
-
|
|
412
|
+
rerender: options.rerender,
|
|
413
|
+
onProgress: json ? void 0 : (current, total, item) => {
|
|
414
|
+
if (item.unchanged) return;
|
|
259
415
|
const icon = operationIcon(item.operation);
|
|
260
416
|
const label = operationLabel(item.operation);
|
|
261
417
|
const prog = progress(current, total);
|
|
262
418
|
console.log(` ${icon} ${item.path} ${prog} ${label}`);
|
|
263
419
|
}
|
|
264
420
|
});
|
|
421
|
+
if (json) {
|
|
422
|
+
printJson(pullJson(result));
|
|
423
|
+
if (result.failed.length > 0) process.exitCode = 1;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
265
426
|
if (result.imageCount > 0) {
|
|
266
427
|
console.log(
|
|
267
428
|
`
|
|
268
|
-
${icons.success} ${result.imageCount} images \u2192 ${dimText(
|
|
429
|
+
${icons.success} ${result.imageCount} images \u2192 ${dimText(`${config.paths.attachments}/`)}`
|
|
269
430
|
);
|
|
270
431
|
}
|
|
271
432
|
if (result.fileCount > 0) {
|
|
272
|
-
console.log(
|
|
433
|
+
console.log(
|
|
434
|
+
` ${icons.success} ${result.fileCount} files \u2192 ${dimText(`${config.paths.attachments}/`)}`
|
|
435
|
+
);
|
|
273
436
|
}
|
|
274
437
|
if (result.linkCount > 0) {
|
|
275
438
|
console.log(` ${icons.success} ${result.linkCount} links resolved`);
|
|
@@ -284,6 +447,9 @@ ${header(" Pulling from Notion...")}`);
|
|
|
284
447
|
console.log(` ${icons.success} ${chalk3.yellow(`${result.restored} restored`)}`);
|
|
285
448
|
}
|
|
286
449
|
console.log(` ${duration(result.duration)}`);
|
|
450
|
+
if (result.remoteScan) {
|
|
451
|
+
for (const line of remoteScanLines(result.remoteScan)) console.log(` ${line}`);
|
|
452
|
+
}
|
|
287
453
|
if (result.conflicts.length > 0) {
|
|
288
454
|
console.log(
|
|
289
455
|
`
|
|
@@ -304,54 +470,59 @@ ${header(" Pulling from Notion...")}`);
|
|
|
304
470
|
|
|
305
471
|
// src/commands/sync.ts
|
|
306
472
|
import { Command as Command4 } from "commander";
|
|
307
|
-
import {
|
|
308
|
-
ConfigManager as ConfigManager4,
|
|
309
|
-
StateDB as StateDB3,
|
|
310
|
-
NotionClient as NotionClient4,
|
|
311
|
-
SyncOrchestrator as SyncOrchestrator3,
|
|
312
|
-
NodeVaultFS as NodeVaultFS3
|
|
313
|
-
} from "@im-nobsidian/core";
|
|
473
|
+
import { ConfigManager as ConfigManager5, StateDB as StateDB4, NotionClient as NotionClient4, SyncOrchestrator as SyncOrchestrator3 } from "@im-nobsidian/core";
|
|
314
474
|
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) => {
|
|
475
|
+
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) => {
|
|
476
|
+
const json = options.json === true;
|
|
477
|
+
if (json) enableJsonMode();
|
|
478
|
+
const log = json ? () => {
|
|
479
|
+
} : (line) => console.log(line);
|
|
316
480
|
const cwd = process.cwd();
|
|
317
|
-
const configManager = new
|
|
481
|
+
const configManager = new ConfigManager5(cwd);
|
|
318
482
|
const config = await configManager.load();
|
|
319
|
-
const stateDb =
|
|
483
|
+
const stateDb = StateDB4.open(configManager.dbPath);
|
|
320
484
|
try {
|
|
321
485
|
const client = NotionClient4.fromConfig(config);
|
|
322
|
-
const vaultFs =
|
|
486
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
323
487
|
const orchestrator = new SyncOrchestrator3(config, stateDb, client, vaultFs);
|
|
324
|
-
|
|
488
|
+
log(`
|
|
325
489
|
${header(" Bidirectional Sync")}`);
|
|
326
|
-
|
|
327
|
-
if (options.dryRun)
|
|
328
|
-
|
|
490
|
+
log(` ${separator()}`);
|
|
491
|
+
if (options.dryRun) log(dimText(" (dry-run mode)"));
|
|
492
|
+
log(`
|
|
329
493
|
${header(chalk4.blue("\u25BC Pull"))} ${dimText("(Notion \u2192 Obsidian)")}`);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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(`
|
|
494
|
+
let pushShown = false;
|
|
495
|
+
const showPush = () => {
|
|
496
|
+
if (pushShown) return;
|
|
497
|
+
pushShown = true;
|
|
498
|
+
log(`
|
|
345
499
|
${header(chalk4.magenta("\u25B2 Push"))} ${dimText("(Obsidian \u2192 Notion)")}`);
|
|
346
|
-
|
|
500
|
+
};
|
|
501
|
+
const result = await orchestrator.sync({
|
|
347
502
|
dryRun: options.dryRun,
|
|
348
503
|
onProgress: (_current, _total, item) => {
|
|
504
|
+
if (item.direction === "push") showPush();
|
|
505
|
+
if (item.unchanged) return;
|
|
349
506
|
const icon = operationIcon(item.operation);
|
|
350
507
|
const label = operationLabel(item.operation);
|
|
351
|
-
|
|
508
|
+
log(` ${icon} ${item.path} ${label}`);
|
|
352
509
|
}
|
|
353
510
|
});
|
|
354
|
-
|
|
511
|
+
showPush();
|
|
512
|
+
const { pull: pullResult, push: pushResult } = result;
|
|
513
|
+
if (json) {
|
|
514
|
+
const pull = pullJson(pullResult);
|
|
515
|
+
const push = pushJson(pushResult);
|
|
516
|
+
printJson({
|
|
517
|
+
pull,
|
|
518
|
+
push,
|
|
519
|
+
churn: pull.churn + push.churn,
|
|
520
|
+
conflicts: pull.conflicts,
|
|
521
|
+
durationMs: result.duration
|
|
522
|
+
});
|
|
523
|
+
if (pullResult.failed.length + pushResult.failed.length > 0) process.exitCode = 1;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
355
526
|
const pullFailed = pullResult.failed.length;
|
|
356
527
|
const pushFailed = pushResult.failed.length;
|
|
357
528
|
console.log(`
|
|
@@ -360,20 +531,29 @@ ${header(" Bidirectional Sync")}`);
|
|
|
360
531
|
` ${chalk4.blue("Pull:")} ${summary(pullResult.created, pullResult.updated, pullResult.deleted)}` + // 사라졌던 파일이 되살아난 경우에만 덧붙인다 — 평상시 요약을 어지럽히지 않는다.
|
|
361
532
|
(pullResult.restored > 0 ? ` ${chalk4.yellow(`${pullResult.restored} restored`)}` : "") + (pullFailed > 0 ? ` ${chalk4.red(`${pullFailed} failed`)}` : "")
|
|
362
533
|
);
|
|
534
|
+
if (pullResult.imageCount > 0) {
|
|
535
|
+
console.log(` ${icons.success} ${pullResult.imageCount} images downloaded`);
|
|
536
|
+
}
|
|
537
|
+
if (pullResult.linkCount > 0) {
|
|
538
|
+
console.log(` ${icons.success} ${pullResult.linkCount} links resolved`);
|
|
539
|
+
}
|
|
363
540
|
console.log(
|
|
364
|
-
` ${chalk4.magenta("Push:")} ${summary(pushResult.created, pushResult.updated, pushResult.deleted)}` + (pushFailed > 0 ? ` ${chalk4.red(`${pushFailed} failed`)}` : "")
|
|
541
|
+
` ${chalk4.magenta("Push:")} ${summary(pushResult.created, pushResult.updated, pushResult.deleted, pushResult.moved)}` + (pushFailed > 0 ? ` ${chalk4.red(`${pushFailed} failed`)}` : "")
|
|
365
542
|
);
|
|
366
|
-
console.log(` ${duration(
|
|
367
|
-
if (pullResult.
|
|
543
|
+
console.log(` ${duration(result.duration)}`);
|
|
544
|
+
if (pullResult.remoteScan) {
|
|
545
|
+
for (const line of remoteScanLines(pullResult.remoteScan)) console.log(` ${line}`);
|
|
546
|
+
}
|
|
547
|
+
if (result.conflicts.length > 0) {
|
|
368
548
|
console.log(
|
|
369
549
|
`
|
|
370
|
-
${icons.conflict} ${chalk4.magenta(`${
|
|
550
|
+
${icons.conflict} ${chalk4.magenta(`${result.conflicts.length} conflicts`)} \u2014 run ${chalk4.cyan("nobsi resolve")}`
|
|
371
551
|
);
|
|
372
552
|
}
|
|
373
553
|
if (pullFailed + pushFailed > 0) {
|
|
374
554
|
console.log("");
|
|
375
|
-
for (const f of pullResult.failed) failedItem(
|
|
376
|
-
for (const f of pushResult.failed) failedItem(
|
|
555
|
+
for (const f of pullResult.failed) failedItem(f.path, f.error, "\u25BC");
|
|
556
|
+
for (const f of pushResult.failed) failedItem(f.path, f.error, "\u25B2");
|
|
377
557
|
process.exitCode = 1;
|
|
378
558
|
}
|
|
379
559
|
} finally {
|
|
@@ -384,21 +564,21 @@ ${header(" Bidirectional Sync")}`);
|
|
|
384
564
|
// src/commands/status.ts
|
|
385
565
|
import { Command as Command5 } from "commander";
|
|
386
566
|
import {
|
|
387
|
-
ConfigManager as
|
|
388
|
-
StateDB as
|
|
567
|
+
ConfigManager as ConfigManager6,
|
|
568
|
+
StateDB as StateDB5,
|
|
389
569
|
NotionClient as NotionClient5,
|
|
390
570
|
SyncOrchestrator as SyncOrchestrator4,
|
|
391
|
-
|
|
571
|
+
isRemoteDeletion
|
|
392
572
|
} from "@im-nobsidian/core";
|
|
393
573
|
import chalk5 from "chalk";
|
|
394
574
|
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) => {
|
|
395
575
|
const cwd = process.cwd();
|
|
396
|
-
const configManager = new
|
|
576
|
+
const configManager = new ConfigManager6(cwd);
|
|
397
577
|
const config = await configManager.load();
|
|
398
|
-
const stateDb =
|
|
578
|
+
const stateDb = StateDB5.open(configManager.dbPath);
|
|
399
579
|
try {
|
|
400
580
|
const client = NotionClient5.fromConfig(config);
|
|
401
|
-
const vaultFs =
|
|
581
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
402
582
|
const orchestrator = new SyncOrchestrator4(config, stateDb, client, vaultFs);
|
|
403
583
|
const status = opts.full ? await orchestrator.status() : await orchestrator.statusLocal();
|
|
404
584
|
console.log(`
|
|
@@ -409,21 +589,27 @@ ${header(" Sync Status")}`);
|
|
|
409
589
|
console.log(` ${dimText("Root page:")} ${rootId.slice(0, 8)}...`);
|
|
410
590
|
console.log(` ${dimText("Direction:")} ${direction}`);
|
|
411
591
|
if (status.lastSyncAt) {
|
|
412
|
-
|
|
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}`);
|
|
592
|
+
console.log(` ${dimText("Last sync:")} ${localDateTime(status.lastSyncAt)}`);
|
|
415
593
|
} else {
|
|
416
594
|
console.log(` ${dimText("Last sync:")} ${chalk5.yellow("never")}`);
|
|
417
595
|
}
|
|
596
|
+
console.log(` ${dimText("Full scan:")} ${lastFullScanText(status.lastFullScanAt)}`);
|
|
597
|
+
for (const folder of await nestedConnections(cwd)) {
|
|
598
|
+
console.log(
|
|
599
|
+
` ${dimText("Connection:")} ${folder}/ ${dimText(`(separate \u2014 cd ${folder} && nobsi status)`)}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
418
602
|
const allRecords = stateDb.getAll();
|
|
419
603
|
const total = allRecords.length;
|
|
420
604
|
const conflictCount = status.conflictRecords.length;
|
|
421
605
|
const createdChanges = status.localChanges.filter((c) => c.type === "created");
|
|
422
606
|
const modifiedChanges = status.localChanges.filter((c) => c.type === "modified");
|
|
423
607
|
const deletedChanges = status.localChanges.filter((c) => c.type === "deleted");
|
|
608
|
+
const movedChanges = status.localChanges.filter((c) => c.type === "moved");
|
|
424
609
|
const dirtyPaths = /* @__PURE__ */ new Set([
|
|
425
610
|
...modifiedChanges.map((c) => c.path),
|
|
426
611
|
...deletedChanges.map((c) => c.path),
|
|
612
|
+
...movedChanges.flatMap((c) => [c.path, c.movedFrom ?? c.path]),
|
|
427
613
|
...status.conflictRecords.map((r) => r.obsidianPath)
|
|
428
614
|
]);
|
|
429
615
|
const synced = allRecords.filter(
|
|
@@ -432,12 +618,17 @@ ${header(" Sync Status")}`);
|
|
|
432
618
|
const remoteCreated = status.remoteChanges.filter((c) => c.type === "created");
|
|
433
619
|
const remoteModified = status.remoteChanges.filter((c) => c.type === "modified");
|
|
434
620
|
const remoteDeleted = status.remoteChanges.filter((c) => c.type === "deleted");
|
|
621
|
+
const remoteMoved = status.remoteChanges.filter((c) => c.type === "moved");
|
|
622
|
+
const { folderMoves } = status;
|
|
435
623
|
console.log(`
|
|
436
624
|
${header(`Tracked files: ${total}`)}`);
|
|
437
625
|
console.log(` ${icons.synced} ${chalk5.green("synced")} ${synced}`);
|
|
438
626
|
if (modifiedChanges.length > 0) {
|
|
439
627
|
console.log(` ${icons.modified} ${chalk5.yellow("modified")} ${modifiedChanges.length}`);
|
|
440
628
|
}
|
|
629
|
+
if (movedChanges.length > 0) {
|
|
630
|
+
console.log(` ${icons.move} ${chalk5.cyan("moved")} ${movedChanges.length}`);
|
|
631
|
+
}
|
|
441
632
|
if (deletedChanges.length > 0) {
|
|
442
633
|
console.log(` ${chalk5.red("-")} ${chalk5.red("deleted")} ${deletedChanges.length}`);
|
|
443
634
|
}
|
|
@@ -461,6 +652,29 @@ ${header(" Sync Status")}`);
|
|
|
461
652
|
console.log(dimText(` ... and ${modifiedChanges.length - 10} more`));
|
|
462
653
|
}
|
|
463
654
|
}
|
|
655
|
+
if (folderMoves.length + movedChanges.length > 0) {
|
|
656
|
+
console.log(`
|
|
657
|
+
${header("Moved:")}`);
|
|
658
|
+
for (const move of folderMoves) {
|
|
659
|
+
console.log(` ${icons.move} ${move.from}/ \u2192 ${move.to}/ ${dimText("(folder)")}`);
|
|
660
|
+
}
|
|
661
|
+
for (const c of movedChanges.slice(0, 10)) {
|
|
662
|
+
console.log(` ${icons.move} ${c.movedFrom ?? "?"} \u2192 ${c.path}`);
|
|
663
|
+
}
|
|
664
|
+
if (movedChanges.length > 10) {
|
|
665
|
+
console.log(dimText(` ... and ${movedChanges.length - 10} more`));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (deletedChanges.length > 0) {
|
|
669
|
+
console.log(`
|
|
670
|
+
${header("Deleted files:")}`);
|
|
671
|
+
for (const c of deletedChanges.slice(0, 10)) {
|
|
672
|
+
console.log(` ${chalk5.red("-")} ${c.path}`);
|
|
673
|
+
}
|
|
674
|
+
if (deletedChanges.length > 10) {
|
|
675
|
+
console.log(dimText(` ... and ${deletedChanges.length - 10} more`));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
464
678
|
if (createdChanges.length > 0) {
|
|
465
679
|
console.log(`
|
|
466
680
|
${header("New files:")}`);
|
|
@@ -482,12 +696,18 @@ ${header(" Sync Status")}`);
|
|
|
482
696
|
` ${chalk5.yellow("~")} ${chalk5.yellow("modified")} ${remoteModified.length}`
|
|
483
697
|
);
|
|
484
698
|
}
|
|
699
|
+
if (remoteMoved.length > 0) {
|
|
700
|
+
console.log(` ${icons.move} ${chalk5.cyan("moved")} ${remoteMoved.length}`);
|
|
701
|
+
}
|
|
485
702
|
if (remoteDeleted.length > 0) {
|
|
486
703
|
console.log(` ${chalk5.red("-")} ${chalk5.red("deleted")} ${remoteDeleted.length}`);
|
|
487
704
|
}
|
|
488
705
|
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
|
-
|
|
706
|
+
const icon = c.type === "created" ? chalk5.cyan("+") : c.type === "modified" ? chalk5.yellow("~") : c.type === "moved" ? icons.move : chalk5.red("-");
|
|
707
|
+
const name = c.path ?? (c.title ? `${c.title} ${dimText("(new page)")}` : void 0);
|
|
708
|
+
console.log(
|
|
709
|
+
` ${icon} ${name ?? `${c.pageId.slice(0, 8)}...`} ${dimText(`(${c.type})`)}`
|
|
710
|
+
);
|
|
491
711
|
}
|
|
492
712
|
if (status.remoteChanges.length > 10) {
|
|
493
713
|
console.log(dimText(` ... and ${status.remoteChanges.length - 10} more`));
|
|
@@ -496,13 +716,15 @@ ${header(" Sync Status")}`);
|
|
|
496
716
|
if (conflictCount > 0) {
|
|
497
717
|
console.log(`
|
|
498
718
|
${header(chalk5.magenta("Conflicts:"))}`);
|
|
719
|
+
const remoteDeleted2 = new Set(
|
|
720
|
+
status.conflicts.filter(isRemoteDeletion).map((c) => c.syncRecord.obsidianPath)
|
|
721
|
+
);
|
|
499
722
|
for (const r of status.conflictRecords) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
);
|
|
723
|
+
const reason = remoteDeleted2.has(r.obsidianPath) ? "(deleted in Notion, local edits not pushed)" : "(both sides changed)";
|
|
724
|
+
console.log(` ${icons.conflict} ${r.obsidianPath.padEnd(30)} ${dimText(reason)}`);
|
|
503
725
|
}
|
|
504
726
|
}
|
|
505
|
-
const hasChanges = status.localChanges.length > 0 || status.remoteChanges.length > 0 || conflictCount > 0;
|
|
727
|
+
const hasChanges = status.localChanges.length > 0 || folderMoves.length > 0 || status.remoteChanges.length > 0 || conflictCount > 0;
|
|
506
728
|
if (hasChanges) {
|
|
507
729
|
console.log("");
|
|
508
730
|
if (conflictCount > 0) {
|
|
@@ -513,6 +735,11 @@ ${header(" Sync Status")}`);
|
|
|
513
735
|
console.log(
|
|
514
736
|
` ${dimText("Run")} ${chalk5.cyan("nobsi sync")} ${dimText("to push/pull changes")}`
|
|
515
737
|
);
|
|
738
|
+
if (modifiedChanges.length + deletedChanges.length > 0) {
|
|
739
|
+
console.log(
|
|
740
|
+
` ${dimText("Run")} ${chalk5.cyan("nobsi discard <path>")} ${dimText("to discard local edits")}`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
516
743
|
} else {
|
|
517
744
|
console.log(`
|
|
518
745
|
${chalk5.green("Everything up to date")} \u2713`);
|
|
@@ -522,6 +749,9 @@ ${header(" Sync Status")}`);
|
|
|
522
749
|
`
|
|
523
750
|
${dimText("Run")} ${chalk5.cyan("nobsi status --full")} ${dimText("to check Notion remote changes")}`
|
|
524
751
|
);
|
|
752
|
+
} else if (status.remoteScan?.deletionsDeferred) {
|
|
753
|
+
console.log(`
|
|
754
|
+
${remoteDeletionHint(status.remoteScan)}`);
|
|
525
755
|
}
|
|
526
756
|
} finally {
|
|
527
757
|
stateDb.close();
|
|
@@ -531,35 +761,61 @@ ${header(" Sync Status")}`);
|
|
|
531
761
|
// src/commands/diff.ts
|
|
532
762
|
import { Command as Command6 } from "commander";
|
|
533
763
|
import {
|
|
534
|
-
ConfigManager as
|
|
535
|
-
StateDB as
|
|
764
|
+
ConfigManager as ConfigManager7,
|
|
765
|
+
StateDB as StateDB6,
|
|
536
766
|
NotionClient as NotionClient6,
|
|
537
767
|
SyncOrchestrator as SyncOrchestrator5,
|
|
538
|
-
|
|
539
|
-
|
|
768
|
+
formatUnifiedDiff,
|
|
769
|
+
lineDiff,
|
|
770
|
+
matchesPathScope as matchesPathScope2
|
|
540
771
|
} from "@im-nobsidian/core";
|
|
541
|
-
|
|
772
|
+
|
|
773
|
+
// src/utils/diff-output.ts
|
|
774
|
+
import { DIFF_SIGN } from "@im-nobsidian/core";
|
|
775
|
+
var BOLD = "\x1B[1m";
|
|
776
|
+
var RED = "\x1B[31m";
|
|
777
|
+
var GREEN = "\x1B[32m";
|
|
778
|
+
var CYAN = "\x1B[36m";
|
|
779
|
+
var RESET = "\x1B[0m";
|
|
780
|
+
var LINE_COLOR = {
|
|
781
|
+
[DIFF_SIGN.added]: GREEN,
|
|
782
|
+
[DIFF_SIGN.removed]: RED,
|
|
783
|
+
"@": CYAN
|
|
784
|
+
};
|
|
785
|
+
function printUnifiedDiff(lines, color) {
|
|
786
|
+
lines.forEach((line, index) => {
|
|
787
|
+
const code = index < 2 ? BOLD : LINE_COLOR[line.charAt(0)];
|
|
788
|
+
console.log(color && code ? `${code}${line}${RESET}` : line);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/commands/diff.ts
|
|
542
793
|
var diffCommand = new Command6("diff").description(
|
|
543
794
|
"\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
795
|
).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) => {
|
|
545
796
|
const cwd = process.cwd();
|
|
546
|
-
const configManager = new
|
|
797
|
+
const configManager = new ConfigManager7(cwd);
|
|
547
798
|
const config = await configManager.load();
|
|
548
|
-
const stateDb =
|
|
799
|
+
const stateDb = StateDB6.open(configManager.dbPath);
|
|
549
800
|
try {
|
|
550
801
|
const client = NotionClient6.fromConfig(config);
|
|
551
|
-
const vaultFs =
|
|
802
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
552
803
|
const orchestrator = new SyncOrchestrator5(config, stateDb, client, vaultFs);
|
|
553
804
|
const status = await orchestrator.statusLocal();
|
|
554
|
-
const changes = targetPath ? status.localChanges.filter((c) =>
|
|
805
|
+
const changes = targetPath ? status.localChanges.filter((c) => matchesPathScope2(c.path, targetPath)) : status.localChanges;
|
|
555
806
|
if (changes.length === 0) {
|
|
556
807
|
console.log("\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C");
|
|
557
808
|
return;
|
|
558
809
|
}
|
|
810
|
+
let failed = 0;
|
|
559
811
|
for (const change of changes) {
|
|
560
|
-
const record = stateDb.getByPath(change.path);
|
|
561
812
|
if (options.remote && change.type !== "deleted") {
|
|
562
|
-
|
|
813
|
+
let remotePath = change.path;
|
|
814
|
+
let remote = await orchestrator.renderRemoteSnapshot(remotePath);
|
|
815
|
+
if (remote === null && change.movedFrom) {
|
|
816
|
+
remotePath = change.movedFrom;
|
|
817
|
+
remote = await orchestrator.renderRemoteSnapshot(remotePath);
|
|
818
|
+
}
|
|
563
819
|
if (remote === null) {
|
|
564
820
|
console.log(`--- (\uC6D0\uACA9 \uC5C6\uC74C) ${change.path}`);
|
|
565
821
|
console.log(`+++ b/${change.path}`);
|
|
@@ -568,94 +824,89 @@ var diffCommand = new Command6("diff").description(
|
|
|
568
824
|
continue;
|
|
569
825
|
}
|
|
570
826
|
const currentContent = await vaultFs.readFile(change.path);
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
currentContent
|
|
576
|
-
);
|
|
577
|
-
printPatch(patch, options.color, `notion/${change.path}`, `b/${change.path}`);
|
|
827
|
+
const hunks = lineDiff(remote, currentContent);
|
|
828
|
+
const lines = formatUnifiedDiff(hunks, `notion/${remotePath}`, `b/${change.path}`);
|
|
829
|
+
if (hunks.length === 0) lines.push("(Notion \uACFC \uB0B4\uC6A9\uC774 \uAC19\uC2B5\uB2C8\uB2E4)");
|
|
830
|
+
printUnifiedDiff(lines, options.color);
|
|
578
831
|
console.log("");
|
|
579
832
|
continue;
|
|
580
833
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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}`);
|
|
834
|
+
let diff;
|
|
835
|
+
try {
|
|
836
|
+
diff = await orchestrator.localChangeDiff(change);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
failed++;
|
|
839
|
+
console.log(`--- a/${change.path}`);
|
|
840
|
+
console.log(`(${error instanceof Error ? error.message : String(error)})`);
|
|
841
|
+
console.log("");
|
|
842
|
+
continue;
|
|
605
843
|
}
|
|
844
|
+
printChangeDiff(diff, options.color);
|
|
606
845
|
console.log("");
|
|
607
846
|
}
|
|
847
|
+
if (failed > 0) process.exitCode = 1;
|
|
608
848
|
} finally {
|
|
609
849
|
stateDb.close();
|
|
610
850
|
}
|
|
611
851
|
});
|
|
612
|
-
function
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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);
|
|
852
|
+
function printChangeDiff(diff, color) {
|
|
853
|
+
const oldPath = diff.before === null ? "/dev/null" : `a/${diff.movedFrom ?? diff.path}`;
|
|
854
|
+
const newPath = diff.after === null ? "/dev/null" : `b/${diff.path}`;
|
|
855
|
+
if (diff.movedFrom) {
|
|
856
|
+
console.log(`rename from ${diff.movedFrom}`);
|
|
857
|
+
console.log(`rename to ${diff.path}`);
|
|
858
|
+
if (diff.before === diff.after) {
|
|
859
|
+
console.log("(\uB0B4\uC6A9\uC740 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4 \u2014 \uC790\uB9AC\uB9CC \uC62E\uACBC\uC2B5\uB2C8\uB2E4)");
|
|
860
|
+
return;
|
|
636
861
|
}
|
|
637
862
|
}
|
|
863
|
+
printUnifiedDiff(
|
|
864
|
+
formatUnifiedDiff(lineDiff(diff.before ?? "", diff.after ?? ""), oldPath, newPath),
|
|
865
|
+
color
|
|
866
|
+
);
|
|
638
867
|
}
|
|
639
868
|
|
|
640
|
-
// src/commands/
|
|
869
|
+
// src/commands/discard.ts
|
|
641
870
|
import { Command as Command7 } from "commander";
|
|
642
|
-
import {
|
|
643
|
-
|
|
644
|
-
StateDB as StateDB6,
|
|
645
|
-
NotionClient as NotionClient7,
|
|
646
|
-
SyncOrchestrator as SyncOrchestrator6,
|
|
647
|
-
NodeVaultFS as NodeVaultFS6
|
|
648
|
-
} 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 () => {
|
|
871
|
+
import { ConfigManager as ConfigManager8, StateDB as StateDB7, NotionClient as NotionClient7, SyncOrchestrator as SyncOrchestrator6 } from "@im-nobsidian/core";
|
|
872
|
+
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
873
|
const cwd = process.cwd();
|
|
652
|
-
const configManager = new
|
|
874
|
+
const configManager = new ConfigManager8(cwd);
|
|
653
875
|
const config = await configManager.load();
|
|
654
|
-
const stateDb =
|
|
876
|
+
const stateDb = StateDB7.open(configManager.dbPath);
|
|
655
877
|
try {
|
|
656
878
|
const client = NotionClient7.fromConfig(config);
|
|
657
|
-
const vaultFs =
|
|
879
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
658
880
|
const orchestrator = new SyncOrchestrator6(config, stateDb, client, vaultFs);
|
|
881
|
+
let failed = 0;
|
|
882
|
+
for (const path of paths) {
|
|
883
|
+
try {
|
|
884
|
+
await orchestrator.discardLocalChange(path);
|
|
885
|
+
console.log(` ${icons.success} ${path}`);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
failed++;
|
|
888
|
+
failedItem(path, error instanceof Error ? error.message : String(error));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (failed > 0) process.exitCode = 1;
|
|
892
|
+
} finally {
|
|
893
|
+
stateDb.close();
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// src/commands/fetch.ts
|
|
898
|
+
import { Command as Command8 } from "commander";
|
|
899
|
+
import { ConfigManager as ConfigManager9, StateDB as StateDB8, NotionClient as NotionClient8, SyncOrchestrator as SyncOrchestrator7 } from "@im-nobsidian/core";
|
|
900
|
+
import chalk6 from "chalk";
|
|
901
|
+
var fetchCommand = new Command8("fetch").description("Notion \uB9AC\uBAA8\uD2B8 \uC0C1\uD0DC \uC2A4\uCE94 (\uC0AD\uC81C \uAC10\uC9C0 \uD3EC\uD568)").action(async () => {
|
|
902
|
+
const cwd = process.cwd();
|
|
903
|
+
const configManager = new ConfigManager9(cwd);
|
|
904
|
+
const config = await configManager.load();
|
|
905
|
+
const stateDb = StateDB8.open(configManager.dbPath);
|
|
906
|
+
try {
|
|
907
|
+
const client = NotionClient8.fromConfig(config);
|
|
908
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
909
|
+
const orchestrator = new SyncOrchestrator7(config, stateDb, client, vaultFs);
|
|
659
910
|
console.log(dimText(" Scanning remote pages..."));
|
|
660
911
|
const result = await orchestrator.fetch();
|
|
661
912
|
console.log(`
|
|
@@ -687,34 +938,67 @@ ${header(" Remote Scan")}`);
|
|
|
687
938
|
});
|
|
688
939
|
|
|
689
940
|
// src/commands/resolve.ts
|
|
690
|
-
import { Command as
|
|
941
|
+
import { Command as Command9 } from "commander";
|
|
691
942
|
import { select, confirm as confirm2 } from "@inquirer/prompts";
|
|
692
943
|
import {
|
|
693
|
-
ConfigManager as
|
|
694
|
-
StateDB as
|
|
695
|
-
NotionClient as
|
|
696
|
-
SyncOrchestrator as
|
|
697
|
-
|
|
944
|
+
ConfigManager as ConfigManager10,
|
|
945
|
+
StateDB as StateDB9,
|
|
946
|
+
NotionClient as NotionClient9,
|
|
947
|
+
SyncOrchestrator as SyncOrchestrator8,
|
|
948
|
+
applicableChoices,
|
|
949
|
+
isRemoteDeletion as isRemoteDeletion2
|
|
698
950
|
} from "@im-nobsidian/core";
|
|
951
|
+
|
|
952
|
+
// src/utils/state-db-failure.ts
|
|
953
|
+
import { StateDbUnavailableError } from "@im-nobsidian/core";
|
|
954
|
+
function describeFailure(error) {
|
|
955
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
956
|
+
if (!(error instanceof StateDbUnavailableError)) return message;
|
|
957
|
+
return `${message} \u2014 ${error.guidance("\uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694")}`;
|
|
958
|
+
}
|
|
959
|
+
function reportStateDbFailure(error, proc) {
|
|
960
|
+
if (!(error instanceof StateDbUnavailableError)) throw error;
|
|
961
|
+
console.error(`\uC624\uB958: ${describeFailure(error)}`);
|
|
962
|
+
proc.exitCode = 1;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/commands/resolve.ts
|
|
699
966
|
var STRATEGIES = ["local-first", "remote-first", "duplicate"];
|
|
967
|
+
var CHOICE_NAMES = {
|
|
968
|
+
local: "\uB85C\uCEEC \uC720\uC9C0 (Obsidian \uBC84\uC804 \uC720\uC9C0)",
|
|
969
|
+
remote: "\uC6D0\uACA9 \uC720\uC9C0 (Notion \uBC84\uC804\uC73C\uB85C \uB36E\uC5B4\uC4F0\uAE30)",
|
|
970
|
+
merge: "\uC790\uB3D9 \uBCD1\uD569 (3-way merge)",
|
|
971
|
+
duplicate: "\uBCF5\uC81C (\uC6D0\uBCF8 \uC720\uC9C0 + \uC62C\uB9AC\uC9C0 \uC54A\uB294 .conflict \uC0AC\uBCF8 \uC0DD\uC131)"
|
|
972
|
+
};
|
|
973
|
+
var REMOTE_DELETION_CHOICE_NAMES = {
|
|
974
|
+
local: "\uB85C\uCEEC \uC720\uC9C0 (Notion \uC5D0 \uC0C8 \uD398\uC774\uC9C0\uB85C \uB2E4\uC2DC \uB9CC\uB4E4\uAE30)",
|
|
975
|
+
remote: "\uC0AD\uC81C \uB530\uB974\uAE30 (Obsidian \uD30C\uC77C\uB3C4 \uC9C0\uC6B0\uAE30)"
|
|
976
|
+
};
|
|
977
|
+
var PULL_ONLY_REMOTE_DELETION_CHOICE_NAMES = {
|
|
978
|
+
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)"
|
|
979
|
+
};
|
|
980
|
+
function choiceName(conflict, choice, uploads) {
|
|
981
|
+
const deleted = isRemoteDeletion2(conflict);
|
|
982
|
+
return (deleted && !uploads ? PULL_ONLY_REMOTE_DELETION_CHOICE_NAMES[choice] : void 0) ?? (deleted ? REMOTE_DELETION_CHOICE_NAMES[choice] : void 0) ?? CHOICE_NAMES[choice] ?? choice;
|
|
983
|
+
}
|
|
700
984
|
function isInteractive() {
|
|
701
985
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
702
986
|
}
|
|
703
|
-
var resolveCommand = new
|
|
987
|
+
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
988
|
if (options.strategy && !STRATEGIES.includes(options.strategy)) {
|
|
705
989
|
console.error(`\uC54C \uC218 \uC5C6\uB294 \uC804\uB7B5 '${options.strategy}' \u2014 \uC0AC\uC6A9 \uAC00\uB2A5: ${STRATEGIES.join(", ")}`);
|
|
706
990
|
process.exitCode = 1;
|
|
707
991
|
return;
|
|
708
992
|
}
|
|
709
993
|
const cwd = process.cwd();
|
|
710
|
-
const configManager = new
|
|
994
|
+
const configManager = new ConfigManager10(cwd);
|
|
711
995
|
let stateDb;
|
|
712
996
|
try {
|
|
713
997
|
const config = await configManager.load();
|
|
714
|
-
stateDb =
|
|
715
|
-
const client =
|
|
716
|
-
const vaultFs =
|
|
717
|
-
const orchestrator = new
|
|
998
|
+
stateDb = StateDB9.open(configManager.dbPath);
|
|
999
|
+
const client = NotionClient9.fromConfig(config);
|
|
1000
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
1001
|
+
const orchestrator = new SyncOrchestrator8(config, stateDb, client, vaultFs);
|
|
718
1002
|
const conflicts = await orchestrator.listConflicts();
|
|
719
1003
|
if (conflicts.length === 0) {
|
|
720
1004
|
console.log("\uCDA9\uB3CC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
@@ -733,8 +1017,20 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
|
|
|
733
1017
|
console.log(`
|
|
734
1018
|
\uCDA9\uB3CC ${pending.length}\uAC74 \uBC1C\uACAC
|
|
735
1019
|
`);
|
|
1020
|
+
const uploads = config.sync.direction !== "pull";
|
|
1021
|
+
if (!uploads) {
|
|
1022
|
+
console.log(
|
|
1023
|
+
" \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"
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
736
1026
|
if (options.strategy) {
|
|
737
1027
|
const strategy = options.strategy;
|
|
1028
|
+
const deletions = pending.filter(isRemoteDeletion2).length;
|
|
1029
|
+
if (deletions > 0) {
|
|
1030
|
+
console.log(
|
|
1031
|
+
` 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"}`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
738
1034
|
if (!options.yes) {
|
|
739
1035
|
if (!isInteractive()) {
|
|
740
1036
|
console.error(
|
|
@@ -750,17 +1046,20 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
|
|
|
750
1046
|
}
|
|
751
1047
|
const results = await orchestrator.resolveAllConflicts(pending, strategy);
|
|
752
1048
|
let failed2 = 0;
|
|
753
|
-
for (const result of results) {
|
|
1049
|
+
for (const [index, result] of results.entries()) {
|
|
754
1050
|
if (!result.success) failed2++;
|
|
755
1051
|
const icon = result.success ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
756
|
-
|
|
1052
|
+
const reason = result.error ? ` \u2014 ${result.error}` : "";
|
|
1053
|
+
console.log(
|
|
1054
|
+
` ${icon} ${result.path} \u2192 ${choiceName(pending[index], result.choice, uploads)}${reason}`
|
|
1055
|
+
);
|
|
757
1056
|
}
|
|
758
1057
|
report(results.length - failed2, failed2);
|
|
759
1058
|
return;
|
|
760
1059
|
}
|
|
761
1060
|
if (!isInteractive()) {
|
|
762
1061
|
console.error(
|
|
763
|
-
|
|
1062
|
+
`\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
1063
|
);
|
|
765
1064
|
process.exitCode = 1;
|
|
766
1065
|
return;
|
|
@@ -768,12 +1067,12 @@ ${cleared.size}\uAC74 \uBAA8\uB450 \uC774\uBBF8 \uD574\uC18C\uB41C \uC0C1\uD0DC\
|
|
|
768
1067
|
let resolved = 0;
|
|
769
1068
|
let failed = 0;
|
|
770
1069
|
for (const conflict of pending) {
|
|
771
|
-
if (await resolveInteractive(conflict, orchestrator)) resolved++;
|
|
1070
|
+
if (await resolveInteractive(conflict, orchestrator, uploads)) resolved++;
|
|
772
1071
|
else failed++;
|
|
773
1072
|
}
|
|
774
1073
|
report(resolved, failed);
|
|
775
1074
|
} catch (error) {
|
|
776
|
-
console.error("\uC624\uB958:",
|
|
1075
|
+
console.error("\uC624\uB958:", describeFailure(error));
|
|
777
1076
|
process.exitCode = 1;
|
|
778
1077
|
} finally {
|
|
779
1078
|
stateDb?.close();
|
|
@@ -789,37 +1088,29 @@ function report(resolved, failed) {
|
|
|
789
1088
|
\x1B[33m\uCDA9\uB3CC ${resolved}\uAC74 \uD574\uACB0 \xB7 ${failed}\uAC74 \uBBF8\uD574\uACB0\x1B[0m`);
|
|
790
1089
|
process.exitCode = 1;
|
|
791
1090
|
}
|
|
792
|
-
async function resolveInteractive(conflict, orchestrator) {
|
|
1091
|
+
async function resolveInteractive(conflict, orchestrator, uploads) {
|
|
793
1092
|
const path = conflict.syncRecord.obsidianPath;
|
|
794
1093
|
console.log(`
|
|
795
1094
|
${"\u2500".repeat(60)}`);
|
|
796
1095
|
console.log(`\uD30C\uC77C: \x1B[1m${path}\x1B[0m`);
|
|
797
1096
|
console.log(`${"\u2500".repeat(60)}`);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
}
|
|
1097
|
+
if (isRemoteDeletion2(conflict)) {
|
|
1098
|
+
console.log(
|
|
1099
|
+
"\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"
|
|
1100
|
+
);
|
|
1101
|
+
} else {
|
|
1102
|
+
printUnifiedDiff(orchestrator.generateConflictDiff(conflict).split("\n"), true);
|
|
810
1103
|
}
|
|
811
1104
|
const choice = await select({
|
|
812
1105
|
message: "\uC5B4\uB5BB\uAC8C \uD574\uACB0\uD560\uAE4C\uC694?",
|
|
813
|
-
choices:
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
{ name: "\uBCF5\uC81C (\uC6D0\uBCF8 \uC720\uC9C0 + .conflict \uD30C\uC77C \uC0DD\uC131)", value: "duplicate" }
|
|
818
|
-
]
|
|
1106
|
+
choices: applicableChoices(conflict).map((value) => ({
|
|
1107
|
+
name: choiceName(conflict, value, uploads),
|
|
1108
|
+
value
|
|
1109
|
+
}))
|
|
819
1110
|
});
|
|
820
1111
|
const result = await orchestrator.resolveConflict(conflict, choice);
|
|
821
1112
|
if (result.success) {
|
|
822
|
-
console.log(`\x1B[32m\u2713 ${path} \u2192 ${choice}\
|
|
1113
|
+
console.log(`\x1B[32m\u2713 ${path} \u2192 ${choiceName(conflict, choice, uploads)}\x1B[0m`);
|
|
823
1114
|
return true;
|
|
824
1115
|
}
|
|
825
1116
|
if (result.mergeHadConflicts) {
|
|
@@ -831,18 +1122,19 @@ ${"\u2500".repeat(60)}`);
|
|
|
831
1122
|
}
|
|
832
1123
|
|
|
833
1124
|
// src/commands/watch.ts
|
|
834
|
-
import { Command as
|
|
1125
|
+
import { Command as Command10 } from "commander";
|
|
835
1126
|
import {
|
|
836
|
-
ConfigManager as
|
|
837
|
-
StateDB as
|
|
838
|
-
NotionClient as
|
|
839
|
-
SyncOrchestrator as
|
|
840
|
-
|
|
841
|
-
|
|
1127
|
+
ConfigManager as ConfigManager11,
|
|
1128
|
+
StateDB as StateDB10,
|
|
1129
|
+
NotionClient as NotionClient10,
|
|
1130
|
+
SyncOrchestrator as SyncOrchestrator9,
|
|
1131
|
+
WatchSyncService,
|
|
1132
|
+
DEFAULT_WATCH_DEBOUNCE_MS
|
|
842
1133
|
} from "@im-nobsidian/core";
|
|
843
1134
|
function formatResult(result) {
|
|
844
1135
|
const pull = `Pull: +${result.pull.created} ~${result.pull.updated} -${result.pull.deleted}`;
|
|
845
|
-
const
|
|
1136
|
+
const moved = result.push.moved > 0 ? ` \u2192${result.push.moved}` : "";
|
|
1137
|
+
const push = `Push: +${result.push.created} ~${result.push.updated} -${result.push.deleted}${moved}`;
|
|
846
1138
|
const parts = [pull, push];
|
|
847
1139
|
if (result.conflicts.length > 0) {
|
|
848
1140
|
parts.push(`\uCDA9\uB3CC ${result.conflicts.length}\uAC74`);
|
|
@@ -852,35 +1144,59 @@ function formatResult(result) {
|
|
|
852
1144
|
function timestamp() {
|
|
853
1145
|
return (/* @__PURE__ */ new Date()).toLocaleTimeString("ko-KR", { hour12: false });
|
|
854
1146
|
}
|
|
855
|
-
|
|
1147
|
+
function syncLabel(scope) {
|
|
1148
|
+
return scope === "full" ? "\uC8FC\uAE30\uC801 \uB3D9\uAE30\uD654" : "\uB3D9\uAE30\uD654";
|
|
1149
|
+
}
|
|
1150
|
+
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) => {
|
|
1151
|
+
const debounceMs = Number(options.debounce);
|
|
1152
|
+
const intervalSec = Number(options.interval);
|
|
1153
|
+
if (!Number.isInteger(debounceMs) || debounceMs < 0) {
|
|
1154
|
+
console.error(`--debounce \uB294 0 \uC774\uC0C1\uC758 \uC815\uC218(ms)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${options.debounce}`);
|
|
1155
|
+
process.exitCode = 1;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (!Number.isInteger(intervalSec) || intervalSec < 0) {
|
|
1159
|
+
console.error(`--interval \uC740 0 \uC774\uC0C1\uC758 \uC815\uC218(\uCD08)\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${options.interval}`);
|
|
1160
|
+
process.exitCode = 1;
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
856
1163
|
const cwd = process.cwd();
|
|
857
|
-
const configManager = new
|
|
1164
|
+
const configManager = new ConfigManager11(cwd);
|
|
858
1165
|
const config = await configManager.load();
|
|
859
|
-
const stateDb =
|
|
860
|
-
const client =
|
|
861
|
-
const vaultFs =
|
|
862
|
-
const orchestrator = new
|
|
863
|
-
const debounceMs = parseInt(options.debounce, 10);
|
|
864
|
-
const intervalSec = parseInt(options.interval, 10);
|
|
1166
|
+
const stateDb = StateDB10.open(configManager.dbPath);
|
|
1167
|
+
const client = NotionClient10.fromConfig(config);
|
|
1168
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
1169
|
+
const orchestrator = new SyncOrchestrator9(config, stateDb, client, vaultFs);
|
|
865
1170
|
const service = new WatchSyncService(cwd, orchestrator, {
|
|
866
1171
|
debounceMs,
|
|
867
1172
|
onFileChange: (event, path) => {
|
|
868
1173
|
console.log(`[${timestamp()}] ${event}: ${path}`);
|
|
869
1174
|
},
|
|
870
|
-
onSyncStart: () => {
|
|
871
|
-
console.log(`[${timestamp()}]
|
|
1175
|
+
onSyncStart: (scope) => {
|
|
1176
|
+
console.log(`[${timestamp()}] ${syncLabel(scope)} \uC2DC\uC791...`);
|
|
872
1177
|
},
|
|
873
|
-
onSyncComplete: (result) => {
|
|
874
|
-
console.log(`[${timestamp()}]
|
|
1178
|
+
onSyncComplete: (result, scope) => {
|
|
1179
|
+
console.log(`[${timestamp()}] ${syncLabel(scope)} \uC644\uB8CC \u2014 ${formatResult(result)}`);
|
|
875
1180
|
},
|
|
876
|
-
|
|
877
|
-
console.
|
|
1181
|
+
onSyncCancelled: (result, scope) => {
|
|
1182
|
+
console.log(
|
|
1183
|
+
`[${timestamp()}] ${syncLabel(scope)} \uCDE8\uC18C \u2014 \uBA48\uCD94\uAE30 \uC804\uAE4C\uC9C0 ${formatResult(result)}`
|
|
1184
|
+
);
|
|
1185
|
+
},
|
|
1186
|
+
onSyncError: (error, scope) => {
|
|
1187
|
+
console.error(`[${timestamp()}] ${syncLabel(scope)} \uC2E4\uD328: ${error.message}`);
|
|
878
1188
|
}
|
|
879
1189
|
});
|
|
880
1190
|
let pullTimer = null;
|
|
1191
|
+
let shuttingDown = false;
|
|
881
1192
|
const shutdown = async () => {
|
|
1193
|
+
if (shuttingDown) {
|
|
1194
|
+
console.log(`[${timestamp()}] \uAC15\uC81C \uC885\uB8CC`);
|
|
1195
|
+
return process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
shuttingDown = true;
|
|
882
1198
|
console.log(`
|
|
883
|
-
[${timestamp()}] \uAC10\uC2DC \uC885\uB8CC \uC911
|
|
1199
|
+
[${timestamp()}] \uAC10\uC2DC \uC885\uB8CC \uC911... (\uB3C4\uB294 \uB3D9\uAE30\uD654\uB97C \uBA48\uCD94\uACE0 \uAE30\uB2E4\uB9BD\uB2C8\uB2E4)`);
|
|
884
1200
|
if (pullTimer) clearInterval(pullTimer);
|
|
885
1201
|
await service.stop();
|
|
886
1202
|
stateDb.close();
|
|
@@ -893,31 +1209,15 @@ var watchCommand = new Command9("watch").description("\uD30C\uC77C \uBCC0\uACBD
|
|
|
893
1209
|
console.log(`[${timestamp()}] \uD30C\uC77C \uAC10\uC2DC \uC2DC\uC791 (debounce: ${debounceMs}ms)`);
|
|
894
1210
|
if (intervalSec > 0) {
|
|
895
1211
|
console.log(`[${timestamp()}] \uD480 \uB3D9\uAE30\uD654 \uC8FC\uAE30: ${intervalSec}\uCD08`);
|
|
896
|
-
pullTimer = setInterval(
|
|
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);
|
|
1212
|
+
pullTimer = setInterval(() => service.requestFullSync(), intervalSec * 1e3);
|
|
907
1213
|
}
|
|
908
1214
|
await new Promise(() => {
|
|
909
1215
|
});
|
|
910
1216
|
});
|
|
911
1217
|
|
|
912
1218
|
// src/commands/verify.ts
|
|
913
|
-
import { Command as
|
|
914
|
-
import {
|
|
915
|
-
ConfigManager as ConfigManager10,
|
|
916
|
-
StateDB as StateDB9,
|
|
917
|
-
NotionClient as NotionClient10,
|
|
918
|
-
SyncOrchestrator as SyncOrchestrator9,
|
|
919
|
-
NodeVaultFS as NodeVaultFS9
|
|
920
|
-
} from "@im-nobsidian/core";
|
|
1219
|
+
import { Command as Command11 } from "commander";
|
|
1220
|
+
import { ConfigManager as ConfigManager12, StateDB as StateDB11, NotionClient as NotionClient11, SyncOrchestrator as SyncOrchestrator10 } from "@im-nobsidian/core";
|
|
921
1221
|
import chalk7 from "chalk";
|
|
922
1222
|
var SAMPLE_LIMIT = 10;
|
|
923
1223
|
function printIdSample(label, ids, color) {
|
|
@@ -1006,15 +1306,15 @@ function printReport(report2) {
|
|
|
1006
1306
|
);
|
|
1007
1307
|
}
|
|
1008
1308
|
}
|
|
1009
|
-
var verifyCommand = new
|
|
1309
|
+
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
1310
|
const cwd = process.cwd();
|
|
1011
|
-
const configManager = new
|
|
1311
|
+
const configManager = new ConfigManager12(cwd);
|
|
1012
1312
|
const config = await configManager.load();
|
|
1013
|
-
const stateDb =
|
|
1313
|
+
const stateDb = StateDB11.open(configManager.dbPath);
|
|
1014
1314
|
try {
|
|
1015
|
-
const client =
|
|
1016
|
-
const vaultFs =
|
|
1017
|
-
const orchestrator = new
|
|
1315
|
+
const client = NotionClient11.fromConfig(config);
|
|
1316
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
1317
|
+
const orchestrator = new SyncOrchestrator10(config, stateDb, client, vaultFs);
|
|
1018
1318
|
const report2 = await orchestrator.verifyCompleteness();
|
|
1019
1319
|
if (opts.json) {
|
|
1020
1320
|
console.log(JSON.stringify(report2, null, 2));
|
|
@@ -1027,6 +1327,37 @@ var verifyCommand = new Command10("verify").description("\uC644\uACB0\uC131 \uAC
|
|
|
1027
1327
|
}
|
|
1028
1328
|
});
|
|
1029
1329
|
|
|
1330
|
+
// src/commands/prepare-db.ts
|
|
1331
|
+
import { Command as Command12 } from "commander";
|
|
1332
|
+
import { ConfigManager as ConfigManager13, StateDB as StateDB12, NotionClient as NotionClient12, SyncOrchestrator as SyncOrchestrator11 } from "@im-nobsidian/core";
|
|
1333
|
+
import chalk8 from "chalk";
|
|
1334
|
+
var prepareDbCommand = new Command12("prepare-db").description("\uBCFC\uD2B8\uC758 \uB178\uD2B8 \uD3F4\uB354\uB97C \uB2E4\uC74C push \uC5D0\uC11C Notion DB \uB85C \uB9CC\uB4E4\uB3C4\uB85D \uC900\uBE44 (Notion \uC740 \uADF8\uB300\uB85C)").argument("<folder>", "DB \uB85C \uB9CC\uB4E4 \uD3F4\uB354 \u2014 \uBCFC\uD2B8 \uAE30\uC900 \uACBD\uB85C. \uBC14\uB85C \uC544\uB798 \uB178\uD2B8\uAC00 \uD589\uC774 \uB41C\uB2E4").action(async (folder) => {
|
|
1335
|
+
const cwd = process.cwd();
|
|
1336
|
+
const configManager = new ConfigManager13(cwd);
|
|
1337
|
+
const config = await configManager.load();
|
|
1338
|
+
const stateDb = StateDB12.open(configManager.dbPath);
|
|
1339
|
+
try {
|
|
1340
|
+
const client = NotionClient12.fromConfig(config);
|
|
1341
|
+
const vaultFs = await openVaultFs(cwd, config.paths);
|
|
1342
|
+
const orchestrator = new SyncOrchestrator11(config, stateDb, client, vaultFs);
|
|
1343
|
+
const report2 = await orchestrator.prepareFolderDatabase(folder, obsidianConfigDir(cwd));
|
|
1344
|
+
console.log(` ${icons.success} ${report2.sidecarPath} \u2014 \uD589 ${report2.rows}\uAC1C`);
|
|
1345
|
+
for (const property of report2.properties) {
|
|
1346
|
+
console.log(` ${property.name}: ${property.type} ${chalk8.dim(`(${property.reason})`)}`);
|
|
1347
|
+
}
|
|
1348
|
+
if (report2.views.length > 0) console.log(` \uBCF4\uAE30: ${report2.views.join(", ")}`);
|
|
1349
|
+
for (const skip of report2.skipped) {
|
|
1350
|
+
console.log(chalk8.dim(` \uC62E\uAE30\uC9C0 \uC54A\uC74C \u2014 ${skip.what}: ${skip.reason}`));
|
|
1351
|
+
}
|
|
1352
|
+
console.log(chalk8.dim(" \uD0C0\uC785\uC744 \uBC14\uAFB8\uB824\uBA74 \uC0AC\uC774\uB4DC\uCE74\uB97C \uACE0\uCE5C \uB4A4 push \uD558\uC138\uC694."));
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
failedItem(folder, error instanceof Error ? error.message : String(error));
|
|
1355
|
+
process.exitCode = 1;
|
|
1356
|
+
} finally {
|
|
1357
|
+
stateDb.close();
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1030
1361
|
// src/utils/exit.ts
|
|
1031
1362
|
var FORCED_EXIT_DELAY_MS = 100;
|
|
1032
1363
|
function resolveExitCode(exitCode) {
|
|
@@ -1044,7 +1375,7 @@ function scheduleForcedExit(proc, schedule = setTimeout) {
|
|
|
1044
1375
|
// src/index.ts
|
|
1045
1376
|
var requireJson = createRequire(import.meta.url);
|
|
1046
1377
|
var { version } = requireJson("../package.json");
|
|
1047
|
-
var program = new
|
|
1378
|
+
var program = new Command13();
|
|
1048
1379
|
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
1380
|
program.addCommand(initCommand);
|
|
1050
1381
|
program.addCommand(pushCommand);
|
|
@@ -1052,11 +1383,19 @@ program.addCommand(pullCommand);
|
|
|
1052
1383
|
program.addCommand(syncCommand);
|
|
1053
1384
|
program.addCommand(statusCommand);
|
|
1054
1385
|
program.addCommand(diffCommand);
|
|
1386
|
+
program.addCommand(discardCommand);
|
|
1055
1387
|
program.addCommand(fetchCommand);
|
|
1056
1388
|
program.addCommand(resolveCommand);
|
|
1057
1389
|
program.addCommand(watchCommand);
|
|
1058
1390
|
program.addCommand(verifyCommand);
|
|
1059
|
-
program.
|
|
1060
|
-
|
|
1061
|
-
|
|
1391
|
+
program.addCommand(prepareDbCommand);
|
|
1392
|
+
program.parseAsync().then(
|
|
1393
|
+
() => {
|
|
1394
|
+
scheduleForcedExit(process);
|
|
1395
|
+
},
|
|
1396
|
+
(error) => {
|
|
1397
|
+
reportStateDbFailure(error, process);
|
|
1398
|
+
scheduleForcedExit(process);
|
|
1399
|
+
}
|
|
1400
|
+
);
|
|
1062
1401
|
//# sourceMappingURL=index.js.map
|