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.
- package/README.md +8 -1
- package/bench/filesystem.bench.ts +78 -0
- package/bench/hashing.bench.ts +60 -0
- package/bench/move-detection.bench.ts +130 -0
- package/bench/runner.ts +49 -0
- package/dist/cli/commands.d.ts +15 -25
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +409 -519
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/output.d.ts +75 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +182 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli.js +119 -51
- package/dist/cli.js.map +1 -1
- package/dist/config/remote-manager.d.ts +65 -0
- package/dist/config/remote-manager.d.ts.map +1 -0
- package/dist/config/remote-manager.js +243 -0
- package/dist/config/remote-manager.js.map +1 -0
- package/dist/core/change-detection.d.ts +8 -0
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +63 -0
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/move-detection.d.ts +9 -48
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +53 -135
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +17 -85
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/types/config.d.ts +45 -5
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/documents.d.ts +0 -1
- package/dist/types/documents.d.ts.map +1 -1
- package/dist/types/snapshot.d.ts +3 -0
- package/dist/types/snapshot.d.ts.map +1 -1
- package/dist/types/snapshot.js.map +1 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +9 -33
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +0 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +18 -9
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.d.ts +14 -0
- package/dist/utils/string-similarity.d.ts.map +1 -0
- package/dist/utils/string-similarity.js +43 -0
- package/dist/utils/string-similarity.js.map +1 -0
- package/package.json +10 -5
- package/src/cli/commands.ts +520 -697
- package/src/cli/output.ts +244 -0
- package/src/cli.ts +182 -73
- package/src/core/change-detection.ts +95 -0
- package/src/core/move-detection.ts +69 -177
- package/src/core/sync-engine.ts +17 -105
- package/src/types/config.ts +50 -7
- package/src/types/documents.ts +0 -1
- package/src/types/snapshot.ts +1 -0
- package/src/utils/fs.ts +9 -33
- package/src/utils/index.ts +0 -1
- package/src/utils/repo-factory.ts +21 -8
- package/src/utils/string-similarity.ts +54 -0
- package/src/utils/content-similarity.ts +0 -194
- package/test/unit/content-similarity.test.ts +0 -236
package/dist/cli/commands.js
CHANGED
|
@@ -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,
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
160
|
+
out.error("Directory already initialized for sync");
|
|
161
|
+
out.exit(1);
|
|
199
162
|
}
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
|
234
|
+
async function sync(targetPath = ".", options) {
|
|
235
|
+
const out = new output_1.Output();
|
|
289
236
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
out.info("No changes detected");
|
|
245
|
+
out.log("Everything is already in sync");
|
|
314
246
|
return;
|
|
315
247
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
268
|
+
out.log("");
|
|
269
|
+
out.log("Moves:");
|
|
350
270
|
for (const move of preview.moves.slice(0, 5)) {
|
|
351
|
-
|
|
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
|
-
|
|
274
|
+
out.log(` ... and ${preview.moves.length - 5} more`);
|
|
361
275
|
}
|
|
362
276
|
}
|
|
363
|
-
|
|
277
|
+
out.log("");
|
|
278
|
+
out.log("Run without --dry-run to apply these changes");
|
|
364
279
|
}
|
|
365
280
|
else {
|
|
366
|
-
|
|
367
|
-
spinner.text = "Detecting changes...";
|
|
368
|
-
const startTime = Date.now();
|
|
281
|
+
out.update("Synchronizing");
|
|
369
282
|
const result = await syncEngine.sync(false);
|
|
370
|
-
|
|
283
|
+
out.update("Writing to disk");
|
|
284
|
+
await safeRepoShutdown(repo, "sync");
|
|
371
285
|
if (result.success) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
296
|
+
out.log("");
|
|
297
|
+
out.warn("WARNINGS", `${result.warnings.length} warnings`);
|
|
379
298
|
for (const warning of result.warnings.slice(0, 5)) {
|
|
380
|
-
|
|
299
|
+
out.log(` ${warning}`);
|
|
381
300
|
}
|
|
382
301
|
if (result.warnings.length > 5) {
|
|
383
|
-
|
|
302
|
+
out.log(` ... and ${result.warnings.length - 5} more`);
|
|
384
303
|
}
|
|
385
304
|
}
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
+
out.success("No changes detected");
|
|
357
|
+
await safeRepoShutdown(repo, "diff");
|
|
358
|
+
out.exit();
|
|
441
359
|
return;
|
|
442
360
|
}
|
|
443
|
-
|
|
361
|
+
out.warn(`${preview.changes.length} changes detected`);
|
|
444
362
|
for (const change of preview.changes) {
|
|
445
|
-
const
|
|
446
|
-
?
|
|
363
|
+
const prefix = change.changeType === "local_only"
|
|
364
|
+
? "[local] "
|
|
447
365
|
: change.changeType === "remote_only"
|
|
448
|
-
?
|
|
449
|
-
:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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
|
-
|
|
665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
756
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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
|
-
|
|
773
|
-
|
|
616
|
+
out.error(`Failed to get URL: ${error}`);
|
|
617
|
+
out.exit(1);
|
|
774
618
|
}
|
|
775
619
|
}
|
|
776
|
-
async function commit(targetPath,
|
|
777
|
-
const
|
|
778
|
-
let repo;
|
|
620
|
+
async function commit(targetPath, options = {}) {
|
|
621
|
+
const out = new output_1.Output();
|
|
779
622
|
try {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
repo
|
|
784
|
-
|
|
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
|
-
|
|
806
|
-
result.errors.forEach((error) =>
|
|
807
|
-
|
|
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
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
900
|
-
|
|
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
|