pushwork 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +8 -1
  2. package/bench/filesystem.bench.ts +78 -0
  3. package/bench/hashing.bench.ts +60 -0
  4. package/bench/move-detection.bench.ts +130 -0
  5. package/bench/runner.ts +49 -0
  6. package/dist/cli/commands.d.ts +15 -25
  7. package/dist/cli/commands.d.ts.map +1 -1
  8. package/dist/cli/commands.js +409 -519
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/cli/output.d.ts +75 -0
  11. package/dist/cli/output.d.ts.map +1 -0
  12. package/dist/cli/output.js +182 -0
  13. package/dist/cli/output.js.map +1 -0
  14. package/dist/cli.js +119 -51
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config/remote-manager.d.ts +65 -0
  17. package/dist/config/remote-manager.d.ts.map +1 -0
  18. package/dist/config/remote-manager.js +243 -0
  19. package/dist/config/remote-manager.js.map +1 -0
  20. package/dist/core/change-detection.d.ts +8 -0
  21. package/dist/core/change-detection.d.ts.map +1 -1
  22. package/dist/core/change-detection.js +63 -0
  23. package/dist/core/change-detection.js.map +1 -1
  24. package/dist/core/move-detection.d.ts +9 -48
  25. package/dist/core/move-detection.d.ts.map +1 -1
  26. package/dist/core/move-detection.js +53 -135
  27. package/dist/core/move-detection.js.map +1 -1
  28. package/dist/core/sync-engine.d.ts.map +1 -1
  29. package/dist/core/sync-engine.js +17 -85
  30. package/dist/core/sync-engine.js.map +1 -1
  31. package/dist/types/config.d.ts +45 -5
  32. package/dist/types/config.d.ts.map +1 -1
  33. package/dist/types/documents.d.ts +0 -1
  34. package/dist/types/documents.d.ts.map +1 -1
  35. package/dist/types/snapshot.d.ts +3 -0
  36. package/dist/types/snapshot.d.ts.map +1 -1
  37. package/dist/types/snapshot.js.map +1 -1
  38. package/dist/utils/fs.d.ts.map +1 -1
  39. package/dist/utils/fs.js +9 -33
  40. package/dist/utils/fs.js.map +1 -1
  41. package/dist/utils/index.d.ts +0 -1
  42. package/dist/utils/index.d.ts.map +1 -1
  43. package/dist/utils/index.js +0 -1
  44. package/dist/utils/index.js.map +1 -1
  45. package/dist/utils/repo-factory.d.ts.map +1 -1
  46. package/dist/utils/repo-factory.js +18 -9
  47. package/dist/utils/repo-factory.js.map +1 -1
  48. package/dist/utils/string-similarity.d.ts +14 -0
  49. package/dist/utils/string-similarity.d.ts.map +1 -0
  50. package/dist/utils/string-similarity.js +43 -0
  51. package/dist/utils/string-similarity.js.map +1 -0
  52. package/package.json +10 -5
  53. package/src/cli/commands.ts +520 -697
  54. package/src/cli/output.ts +244 -0
  55. package/src/cli.ts +182 -73
  56. package/src/core/change-detection.ts +95 -0
  57. package/src/core/move-detection.ts +69 -177
  58. package/src/core/sync-engine.ts +17 -105
  59. package/src/types/config.ts +50 -7
  60. package/src/types/documents.ts +0 -1
  61. package/src/types/snapshot.ts +1 -0
  62. package/src/utils/fs.ts +9 -33
  63. package/src/utils/index.ts +0 -1
  64. package/src/utils/repo-factory.ts +21 -8
  65. package/src/utils/string-similarity.ts +54 -0
  66. package/src/utils/content-similarity.ts +0 -194
  67. package/test/unit/content-similarity.test.ts +0 -236
@@ -32,11 +32,7 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.ProgressMessages = void 0;
40
36
  exports.setupCommandContext = setupCommandContext;
41
37
  exports.safeRepoShutdown = safeRepoShutdown;
42
38
  exports.init = init;
@@ -49,15 +45,51 @@ exports.clone = clone;
49
45
  exports.url = url;
50
46
  exports.commit = commit;
51
47
  exports.debug = debug;
48
+ exports.ls = ls;
49
+ exports.config = config;
52
50
  const path = __importStar(require("path"));
53
51
  const fs = __importStar(require("fs/promises"));
54
- const chalk_1 = __importDefault(require("chalk"));
55
- const ora_1 = __importDefault(require("ora"));
56
52
  const diffLib = __importStar(require("diff"));
57
53
  const core_1 = require("../core");
58
54
  const utils_1 = require("../utils");
59
55
  const config_1 = require("../config");
60
56
  const repo_factory_1 = require("../utils/repo-factory");
57
+ const output_1 = require("./output");
58
+ /**
59
+ * Simple key transformation for debug output: snake_case -> Title Case
60
+ */
61
+ function prettifyKey(key) {
62
+ return (key
63
+ .split("_")
64
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
65
+ .join(" ") + ":");
66
+ }
67
+ /**
68
+ * Format timing value with percentage and optional metadata
69
+ */
70
+ function formatTimingValue(value, key, total, timings) {
71
+ // Skip non-timing values
72
+ if (key === "documents_to_sync")
73
+ return "";
74
+ if (key === "total")
75
+ return `${(value / 1000).toFixed(3)}s`;
76
+ const timeStr = `${(value / 1000).toFixed(3)}s`;
77
+ const pctStr = `(${((value / total) * 100).toFixed(1)}%)`;
78
+ return `${timeStr} ${pctStr}`;
79
+ }
80
+ /**
81
+ * Validate that sync server options are used together
82
+ */
83
+ function validateSyncServerOptions(syncServer, syncServerStorageId) {
84
+ const hasSyncServer = !!syncServer;
85
+ const hasSyncServerStorageId = !!syncServerStorageId;
86
+ if (hasSyncServer && !hasSyncServerStorageId) {
87
+ throw new Error("--sync-server requires --sync-server-storage-id\nBoth arguments must be provided together.");
88
+ }
89
+ if (hasSyncServerStorageId && !hasSyncServer) {
90
+ throw new Error("--sync-server-storage-id requires --sync-server\nBoth arguments must be provided together.");
91
+ }
92
+ }
61
93
  /**
62
94
  * Shared pre-action that ensures repository and sync engine are properly initialized
63
95
  * This function always works, with or without network connectivity
@@ -111,102 +143,30 @@ async function safeRepoShutdown(repo, context) {
111
143
  console.warn(`Warning: Repository shutdown failed${context ? ` (${context})` : ""}: ${shutdownError}`);
112
144
  }
113
145
  }
114
- /**
115
- * Common progress message helpers
116
- */
117
- exports.ProgressMessages = {
118
- // Setup messages
119
- directoryFound: () => console.log(chalk_1.default.gray(" ✓ Sync directory found")),
120
- configLoaded: () => console.log(chalk_1.default.gray(" ✓ Configuration loaded")),
121
- repoConnected: () => console.log(chalk_1.default.gray(" ✓ Connected to repository")),
122
- // Configuration display
123
- syncServer: (server) => console.log(chalk_1.default.gray(` ✓ Sync server: ${server}`)),
124
- storageId: (id) => console.log(chalk_1.default.gray(` ✓ Storage ID: ${id}`)),
125
- rootUrl: (url) => console.log(chalk_1.default.gray(` ✓ Root URL: ${url}`)),
126
- // Operation completion
127
- changesWritten: () => console.log(chalk_1.default.gray(" ✓ All changes written to disk")),
128
- syncCompleted: (duration) => console.log(chalk_1.default.gray(` ✓ Initial sync completed in ${duration}ms`)),
129
- directoryStructureCreated: () => console.log(chalk_1.default.gray(" ✓ Created sync directory structure")),
130
- configSaved: () => console.log(chalk_1.default.gray(" ✓ Saved configuration")),
131
- repoCreated: () => console.log(chalk_1.default.gray(" ✓ Created Automerge repository")),
132
- };
133
- /**
134
- * Show actual content diff for a changed file
135
- */
136
- async function showContentDiff(change) {
137
- try {
138
- // Get old content (from snapshot/remote)
139
- const oldContent = change.remoteContent || "";
140
- // Get new content (current local)
141
- const newContent = change.localContent || "";
142
- // Convert binary content to string representation if needed
143
- const oldText = typeof oldContent === "string"
144
- ? oldContent
145
- : `<binary content: ${oldContent.length} bytes>`;
146
- const newText = typeof newContent === "string"
147
- ? newContent
148
- : `<binary content: ${newContent.length} bytes>`;
149
- // Generate unified diff
150
- const diffResult = diffLib.createPatch(change.path, oldText, newText, "previous", "current");
151
- // Skip the header lines and process the diff
152
- const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
153
- if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
154
- console.log(chalk_1.default.gray(" (content identical)"));
155
- return;
156
- }
157
- for (const line of lines) {
158
- if (line.startsWith("@@")) {
159
- // Hunk header
160
- console.log(chalk_1.default.cyan(line));
161
- }
162
- else if (line.startsWith("+")) {
163
- // Added line
164
- console.log(chalk_1.default.green(line));
165
- }
166
- else if (line.startsWith("-")) {
167
- // Removed line
168
- console.log(chalk_1.default.red(line));
169
- }
170
- else if (line.startsWith(" ")) {
171
- // Context line
172
- console.log(chalk_1.default.gray(line));
173
- }
174
- else if (line === "") {
175
- // Empty line
176
- console.log("");
177
- }
178
- }
179
- }
180
- catch (error) {
181
- console.log(chalk_1.default.gray(` (diff error: ${error})`));
182
- }
183
- }
184
146
  /**
185
147
  * Initialize sync in a directory
186
148
  */
187
- async function init(targetPath, syncServer, syncServerStorageId) {
188
- const spinner = (0, ora_1.default)("Starting initialization...").start();
149
+ async function init(targetPath, options = {}) {
150
+ // Validate sync server options
151
+ validateSyncServerOptions(options.syncServer, options.syncServerStorageId);
152
+ const out = new output_1.Output();
189
153
  try {
190
154
  const resolvedPath = path.resolve(targetPath);
191
- // Step 1: Directory setup
192
- spinner.text = "Setting up directory structure...";
155
+ out.task(`Initializing ${resolvedPath}`);
193
156
  await (0, utils_1.ensureDirectoryExists)(resolvedPath);
194
157
  // Check if already initialized
195
158
  const syncToolDir = path.join(resolvedPath, ".pushwork");
196
159
  if (await (0, utils_1.pathExists)(syncToolDir)) {
197
- spinner.fail("Directory already initialized for sync");
198
- return;
160
+ out.error("Directory already initialized for sync");
161
+ out.exit(1);
199
162
  }
200
- // Step 2: Create sync directories
201
- spinner.text = "Creating .pushwork directory...";
163
+ out.update("Creating sync directory");
202
164
  await (0, utils_1.ensureDirectoryExists)(syncToolDir);
203
165
  await (0, utils_1.ensureDirectoryExists)(path.join(syncToolDir, "automerge"));
204
- exports.ProgressMessages.directoryStructureCreated();
205
- // Step 3: Configuration setup
206
- spinner.text = "Setting up configuration...";
166
+ out.update("Setting up configuration");
207
167
  const configManager = new config_1.ConfigManager(resolvedPath);
208
- const defaultSyncServer = syncServer || "wss://sync3.automerge.org";
209
- const defaultStorageId = syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
168
+ const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
169
+ const defaultStorageId = options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
210
170
  const config = {
211
171
  sync_server: defaultSyncServer,
212
172
  sync_server_storage_id: defaultStorageId,
@@ -226,432 +186,339 @@ async function init(targetPath, syncServer, syncServerStorageId) {
226
186
  },
227
187
  };
228
188
  await configManager.save(config);
229
- exports.ProgressMessages.configSaved();
230
- exports.ProgressMessages.syncServer(defaultSyncServer);
231
- exports.ProgressMessages.storageId(defaultStorageId);
232
- // Step 4: Initialize Automerge repo and create root directory document
233
- spinner.text = "Creating root directory document...";
189
+ out.update("Creating root directory");
234
190
  const repo = await (0, repo_factory_1.createRepo)(resolvedPath, {
235
191
  enableNetwork: true,
236
- syncServer: syncServer,
237
- syncServerStorageId: syncServerStorageId,
192
+ syncServer: options.syncServer,
193
+ syncServerStorageId: options.syncServerStorageId,
238
194
  });
239
- // Create the root directory document
240
195
  const rootDoc = {
241
196
  "@patchwork": { type: "folder" },
242
197
  docs: [],
243
198
  };
244
199
  const rootHandle = repo.create(rootDoc);
245
- exports.ProgressMessages.repoCreated();
246
- exports.ProgressMessages.rootUrl(rootHandle.url);
247
- // Step 5: Scan existing files
248
- spinner.text = "Scanning existing files...";
249
- const syncEngine = new core_1.SyncEngine(repo, resolvedPath, config.defaults.exclude_patterns, true, // Network sync enabled for init
250
- config.sync_server_storage_id);
251
- // Get file count for progress
252
- const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true });
253
- const fileCount = dirEntries.filter((dirent) => dirent.isFile()).length;
254
- if (fileCount > 0) {
255
- console.log(chalk_1.default.gray(` ✓ Found ${fileCount} existing files`));
256
- spinner.text = `Creating initial snapshot with ${fileCount} files...`;
257
- }
258
- else {
259
- spinner.text = "Creating initial empty snapshot...";
260
- }
261
- // Step 6: Set the root directory URL before creating initial snapshot
200
+ out.update("Scanning existing files");
201
+ const syncEngine = new core_1.SyncEngine(repo, resolvedPath, config.defaults.exclude_patterns, true, config.sync_server_storage_id);
262
202
  await syncEngine.setRootDirectoryUrl(rootHandle.url);
263
- // Step 7: Create initial snapshot
264
- spinner.text = "Creating initial snapshot...";
265
- const startTime = Date.now();
266
- await syncEngine.sync(false);
267
- const duration = Date.now() - startTime;
268
- exports.ProgressMessages.syncCompleted(duration);
269
- // Step 8: Ensure all Automerge operations are flushed to disk
270
- spinner.text = "Flushing changes to disk...";
203
+ const result = await syncEngine.sync(false);
204
+ out.update("Writing to disk");
271
205
  await safeRepoShutdown(repo, "init");
272
- exports.ProgressMessages.changesWritten();
273
- spinner.succeed(`Initialized sync in ${chalk_1.default.green(resolvedPath)}`);
274
- console.log(`\n${chalk_1.default.bold("🎉 Sync Directory Created!")}`);
275
- console.log(` 📁 Directory: ${chalk_1.default.blue(resolvedPath)}`);
276
- console.log(` 🔗 Sync server: ${chalk_1.default.blue(defaultSyncServer)}`);
277
- console.log(`\n${chalk_1.default.green("Initialization complete!")} Run ${chalk_1.default.cyan("pushwork sync")} to start syncing.`);
206
+ out.done();
207
+ out.pair("Sync", defaultSyncServer);
208
+ if (result.filesChanged > 0) {
209
+ out.pair("Files", `${result.filesChanged} added`);
210
+ }
211
+ out.success("INITIALIZED", rootHandle.url);
212
+ // Show timing breakdown if debug mode is enabled
213
+ if (options.debug &&
214
+ result.timings &&
215
+ Object.keys(result.timings).length > 0) {
216
+ const total = result.timings.total || 0;
217
+ out.log("");
218
+ out.special("TIMING", "");
219
+ out.obj(result.timings, prettifyKey, (value, key) => formatTimingValue(value, key, total, result.timings));
220
+ }
221
+ out.log("");
222
+ out.log(`Run 'pushwork sync' to start synchronizing`);
278
223
  }
279
224
  catch (error) {
280
- spinner.fail(`Failed to initialize: ${error}`);
281
- throw error;
225
+ out.error("FAILED", "Initialization failed");
226
+ out.log(` ${error}`);
227
+ out.exit(1);
282
228
  }
229
+ process.exit();
283
230
  }
284
231
  /**
285
232
  * Run bidirectional sync
286
233
  */
287
- async function sync(options) {
288
- const spinner = (0, ora_1.default)("Starting sync operation...").start();
234
+ async function sync(targetPath = ".", options) {
235
+ const out = new output_1.Output();
289
236
  try {
290
- // Step 1: Setup shared context
291
- spinner.text = "Setting up sync context...";
292
- const { repo, syncEngine, config, workingDir } = await setupCommandContext();
293
- exports.ProgressMessages.directoryFound();
294
- exports.ProgressMessages.configLoaded();
295
- exports.ProgressMessages.syncServer(config?.sync_server || "wss://sync3.automerge.org");
296
- exports.ProgressMessages.repoConnected();
297
- // Show root directory URL for context
298
- const syncStatus = await syncEngine.getStatus();
299
- if (syncStatus.snapshot?.rootDirectoryUrl) {
300
- exports.ProgressMessages.rootUrl(syncStatus.snapshot.rootDirectoryUrl);
301
- }
237
+ out.task("Syncing");
238
+ const { repo, syncEngine, workingDir } = await setupCommandContext(targetPath);
302
239
  if (options.dryRun) {
303
- // Dry run mode - detailed preview
304
- spinner.text = "Analyzing changes (dry run)...";
305
- const startTime = Date.now();
240
+ out.update("Analyzing changes");
306
241
  const preview = await syncEngine.previewChanges();
307
- const analysisTime = Date.now() - startTime;
308
- spinner.succeed("Change analysis completed");
309
- console.log(`\n${chalk_1.default.bold("📊 Change Analysis")} (${analysisTime}ms):`);
310
- console.log(chalk_1.default.gray(` Directory: ${workingDir}`));
311
- console.log(chalk_1.default.gray(` Analysis time: ${analysisTime}ms`));
242
+ out.done();
312
243
  if (preview.changes.length === 0 && preview.moves.length === 0) {
313
- console.log(`\n${chalk_1.default.green("No changes detected")} - everything is in sync!`);
244
+ out.info("No changes detected");
245
+ out.log("Everything is already in sync");
314
246
  return;
315
247
  }
316
- console.log(`\n${chalk_1.default.bold("📋 Summary:")}`);
317
- console.log(` ${preview.summary}`);
318
- if (preview.changes.length > 0) {
319
- const localChanges = preview.changes.filter((c) => c.changeType === "local_only" || c.changeType === "both_changed").length;
320
- const remoteChanges = preview.changes.filter((c) => c.changeType === "remote_only" || c.changeType === "both_changed").length;
321
- const conflicts = preview.changes.filter((c) => c.changeType === "both_changed").length;
322
- console.log(`\n${chalk_1.default.bold("📁 File Changes:")} (${preview.changes.length} total)`);
323
- if (localChanges > 0) {
324
- console.log(` ${chalk_1.default.green("📤")} Local changes: ${localChanges}`);
325
- }
326
- if (remoteChanges > 0) {
327
- console.log(` ${chalk_1.default.blue("📥")} Remote changes: ${remoteChanges}`);
328
- }
329
- if (conflicts > 0) {
330
- console.log(` ${chalk_1.default.yellow("⚠️")} Conflicts: ${conflicts}`);
331
- }
332
- console.log(`\n${chalk_1.default.bold("📄 Changed Files:")}`);
333
- for (const change of preview.changes.slice(0, 10)) {
334
- // Show first 10
335
- const typeIcon = change.changeType === "local_only"
336
- ? chalk_1.default.green("📤")
337
- : change.changeType === "remote_only"
338
- ? chalk_1.default.blue("📥")
339
- : change.changeType === "both_changed"
340
- ? chalk_1.default.yellow("⚠️")
341
- : chalk_1.default.gray("➖");
342
- console.log(` ${typeIcon} ${change.path}`);
343
- }
344
- if (preview.changes.length > 10) {
345
- console.log(` ${chalk_1.default.gray(`... and ${preview.changes.length - 10} more files`)}`);
346
- }
248
+ out.info("CHANGES", "Pending");
249
+ out.pair("Directory", workingDir);
250
+ out.pair("Changes", preview.changes.length.toString());
251
+ if (preview.moves.length > 0) {
252
+ out.pair("Moves", preview.moves.length.toString());
253
+ }
254
+ out.log("");
255
+ out.log("Files:");
256
+ for (const change of preview.changes.slice(0, 10)) {
257
+ const prefix = change.changeType === "local_only"
258
+ ? "[local] "
259
+ : change.changeType === "remote_only"
260
+ ? "[remote] "
261
+ : "[conflict]";
262
+ out.log(` ${prefix} ${change.path}`);
263
+ }
264
+ if (preview.changes.length > 10) {
265
+ out.log(` ... and ${preview.changes.length - 10} more`);
347
266
  }
348
267
  if (preview.moves.length > 0) {
349
- console.log(`\n${chalk_1.default.bold("🔄 Potential Moves:")} (${preview.moves.length})`);
268
+ out.log("");
269
+ out.log("Moves:");
350
270
  for (const move of preview.moves.slice(0, 5)) {
351
- // Show first 5
352
- const confidence = move.confidence === "auto"
353
- ? chalk_1.default.green("Auto")
354
- : move.confidence === "prompt"
355
- ? chalk_1.default.yellow("Prompt")
356
- : chalk_1.default.red("Low");
357
- console.log(` 🔄 ${move.fromPath} → ${move.toPath} (${confidence})`);
271
+ out.log(` ${move.fromPath} ${move.toPath}`);
358
272
  }
359
273
  if (preview.moves.length > 5) {
360
- console.log(` ${chalk_1.default.gray(`... and ${preview.moves.length - 5} more moves`)}`);
274
+ out.log(` ... and ${preview.moves.length - 5} more`);
361
275
  }
362
276
  }
363
- console.log(`\n${chalk_1.default.cyan("ℹ️ Run without --dry-run to apply these changes")}`);
277
+ out.log("");
278
+ out.log("Run without --dry-run to apply these changes");
364
279
  }
365
280
  else {
366
- // Actual sync operation
367
- spinner.text = "Detecting changes...";
368
- const startTime = Date.now();
281
+ out.update("Synchronizing");
369
282
  const result = await syncEngine.sync(false);
370
- const totalTime = Date.now() - startTime;
283
+ out.update("Writing to disk");
284
+ await safeRepoShutdown(repo, "sync");
371
285
  if (result.success) {
372
- spinner.succeed(`Sync completed in ${totalTime}ms`);
373
- console.log(`\n${chalk_1.default.bold("✅ Sync Results:")}`);
374
- console.log(` 📄 Files changed: ${chalk_1.default.yellow(result.filesChanged)}`);
375
- console.log(` 📁 Directories changed: ${chalk_1.default.yellow(result.directoriesChanged)}`);
376
- console.log(` ⏱️ Total time: ${chalk_1.default.gray(totalTime + "ms")}`);
286
+ if (result.filesChanged === 0 && result.directoriesChanged === 0) {
287
+ out.done();
288
+ out.success("Already in sync");
289
+ }
290
+ else {
291
+ out.done();
292
+ out.success("SYNCED", `${result.filesChanged} file${result.filesChanged !== 1 ? "s" : ""} updated`);
293
+ out.pair("Files", result.filesChanged.toString());
294
+ }
377
295
  if (result.warnings.length > 0) {
378
- console.log(`\n${chalk_1.default.yellow("⚠️ Warnings:")} (${result.warnings.length})`);
296
+ out.log("");
297
+ out.warn("WARNINGS", `${result.warnings.length} warnings`);
379
298
  for (const warning of result.warnings.slice(0, 5)) {
380
- console.log(` ${chalk_1.default.yellow("⚠️")} ${warning}`);
299
+ out.log(` ${warning}`);
381
300
  }
382
301
  if (result.warnings.length > 5) {
383
- console.log(` ${chalk_1.default.gray(`... and ${result.warnings.length - 5} more warnings`)}`);
302
+ out.log(` ... and ${result.warnings.length - 5} more`);
384
303
  }
385
304
  }
386
- if (result.filesChanged === 0 && result.directoriesChanged === 0) {
387
- console.log(`\n${chalk_1.default.green("✨ Everything already in sync!")}`);
305
+ // Show timing breakdown if debug mode is enabled
306
+ if (options.debug &&
307
+ result.timings &&
308
+ Object.keys(result.timings).length > 0) {
309
+ const total = result.timings.total || 0;
310
+ out.log("");
311
+ out.special("TIMING", "");
312
+ out.obj(result.timings, prettifyKey, (value, key) => formatTimingValue(value, key, total, result.timings));
388
313
  }
389
- // Ensure all changes are flushed to disk
390
- spinner.text = "Flushing changes to disk...";
391
- await safeRepoShutdown(repo, "sync");
392
- exports.ProgressMessages.changesWritten();
393
314
  }
394
315
  else {
395
- spinner.fail("Sync completed with errors");
396
- console.log(`\n${chalk_1.default.red("❌ Sync Errors:")} (${result.errors.length})`);
316
+ out.done("partial", false);
317
+ out.warn("PARTIAL", `${result.filesChanged} updated, ${result.errors.length} errors`);
318
+ out.pair("Files", result.filesChanged.toString());
319
+ out.pair("Errors", result.errors.length.toString());
320
+ out.log("");
397
321
  for (const error of result.errors.slice(0, 5)) {
398
- console.log(` ${chalk_1.default.red("")} ${error.path}: ${error.error.message}`);
322
+ out.error("ERROR", error.path);
323
+ out.log(` ${error.error.message}`);
324
+ out.log("");
399
325
  }
400
326
  if (result.errors.length > 5) {
401
- console.log(` ${chalk_1.default.gray(`... and ${result.errors.length - 5} more errors`)}`);
327
+ out.log(`... and ${result.errors.length - 5} more errors`);
402
328
  }
403
- if (result.filesChanged > 0 || result.directoriesChanged > 0) {
404
- console.log(`\n${chalk_1.default.yellow("⚠️ Partial sync completed:")}`);
405
- console.log(` 📄 Files changed: ${result.filesChanged}`);
406
- console.log(` 📁 Directories changed: ${result.directoriesChanged}`);
407
- }
408
- // Still try to flush any partial changes
409
- await safeRepoShutdown(repo, "sync-error");
410
329
  }
411
330
  }
412
331
  }
413
332
  catch (error) {
414
- spinner.fail(`Sync failed: ${error}`);
415
- throw error;
333
+ out.error("FAILED", "Sync failed");
334
+ out.log(` ${error}`);
335
+ out.exit(1);
416
336
  }
337
+ process.exit();
417
338
  }
418
339
  /**
419
340
  * Show differences between local and remote
420
341
  */
421
342
  async function diff(targetPath = ".", options) {
343
+ const out = new output_1.Output();
422
344
  try {
423
- // Setup shared context with network disabled for diff check
345
+ out.task("Analyzing changes");
424
346
  const { repo, syncEngine } = await setupCommandContext(targetPath, undefined, undefined, false);
425
347
  const preview = await syncEngine.previewChanges();
348
+ out.done();
426
349
  if (options.nameOnly) {
427
- // Show only file names
428
350
  for (const change of preview.changes) {
429
351
  console.log(change.path);
430
352
  }
431
353
  return;
432
354
  }
433
- // Show root directory URL for context
434
- const diffStatus = await syncEngine.getStatus();
435
- if (diffStatus.snapshot?.rootDirectoryUrl) {
436
- console.log(chalk_1.default.gray(`Root URL: ${diffStatus.snapshot.rootDirectoryUrl}`));
437
- console.log("");
438
- }
439
355
  if (preview.changes.length === 0) {
440
- console.log(chalk_1.default.green("No changes detected"));
356
+ out.success("No changes detected");
357
+ await safeRepoShutdown(repo, "diff");
358
+ out.exit();
441
359
  return;
442
360
  }
443
- console.log(chalk_1.default.bold("Differences:"));
361
+ out.warn(`${preview.changes.length} changes detected`);
444
362
  for (const change of preview.changes) {
445
- const typeLabel = change.changeType === "local_only"
446
- ? chalk_1.default.green("[LOCAL]")
363
+ const prefix = change.changeType === "local_only"
364
+ ? "[local] "
447
365
  : change.changeType === "remote_only"
448
- ? chalk_1.default.blue("[REMOTE]")
449
- : change.changeType === "both_changed"
450
- ? chalk_1.default.yellow("[CONFLICT]")
451
- : chalk_1.default.gray("[NO CHANGE]");
452
- console.log(`\n${typeLabel} ${change.path}`);
453
- if (options.tool) {
454
- console.log(` Use "${options.tool}" to view detailed diff`);
366
+ ? "[remote] "
367
+ : "[conflict]";
368
+ if (!options.tool) {
369
+ try {
370
+ // Get old content (from snapshot/remote)
371
+ const oldContent = change.remoteContent || "";
372
+ // Get new content (current local)
373
+ const newContent = change.localContent || "";
374
+ // Convert binary content to string representation if needed
375
+ const oldText = typeof oldContent === "string"
376
+ ? oldContent
377
+ : `<binary content: ${oldContent.length} bytes>`;
378
+ const newText = typeof newContent === "string"
379
+ ? newContent
380
+ : `<binary content: ${newContent.length} bytes>`;
381
+ // Generate unified diff
382
+ const diffResult = diffLib.createPatch(change.path, oldText, newText, "previous", "current");
383
+ // Skip the header lines and process the diff
384
+ const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
385
+ if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
386
+ out.log(`${prefix}${change.path} (content identical)`, "cyan");
387
+ continue;
388
+ }
389
+ // Extract first hunk header and show inline with path
390
+ let firstHunk = "";
391
+ let diffLines = lines;
392
+ if (lines[0]?.startsWith("@@")) {
393
+ firstHunk = ` ${lines[0]}`;
394
+ diffLines = lines.slice(1);
395
+ }
396
+ out.log(`${prefix}${change.path}${firstHunk}`, "cyan");
397
+ for (const line of diffLines) {
398
+ if (line.startsWith("@@")) {
399
+ // Additional hunk headers
400
+ out.log(line, "dim");
401
+ }
402
+ else if (line.startsWith("+")) {
403
+ // Added line
404
+ out.log(line, "green");
405
+ }
406
+ else if (line.startsWith("-")) {
407
+ // Removed line
408
+ out.log(line, "red");
409
+ }
410
+ else if (line.startsWith(" ") || line === "") {
411
+ // Context line or empty
412
+ out.log(line, "dim");
413
+ }
414
+ }
415
+ }
416
+ catch (error) {
417
+ out.log(`${prefix}${change.path} (diff error: ${error})`, "cyan");
418
+ }
455
419
  }
456
420
  else {
457
- // Show actual diff content
458
- await showContentDiff(change);
421
+ out.log(`${prefix} ${change.path}`);
459
422
  }
460
423
  }
461
- // Cleanup repo resources
462
424
  await safeRepoShutdown(repo, "diff");
463
425
  }
464
426
  catch (error) {
465
- console.error(chalk_1.default.red(`Diff failed: ${error}`));
466
- throw error;
427
+ out.error(`Diff failed: ${error}`);
428
+ out.exit(1);
467
429
  }
468
430
  }
469
431
  /**
470
432
  * Show sync status
471
433
  */
472
- async function status() {
434
+ async function status(targetPath = ".", options = {}) {
435
+ const out = new output_1.Output();
473
436
  try {
474
- const spinner = (0, ora_1.default)("Loading sync status...").start();
475
- // Setup shared context with network disabled for status check
476
- const { repo, syncEngine, workingDir } = await setupCommandContext(process.cwd(), undefined, undefined, false);
437
+ out.task("Loading status");
438
+ const { repo, syncEngine, workingDir, config } = await setupCommandContext(targetPath, undefined, undefined, false);
477
439
  const syncStatus = await syncEngine.getStatus();
478
- spinner.stop();
479
- console.log(chalk_1.default.bold("📊 Sync Status Report"));
480
- console.log(`${"=".repeat(50)}`);
481
- // Directory information
482
- console.log(`\n${chalk_1.default.bold("📁 Directory Information:")}`);
483
- console.log(` 📂 Path: ${chalk_1.default.blue(workingDir)}`);
484
- console.log(` 🔧 Config: ${path.join(workingDir, ".pushwork")}`);
485
- // Show root directory URL if available
440
+ out.done();
441
+ out.info("STATUS", workingDir);
486
442
  if (syncStatus.snapshot?.rootDirectoryUrl) {
487
- console.log(` 🔗 Root URL: ${chalk_1.default.cyan(syncStatus.snapshot.rootDirectoryUrl)}`);
488
- // Try to show lastSyncAt from root directory document
489
- try {
490
- const rootHandle = await repo.find(syncStatus.snapshot.rootDirectoryUrl);
491
- const rootDoc = await rootHandle.doc();
492
- if (rootDoc?.lastSyncAt) {
493
- const lastSyncDate = new Date(rootDoc.lastSyncAt);
494
- const timeSince = Date.now() - rootDoc.lastSyncAt;
495
- const timeAgo = timeSince < 60000
496
- ? `${Math.floor(timeSince / 1000)}s ago`
497
- : timeSince < 3600000
498
- ? `${Math.floor(timeSince / 60000)}m ago`
499
- : `${Math.floor(timeSince / 3600000)}h ago`;
500
- console.log(` 🕒 Root last touched: ${chalk_1.default.green(lastSyncDate.toLocaleString())} (${chalk_1.default.gray(timeAgo)})`);
501
- }
502
- else {
503
- console.log(` 🕒 Root last touched: ${chalk_1.default.yellow("Never")}`);
504
- }
505
- }
506
- catch (error) {
507
- console.log(` 🕒 Root last touched: ${chalk_1.default.gray("Unable to determine")}`);
508
- }
509
- }
510
- else {
511
- console.log(` 🔗 Root URL: ${chalk_1.default.yellow("Not set")}`);
512
- }
513
- // Sync timing
514
- if (syncStatus.lastSync) {
515
- const timeSince = Date.now() - syncStatus.lastSync.getTime();
516
- const timeAgo = timeSince < 60000
517
- ? `${Math.floor(timeSince / 1000)}s ago`
518
- : timeSince < 3600000
519
- ? `${Math.floor(timeSince / 60000)}m ago`
520
- : `${Math.floor(timeSince / 3600000)}h ago`;
521
- console.log(`\n${chalk_1.default.bold("⏱️ Sync Timing:")}`);
522
- console.log(` 🕐 Last sync: ${chalk_1.default.green(syncStatus.lastSync.toLocaleString())}`);
523
- console.log(` ⏳ Time since: ${chalk_1.default.gray(timeAgo)}`);
524
- }
525
- else {
526
- console.log(`\n${chalk_1.default.bold("⏱️ Sync Timing:")}`);
527
- console.log(` 🕐 Last sync: ${chalk_1.default.yellow("Never synced")}`);
528
- console.log(` 💡 Run ${chalk_1.default.cyan("pushwork sync")} to perform initial sync`);
529
- }
530
- // Change status
531
- console.log(`\n${chalk_1.default.bold("📝 Change Status:")}`);
532
- if (syncStatus.hasChanges) {
533
- console.log(` 📄 Pending changes: ${chalk_1.default.yellow(syncStatus.changeCount)}`);
534
- console.log(` 🔄 Status: ${chalk_1.default.yellow("Sync needed")}`);
535
- console.log(` 💡 Run ${chalk_1.default.cyan("pushwork diff")} to see details`);
536
- }
537
- else {
538
- console.log(` 📄 Pending changes: ${chalk_1.default.green("None")}`);
539
- console.log(` ✅ Status: ${chalk_1.default.green("Up to date")}`);
443
+ out.pair("URL", syncStatus.snapshot.rootDirectoryUrl);
540
444
  }
541
- // Configuration
542
- console.log(`\n${chalk_1.default.bold("⚙️ Configuration:")}`);
543
- const statusConfigManager2 = new config_1.ConfigManager(workingDir);
544
- const statusConfig2 = await statusConfigManager2.load();
545
- if (statusConfig2?.sync_server) {
546
- console.log(` 🔗 Sync server: ${chalk_1.default.blue(statusConfig2.sync_server)}`);
547
- }
548
- else {
549
- console.log(` 🔗 Sync server: ${chalk_1.default.blue("wss://sync3.automerge.org")} (default)`);
550
- }
551
- console.log(` ⚡ Auto sync: ${statusConfig2?.sync?.auto_sync
552
- ? chalk_1.default.green("Enabled")
553
- : chalk_1.default.gray("Disabled")}`);
554
- // Snapshot information
555
445
  if (syncStatus.snapshot) {
556
446
  const fileCount = syncStatus.snapshot.files.size;
557
- const dirCount = syncStatus.snapshot.directories.size;
558
- console.log(`\n${chalk_1.default.bold("📊 Repository Statistics:")}`);
559
- console.log(` 📄 Tracked files: ${chalk_1.default.yellow(fileCount)}`);
560
- console.log(` 📁 Tracked directories: ${chalk_1.default.yellow(dirCount)}`);
561
- console.log(` 🏷️ Snapshot timestamp: ${chalk_1.default.gray(new Date(syncStatus.snapshot.timestamp).toLocaleString())}`);
562
- }
563
- // Quick actions
564
- console.log(`\n${chalk_1.default.bold("🚀 Quick Actions:")}`);
447
+ out.pair("Files", `${fileCount} tracked`);
448
+ }
449
+ out.pair("Sync", config?.sync_server || "wss://sync3.automerge.org");
565
450
  if (syncStatus.hasChanges) {
566
- console.log(` ${chalk_1.default.cyan("pushwork diff")} - View pending changes`);
567
- console.log(` ${chalk_1.default.cyan("pushwork sync")} - Apply changes`);
451
+ out.pair("Changes", `${syncStatus.changeCount} pending`);
452
+ out.log("");
453
+ out.log("Run 'pushwork diff' to see changes");
568
454
  }
569
455
  else {
570
- console.log(` ${chalk_1.default.cyan("pushwork sync")} - Check for remote changes`);
456
+ out.pair("Status", "up to date");
571
457
  }
572
- console.log(` ${chalk_1.default.cyan("pushwork log")} - View sync history`);
573
- // Cleanup repo resources
574
458
  await safeRepoShutdown(repo, "status");
575
459
  }
576
460
  catch (error) {
577
- console.error(chalk_1.default.red(`❌ Status check failed: ${error}`));
578
- throw error;
461
+ out.error(`Status check failed: ${error}`);
462
+ out.exit(1);
579
463
  }
580
464
  }
581
465
  /**
582
466
  * Show sync history
583
467
  */
584
468
  async function log(targetPath = ".", options) {
469
+ const out = new output_1.Output();
585
470
  try {
586
- // Setup shared context with network disabled for log check
587
- const { repo: logRepo, syncEngine: logSyncEngine, workingDir, } = await setupCommandContext(targetPath, undefined, undefined, false);
588
- const logStatus = await logSyncEngine.getStatus();
589
- if (logStatus.snapshot?.rootDirectoryUrl) {
590
- console.log(chalk_1.default.gray(`Root URL: ${logStatus.snapshot.rootDirectoryUrl}`));
591
- console.log("");
592
- }
593
- // TODO: Implement history tracking and display
594
- // For now, show basic information
595
- console.log(chalk_1.default.bold("Sync History:"));
596
- // Check for snapshot files
471
+ const { repo: logRepo, workingDir } = await setupCommandContext(targetPath, undefined, undefined, false);
472
+ // TODO: Implement history tracking
597
473
  const snapshotPath = path.join(workingDir, ".pushwork", "snapshot.json");
598
474
  if (await (0, utils_1.pathExists)(snapshotPath)) {
599
475
  const stats = await fs.stat(snapshotPath);
600
- if (options.oneline) {
601
- console.log(`${stats.mtime.toISOString()} - Last sync`);
602
- }
603
- else {
604
- console.log(`Last sync: ${chalk_1.default.green(stats.mtime.toISOString())}`);
605
- console.log(`Snapshot size: ${stats.size} bytes`);
606
- }
476
+ out.info("HISTORY", "Sync history (stub)");
477
+ out.pair("Last sync", stats.mtime.toISOString());
607
478
  }
608
479
  else {
609
- console.log(chalk_1.default.yellow("No sync history found"));
480
+ out.info("No sync history found");
610
481
  }
611
- // Cleanup repo resources
612
482
  await safeRepoShutdown(logRepo, "log");
613
483
  }
614
484
  catch (error) {
615
- console.error(chalk_1.default.red(`Log failed: ${error}`));
616
- throw error;
485
+ out.error(`Log failed: ${error}`);
486
+ out.exit(1);
617
487
  }
618
488
  }
619
489
  /**
620
490
  * Checkout/restore from previous sync
621
491
  */
622
492
  async function checkout(syncId, targetPath = ".", options) {
493
+ const out = new output_1.Output();
623
494
  try {
624
- // Setup shared context
625
495
  const { workingDir } = await setupCommandContext(targetPath);
626
496
  // TODO: Implement checkout functionality
627
- // This would involve:
628
- // 1. Finding the sync with the given ID
629
- // 2. Restoring file states from that sync
630
- // 3. Updating the snapshot
631
- console.log(chalk_1.default.yellow(`Checkout functionality not yet implemented`));
632
- console.log(`Would restore to sync: ${syncId}`);
633
- console.log(`Target path: ${workingDir}`);
497
+ out.warn("NOT IMPLEMENTED", "Checkout not yet implemented");
498
+ out.pair("Sync ID", syncId);
499
+ out.pair("Path", workingDir);
634
500
  }
635
501
  catch (error) {
636
- console.error(chalk_1.default.red(`Checkout failed: ${error}`));
637
- throw error;
502
+ out.error(`Checkout failed: ${error}`);
503
+ out.exit(1);
638
504
  }
639
505
  }
640
506
  /**
641
507
  * Clone an existing synced directory from an AutomergeUrl
642
508
  */
643
509
  async function clone(rootUrl, targetPath, options) {
644
- const spinner = (0, ora_1.default)("Starting clone operation...").start();
510
+ // Validate sync server options
511
+ validateSyncServerOptions(options.syncServer, options.syncServerStorageId);
512
+ const out = new output_1.Output();
645
513
  try {
646
514
  const resolvedPath = path.resolve(targetPath);
647
- // Step 1: Directory setup
648
- spinner.text = "Setting up target directory...";
515
+ out.task(`Cloning ${rootUrl}`);
649
516
  // Check if directory exists and handle --force
650
517
  if (await (0, utils_1.pathExists)(resolvedPath)) {
651
518
  const files = await fs.readdir(resolvedPath);
652
519
  if (files.length > 0 && !options.force) {
653
- spinner.fail("Target directory is not empty. Use --force to overwrite.");
654
- return;
520
+ out.error("Target directory is not empty. Use --force to overwrite");
521
+ out.exit(1);
655
522
  }
656
523
  }
657
524
  else {
@@ -661,20 +528,15 @@ async function clone(rootUrl, targetPath, options) {
661
528
  const syncToolDir = path.join(resolvedPath, ".pushwork");
662
529
  if (await (0, utils_1.pathExists)(syncToolDir)) {
663
530
  if (!options.force) {
664
- spinner.fail("Directory already initialized for sync. Use --force to overwrite.");
665
- return;
531
+ out.error("Directory already initialized. Use --force to overwrite");
532
+ out.exit(1);
666
533
  }
667
- // Clean up existing sync directory
668
534
  await fs.rm(syncToolDir, { recursive: true, force: true });
669
535
  }
670
- console.log(chalk_1.default.gray(" Target directory prepared"));
671
- // Step 2: Create sync directories
672
- spinner.text = "Creating .pushwork directory...";
536
+ out.update("Creating sync directory");
673
537
  await (0, utils_1.ensureDirectoryExists)(syncToolDir);
674
538
  await (0, utils_1.ensureDirectoryExists)(path.join(syncToolDir, "automerge"));
675
- exports.ProgressMessages.directoryStructureCreated();
676
- // Step 3: Configuration setup
677
- spinner.text = "Setting up configuration...";
539
+ out.update("Setting up configuration");
678
540
  const configManager = new config_1.ConfigManager(resolvedPath);
679
541
  const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
680
542
  const defaultStorageId = options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
@@ -697,64 +559,47 @@ async function clone(rootUrl, targetPath, options) {
697
559
  },
698
560
  };
699
561
  await configManager.save(config);
700
- exports.ProgressMessages.configSaved();
701
- exports.ProgressMessages.syncServer(defaultSyncServer);
702
- exports.ProgressMessages.storageId(defaultStorageId);
703
- // Step 4: Initialize Automerge repo and connect to root directory
704
- spinner.text = "Connecting to root directory document...";
562
+ out.update("Connecting to sync server");
705
563
  const repo = await (0, repo_factory_1.createRepo)(resolvedPath, {
706
564
  enableNetwork: true,
707
565
  syncServer: options.syncServer,
708
566
  syncServerStorageId: options.syncServerStorageId,
709
567
  });
710
- exports.ProgressMessages.repoCreated();
711
- exports.ProgressMessages.rootUrl(rootUrl);
712
- // Step 5: Initialize sync engine and pull existing structure
713
- spinner.text = "Downloading directory structure...";
714
- const syncEngine = new core_1.SyncEngine(repo, resolvedPath, config.defaults.exclude_patterns, true, // Network sync enabled for clone
715
- defaultStorageId);
716
- // Set the root directory URL to connect to the cloned repository
568
+ out.update("Downloading files");
569
+ const syncEngine = new core_1.SyncEngine(repo, resolvedPath, config.defaults.exclude_patterns, true, defaultStorageId);
717
570
  await syncEngine.setRootDirectoryUrl(rootUrl);
718
- // Sync to pull the existing directory structure and files
719
- const startTime = Date.now();
720
- await syncEngine.sync(false);
721
- const duration = Date.now() - startTime;
722
- console.log(chalk_1.default.gray(` ✓ Directory sync completed in ${duration}ms`));
723
- // Ensure all changes are flushed to disk
724
- spinner.text = "Flushing changes to disk...";
571
+ const result = await syncEngine.sync(false);
572
+ out.update("Writing to disk");
725
573
  await safeRepoShutdown(repo, "clone");
726
- exports.ProgressMessages.changesWritten();
727
- spinner.succeed(`Cloned sync directory to ${chalk_1.default.green(resolvedPath)}`);
728
- console.log(`\n${chalk_1.default.bold("📂 Directory Cloned!")}`);
729
- console.log(` 📁 Directory: ${chalk_1.default.blue(resolvedPath)}`);
730
- console.log(` 🔗 Root URL: ${chalk_1.default.cyan(rootUrl)}`);
731
- console.log(` 🔗 Sync server: ${chalk_1.default.blue(defaultSyncServer)}`);
732
- console.log(`\n${chalk_1.default.green("Clone complete!")} Run ${chalk_1.default.cyan("pushwork sync")} to stay in sync.`);
574
+ out.done();
575
+ out.pair("Path", resolvedPath);
576
+ out.pair("Files", `${result.filesChanged} downloaded`);
577
+ out.pair("Sync", defaultSyncServer);
578
+ out.success("CLONED", rootUrl);
733
579
  }
734
580
  catch (error) {
735
- spinner.fail(`Failed to clone: ${error}`);
736
- throw error;
581
+ out.error("FAILED", "Clone failed");
582
+ out.log(` ${error}`);
583
+ out.exit(1);
737
584
  }
585
+ process.exit();
738
586
  }
739
587
  /**
740
588
  * Get the root URL for the current pushwork repository
741
589
  */
742
- async function url(targetPath = ".") {
590
+ async function url(targetPath = ".", options = {}) {
591
+ const out = new output_1.Output();
743
592
  try {
744
593
  const resolvedPath = path.resolve(targetPath);
745
- // Check if initialized
746
594
  const syncToolDir = path.join(resolvedPath, ".pushwork");
747
595
  if (!(await (0, utils_1.pathExists)(syncToolDir))) {
748
- console.error(chalk_1.default.red("Directory not initialized for sync"));
749
- console.error(`Run ${chalk_1.default.cyan("pushwork init .")} to get started`);
750
- process.exit(1);
596
+ out.error("Directory not initialized for sync");
597
+ out.exit(1);
751
598
  }
752
- // Load the snapshot directly to get the URL without all the verbose output
753
599
  const snapshotPath = path.join(syncToolDir, "snapshot.json");
754
600
  if (!(await (0, utils_1.pathExists)(snapshotPath))) {
755
- console.error(chalk_1.default.red("No snapshot found"));
756
- console.error(chalk_1.default.gray("The repository may not be properly initialized"));
757
- process.exit(1);
601
+ out.error("No snapshot found");
602
+ out.exit(1);
758
603
  }
759
604
  const snapshotData = await fs.readFile(snapshotPath, "utf-8");
760
605
  const snapshot = JSON.parse(snapshotData);
@@ -763,141 +608,186 @@ async function url(targetPath = ".") {
763
608
  console.log(snapshot.rootDirectoryUrl);
764
609
  }
765
610
  else {
766
- console.error(chalk_1.default.red("No root URL found in snapshot"));
767
- console.error(chalk_1.default.gray("The repository may not be properly initialized"));
768
- process.exit(1);
611
+ out.error("No root URL found in snapshot");
612
+ out.exit(1);
769
613
  }
770
614
  }
771
615
  catch (error) {
772
- console.error(chalk_1.default.red(`Failed to get URL: ${error}`));
773
- process.exit(1);
616
+ out.error(`Failed to get URL: ${error}`);
617
+ out.exit(1);
774
618
  }
775
619
  }
776
- async function commit(targetPath, dryRun = false) {
777
- const spinner = (0, ora_1.default)("Starting commit operation...").start();
778
- let repo;
620
+ async function commit(targetPath, options = {}) {
621
+ const out = new output_1.Output();
779
622
  try {
780
- // Setup shared context with network disabled for local-only commit
781
- spinner.text = "Setting up commit context...";
782
- const context = await setupCommandContext(targetPath, undefined, undefined, false);
783
- repo = context.repo;
784
- const syncEngine = context.syncEngine;
785
- spinner.succeed("Connected to repository");
786
- // Run local commit only
787
- spinner.text = "Committing local changes...";
788
- const startTime = Date.now();
789
- const result = await syncEngine.commitLocal(dryRun);
790
- const duration = Date.now() - startTime;
791
- if (repo) {
792
- await safeRepoShutdown(repo, "commit");
793
- }
794
- spinner.succeed(`Commit completed in ${duration}ms`);
795
- // Display results
796
- console.log(chalk_1.default.green("\n✅ Commit Results:"));
797
- console.log(` 📄 Files committed: ${result.filesChanged}`);
798
- console.log(` 📁 Directories committed: ${result.directoriesChanged}`);
799
- console.log(` ⏱️ Total time: ${duration}ms`);
800
- if (result.warnings.length > 0) {
801
- console.log(chalk_1.default.yellow("\n⚠️ Warnings:"));
802
- result.warnings.forEach((warning) => console.log(chalk_1.default.yellow(` • ${warning}`)));
803
- }
623
+ out.task("Committing local changes");
624
+ const { repo, syncEngine } = await setupCommandContext(targetPath, undefined, undefined, false);
625
+ const result = await syncEngine.commitLocal(options.dryRun || false);
626
+ await safeRepoShutdown(repo, "commit");
627
+ out.done();
804
628
  if (result.errors.length > 0) {
805
- console.log(chalk_1.default.red("\n❌ Errors:"));
806
- result.errors.forEach((error) => console.log(chalk_1.default.red(` • ${error.operation} at ${error.path}: ${error.error.message}`)));
807
- process.exit(1);
629
+ out.error("ERRORS", `${result.errors.length} errors`);
630
+ result.errors.forEach((error) => {
631
+ out.log(` ${error.path}: ${error.error.message}`);
632
+ });
633
+ out.exit(1);
634
+ }
635
+ out.success("COMMITTED", `${result.filesChanged} files committed`);
636
+ out.pair("Files", result.filesChanged.toString());
637
+ out.pair("Directories", result.directoriesChanged.toString());
638
+ if (result.warnings.length > 0) {
639
+ out.log("");
640
+ out.warn("WARNINGS", `${result.warnings.length} warnings`);
641
+ result.warnings.forEach((warning) => out.log(` ${warning}`));
808
642
  }
809
- console.log(chalk_1.default.gray("\n💡 Run 'pushwork push' to upload to sync server"));
810
643
  }
811
644
  catch (error) {
812
- if (repo) {
813
- await safeRepoShutdown(repo, "commit-error");
814
- }
815
- spinner.fail(`Commit failed: ${error}`);
816
- console.error(chalk_1.default.red(`Error: ${error}`));
817
- process.exit(1);
645
+ out.error(`Commit failed: ${error}`);
646
+ out.exit(1);
818
647
  }
648
+ process.exit();
819
649
  }
820
650
  /**
821
651
  * Debug command to inspect internal document state
822
652
  */
823
653
  async function debug(targetPath = ".", options = {}) {
654
+ const out = new output_1.Output();
824
655
  try {
825
- const spinner = (0, ora_1.default)("Loading debug information...").start();
826
- // Setup shared context with network disabled for debug check
656
+ out.task("Loading debug info");
827
657
  const { repo, syncEngine, workingDir } = await setupCommandContext(targetPath, undefined, undefined, false);
828
658
  const debugStatus = await syncEngine.getStatus();
829
- spinner.stop();
830
- console.log(chalk_1.default.bold("🔍 Debug Information"));
831
- console.log(`${"=".repeat(50)}`);
832
- // Directory information
833
- console.log(`\n${chalk_1.default.bold("📁 Directory Information:")}`);
834
- console.log(` 📂 Path: ${chalk_1.default.blue(workingDir)}`);
835
- console.log(` 🔧 Config: ${path.join(workingDir, ".pushwork")}`);
659
+ out.done("done");
660
+ out.info("DEBUG", workingDir);
836
661
  if (debugStatus.snapshot?.rootDirectoryUrl) {
837
- console.log(`\n${chalk_1.default.bold("🗂️ Root Directory Document:")}`);
838
- console.log(` 🔗 URL: ${chalk_1.default.cyan(debugStatus.snapshot.rootDirectoryUrl)}`);
662
+ out.pair("URL", debugStatus.snapshot.rootDirectoryUrl);
839
663
  try {
840
664
  const rootHandle = await repo.find(debugStatus.snapshot.rootDirectoryUrl);
841
665
  const rootDoc = await rootHandle.doc();
842
666
  if (rootDoc) {
843
- console.log(` 📊 Document Structure:`);
844
- console.log(` 📄 Entries: ${rootDoc.docs.length}`);
845
- console.log(` 🏷️ Type: ${rootDoc["@patchwork"].type}`);
667
+ out.pair("Entries", rootDoc.docs.length.toString());
846
668
  if (rootDoc.lastSyncAt) {
847
669
  const lastSyncDate = new Date(rootDoc.lastSyncAt);
848
- console.log(` 🕒 Last Sync At: ${chalk_1.default.green(lastSyncDate.toISOString())}`);
849
- console.log(` 🕒 Last Sync Timestamp: ${chalk_1.default.gray(rootDoc.lastSyncAt)}`);
850
- }
851
- else {
852
- console.log(` 🕒 Last Sync At: ${chalk_1.default.yellow("Never set")}`);
670
+ out.pair("Last sync", lastSyncDate.toISOString());
853
671
  }
854
672
  if (options.verbose) {
855
- console.log(`\n 📋 Full Document Content:`);
856
- console.log(JSON.stringify(rootDoc, null, 2));
857
- console.log(`\n 🏷️ Document Heads:`);
858
- console.log(JSON.stringify(rootHandle.heads(), null, 2));
673
+ out.log("");
674
+ out.log("Document:");
675
+ out.log(JSON.stringify(rootDoc, null, 2));
676
+ out.log("");
677
+ out.log("Heads:");
678
+ out.log(JSON.stringify(rootHandle.heads(), null, 2));
859
679
  }
860
- console.log(`\n 📁 Directory Entries:`);
861
- rootDoc.docs.forEach((entry, index) => {
862
- console.log(` ${index + 1}. ${entry.name} (${entry.type}) -> ${entry.url}`);
863
- });
864
- }
865
- else {
866
- console.log(` ❌ Unable to load root document`);
867
680
  }
868
681
  }
869
682
  catch (error) {
870
- console.log(`Error loading root document: ${error}`);
683
+ out.warn(`Error loading root document: ${error}`);
871
684
  }
872
685
  }
873
- else {
874
- console.log(`\n${chalk_1.default.bold("🗂️ Root Directory Document:")}`);
875
- console.log(` ❌ No root directory URL set`);
876
- }
877
- // Snapshot information
878
686
  if (debugStatus.snapshot) {
879
- console.log(`\n${chalk_1.default.bold("📸 Snapshot Information:")}`);
880
- console.log(` 📄 Tracked files: ${debugStatus.snapshot.files.size}`);
881
- console.log(` 📁 Tracked directories: ${debugStatus.snapshot.directories.size}`);
882
- console.log(` 🏷️ Timestamp: ${new Date(debugStatus.snapshot.timestamp).toISOString()}`);
883
- console.log(` 📂 Root path: ${debugStatus.snapshot.rootPath}`);
687
+ out.pair("Files", debugStatus.snapshot.files.size.toString());
688
+ out.pair("Directories", debugStatus.snapshot.directories.size.toString());
884
689
  if (options.verbose) {
885
- console.log(`\n 📋 All Tracked Files:`);
886
- debugStatus.snapshot.files.forEach((entry, path) => {
887
- console.log(` ${path} -> ${entry.url}`);
888
- });
889
- console.log(`\n 📋 All Tracked Directories:`);
890
- debugStatus.snapshot.directories.forEach((entry, path) => {
891
- console.log(` ${path} -> ${entry.url}`);
690
+ out.log("");
691
+ out.log("All tracked files:");
692
+ debugStatus.snapshot.files.forEach((entry, filePath) => {
693
+ out.log(` ${filePath} -> ${entry.url}`);
892
694
  });
893
695
  }
894
696
  }
895
- // Cleanup repo resources
896
697
  await safeRepoShutdown(repo, "debug");
897
698
  }
898
699
  catch (error) {
899
- console.error(chalk_1.default.red(`Debug failed: ${error}`));
900
- throw error;
700
+ out.error(`Debug failed: ${error}`);
701
+ out.exit(1);
702
+ }
703
+ }
704
+ /**
705
+ * List tracked files
706
+ */
707
+ async function ls(targetPath = ".", options = {}) {
708
+ const out = new output_1.Output();
709
+ try {
710
+ const { repo, syncEngine } = await setupCommandContext(targetPath, undefined, undefined, false);
711
+ const syncStatus = await syncEngine.getStatus();
712
+ if (!syncStatus.snapshot) {
713
+ out.error("No snapshot found");
714
+ await safeRepoShutdown(repo, "ls");
715
+ out.exit(1);
716
+ return;
717
+ }
718
+ const files = Array.from(syncStatus.snapshot.files.entries()).sort(([pathA], [pathB]) => pathA.localeCompare(pathB));
719
+ if (files.length === 0) {
720
+ out.info("No tracked files");
721
+ await safeRepoShutdown(repo, "ls");
722
+ return;
723
+ }
724
+ if (options.long) {
725
+ // Long format with URLs
726
+ for (const [filePath, entry] of files) {
727
+ const url = entry?.url || "unknown";
728
+ console.log(`${filePath} -> ${url}`);
729
+ }
730
+ }
731
+ else {
732
+ // Simple list
733
+ for (const [filePath] of files) {
734
+ console.log(filePath);
735
+ }
736
+ }
737
+ await safeRepoShutdown(repo, "ls");
738
+ }
739
+ catch (error) {
740
+ out.error(`List failed: ${error}`);
741
+ out.exit(1);
742
+ }
743
+ }
744
+ /**
745
+ * View or edit configuration
746
+ */
747
+ async function config(targetPath = ".", options = {}) {
748
+ const out = new output_1.Output();
749
+ try {
750
+ const resolvedPath = path.resolve(targetPath);
751
+ const syncToolDir = path.join(resolvedPath, ".pushwork");
752
+ if (!(await (0, utils_1.pathExists)(syncToolDir))) {
753
+ out.error("Directory not initialized for sync");
754
+ out.exit(1);
755
+ }
756
+ const configManager = new config_1.ConfigManager(resolvedPath);
757
+ const config = await configManager.getMerged();
758
+ if (options.list) {
759
+ // List all configuration
760
+ out.info("CONFIGURATION", "Full configuration");
761
+ out.log(JSON.stringify(config, null, 2));
762
+ }
763
+ else if (options.get) {
764
+ // Get specific config value
765
+ const keys = options.get.split(".");
766
+ let value = config;
767
+ for (const key of keys) {
768
+ value = value?.[key];
769
+ }
770
+ if (value !== undefined) {
771
+ console.log(typeof value === "object" ? JSON.stringify(value, null, 2) : value);
772
+ }
773
+ else {
774
+ out.error(`Config key not found: ${options.get}`);
775
+ out.exit(1);
776
+ }
777
+ }
778
+ else {
779
+ // Show basic config info
780
+ out.info("CONFIGURATION", resolvedPath);
781
+ out.pair("Sync server", config.sync_server || "default");
782
+ out.pair("Sync enabled", config.sync_enabled ? "yes" : "no");
783
+ out.pair("Exclusions", config.defaults.exclude_patterns.length.toString());
784
+ out.log("");
785
+ out.log("Use --list to see full configuration");
786
+ }
787
+ }
788
+ catch (error) {
789
+ out.error(`Config failed: ${error}`);
790
+ out.exit(1);
901
791
  }
902
792
  }
903
793
  // TODO: Add push and pull commands later