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/src/cli/commands.ts
CHANGED
|
@@ -1,24 +1,59 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
|
-
import { Repo,
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import ora from "ora";
|
|
3
|
+
import { Repo, AutomergeUrl } from "@automerge/automerge-repo";
|
|
6
4
|
import * as diffLib from "diff";
|
|
7
5
|
import {
|
|
8
|
-
InitOptions,
|
|
9
6
|
CloneOptions,
|
|
10
7
|
SyncOptions,
|
|
11
8
|
DiffOptions,
|
|
12
9
|
LogOptions,
|
|
13
10
|
CheckoutOptions,
|
|
11
|
+
InitOptions,
|
|
12
|
+
CommitOptions,
|
|
13
|
+
StatusOptions,
|
|
14
|
+
UrlOptions,
|
|
15
|
+
ListOptions,
|
|
16
|
+
ConfigOptions,
|
|
17
|
+
DebugOptions,
|
|
14
18
|
DirectoryConfig,
|
|
15
19
|
DirectoryDocument,
|
|
16
20
|
} from "../types";
|
|
17
21
|
import { SyncEngine } from "../core";
|
|
18
|
-
import { DetectedChange } from "../core/change-detection";
|
|
19
22
|
import { pathExists, ensureDirectoryExists } from "../utils";
|
|
20
23
|
import { ConfigManager } from "../config";
|
|
21
24
|
import { createRepo } from "../utils/repo-factory";
|
|
25
|
+
import { Output } from "./output";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simple key transformation for debug output: snake_case -> Title Case
|
|
29
|
+
*/
|
|
30
|
+
function prettifyKey(key: string): string {
|
|
31
|
+
return (
|
|
32
|
+
key
|
|
33
|
+
.split("_")
|
|
34
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
35
|
+
.join(" ") + ":"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format timing value with percentage and optional metadata
|
|
41
|
+
*/
|
|
42
|
+
function formatTimingValue(
|
|
43
|
+
value: any,
|
|
44
|
+
key: string,
|
|
45
|
+
total: number,
|
|
46
|
+
timings?: Record<string, any>
|
|
47
|
+
): string {
|
|
48
|
+
// Skip non-timing values
|
|
49
|
+
if (key === "documents_to_sync") return "";
|
|
50
|
+
if (key === "total") return `${(value / 1000).toFixed(3)}s`;
|
|
51
|
+
|
|
52
|
+
const timeStr = `${(value / 1000).toFixed(3)}s`;
|
|
53
|
+
const pctStr = `(${((value / total) * 100).toFixed(1)}%)`;
|
|
54
|
+
|
|
55
|
+
return `${timeStr} ${pctStr}`;
|
|
56
|
+
}
|
|
22
57
|
|
|
23
58
|
/**
|
|
24
59
|
* Shared context that commands can use
|
|
@@ -30,6 +65,29 @@ export interface CommandContext {
|
|
|
30
65
|
workingDir: string;
|
|
31
66
|
}
|
|
32
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Validate that sync server options are used together
|
|
70
|
+
*/
|
|
71
|
+
function validateSyncServerOptions(
|
|
72
|
+
syncServer?: string,
|
|
73
|
+
syncServerStorageId?: string
|
|
74
|
+
): void {
|
|
75
|
+
const hasSyncServer = !!syncServer;
|
|
76
|
+
const hasSyncServerStorageId = !!syncServerStorageId;
|
|
77
|
+
|
|
78
|
+
if (hasSyncServer && !hasSyncServerStorageId) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"--sync-server requires --sync-server-storage-id\nBoth arguments must be provided together."
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hasSyncServerStorageId && !hasSyncServer) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"--sync-server-storage-id requires --sync-server\nBoth arguments must be provided together."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
33
91
|
/**
|
|
34
92
|
* Shared pre-action that ensures repository and sync engine are properly initialized
|
|
35
93
|
* This function always works, with or without network connectivity
|
|
@@ -114,131 +172,41 @@ export async function safeRepoShutdown(
|
|
|
114
172
|
}
|
|
115
173
|
}
|
|
116
174
|
|
|
117
|
-
/**
|
|
118
|
-
* Common progress message helpers
|
|
119
|
-
*/
|
|
120
|
-
export const ProgressMessages = {
|
|
121
|
-
// Setup messages
|
|
122
|
-
directoryFound: () => console.log(chalk.gray(" ✓ Sync directory found")),
|
|
123
|
-
configLoaded: () => console.log(chalk.gray(" ✓ Configuration loaded")),
|
|
124
|
-
repoConnected: () => console.log(chalk.gray(" ✓ Connected to repository")),
|
|
125
|
-
|
|
126
|
-
// Configuration display
|
|
127
|
-
syncServer: (server: string) =>
|
|
128
|
-
console.log(chalk.gray(` ✓ Sync server: ${server}`)),
|
|
129
|
-
storageId: (id: string) => console.log(chalk.gray(` ✓ Storage ID: ${id}`)),
|
|
130
|
-
rootUrl: (url: string) => console.log(chalk.gray(` ✓ Root URL: ${url}`)),
|
|
131
|
-
|
|
132
|
-
// Operation completion
|
|
133
|
-
changesWritten: () =>
|
|
134
|
-
console.log(chalk.gray(" ✓ All changes written to disk")),
|
|
135
|
-
syncCompleted: (duration: number) =>
|
|
136
|
-
console.log(chalk.gray(` ✓ Initial sync completed in ${duration}ms`)),
|
|
137
|
-
directoryStructureCreated: () =>
|
|
138
|
-
console.log(chalk.gray(" ✓ Created sync directory structure")),
|
|
139
|
-
configSaved: () => console.log(chalk.gray(" ✓ Saved configuration")),
|
|
140
|
-
repoCreated: () =>
|
|
141
|
-
console.log(chalk.gray(" ✓ Created Automerge repository")),
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Show actual content diff for a changed file
|
|
146
|
-
*/
|
|
147
|
-
async function showContentDiff(change: DetectedChange): Promise<void> {
|
|
148
|
-
try {
|
|
149
|
-
// Get old content (from snapshot/remote)
|
|
150
|
-
const oldContent = change.remoteContent || "";
|
|
151
|
-
|
|
152
|
-
// Get new content (current local)
|
|
153
|
-
const newContent = change.localContent || "";
|
|
154
|
-
|
|
155
|
-
// Convert binary content to string representation if needed
|
|
156
|
-
const oldText =
|
|
157
|
-
typeof oldContent === "string"
|
|
158
|
-
? oldContent
|
|
159
|
-
: `<binary content: ${oldContent.length} bytes>`;
|
|
160
|
-
const newText =
|
|
161
|
-
typeof newContent === "string"
|
|
162
|
-
? newContent
|
|
163
|
-
: `<binary content: ${newContent.length} bytes>`;
|
|
164
|
-
|
|
165
|
-
// Generate unified diff
|
|
166
|
-
const diffResult = diffLib.createPatch(
|
|
167
|
-
change.path,
|
|
168
|
-
oldText,
|
|
169
|
-
newText,
|
|
170
|
-
"previous",
|
|
171
|
-
"current"
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Skip the header lines and process the diff
|
|
175
|
-
const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
|
|
176
|
-
|
|
177
|
-
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
178
|
-
console.log(chalk.gray(" (content identical)"));
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const line of lines) {
|
|
183
|
-
if (line.startsWith("@@")) {
|
|
184
|
-
// Hunk header
|
|
185
|
-
console.log(chalk.cyan(line));
|
|
186
|
-
} else if (line.startsWith("+")) {
|
|
187
|
-
// Added line
|
|
188
|
-
console.log(chalk.green(line));
|
|
189
|
-
} else if (line.startsWith("-")) {
|
|
190
|
-
// Removed line
|
|
191
|
-
console.log(chalk.red(line));
|
|
192
|
-
} else if (line.startsWith(" ")) {
|
|
193
|
-
// Context line
|
|
194
|
-
console.log(chalk.gray(line));
|
|
195
|
-
} else if (line === "") {
|
|
196
|
-
// Empty line
|
|
197
|
-
console.log("");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch (error) {
|
|
201
|
-
console.log(chalk.gray(` (diff error: ${error})`));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
175
|
/**
|
|
206
176
|
* Initialize sync in a directory
|
|
207
177
|
*/
|
|
208
178
|
export async function init(
|
|
209
179
|
targetPath: string,
|
|
210
|
-
|
|
211
|
-
syncServerStorageId?: string
|
|
180
|
+
options: InitOptions = {}
|
|
212
181
|
): Promise<void> {
|
|
213
|
-
|
|
182
|
+
// Validate sync server options
|
|
183
|
+
validateSyncServerOptions(options.syncServer, options.syncServerStorageId);
|
|
184
|
+
|
|
185
|
+
const out = new Output();
|
|
214
186
|
|
|
215
187
|
try {
|
|
216
188
|
const resolvedPath = path.resolve(targetPath);
|
|
217
189
|
|
|
218
|
-
|
|
219
|
-
|
|
190
|
+
out.task(`Initializing ${resolvedPath}`);
|
|
191
|
+
|
|
220
192
|
await ensureDirectoryExists(resolvedPath);
|
|
221
193
|
|
|
222
194
|
// Check if already initialized
|
|
223
195
|
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
224
196
|
if (await pathExists(syncToolDir)) {
|
|
225
|
-
|
|
226
|
-
|
|
197
|
+
out.error("Directory already initialized for sync");
|
|
198
|
+
out.exit(1);
|
|
227
199
|
}
|
|
228
200
|
|
|
229
|
-
|
|
230
|
-
spinner.text = "Creating .pushwork directory...";
|
|
201
|
+
out.update("Creating sync directory");
|
|
231
202
|
await ensureDirectoryExists(syncToolDir);
|
|
232
203
|
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
233
204
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Step 3: Configuration setup
|
|
237
|
-
spinner.text = "Setting up configuration...";
|
|
205
|
+
out.update("Setting up configuration");
|
|
238
206
|
const configManager = new ConfigManager(resolvedPath);
|
|
239
|
-
const defaultSyncServer = syncServer || "wss://sync3.automerge.org";
|
|
207
|
+
const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
|
|
240
208
|
const defaultStorageId =
|
|
241
|
-
syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
209
|
+
options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
242
210
|
const config: DirectoryConfig = {
|
|
243
211
|
sync_server: defaultSyncServer,
|
|
244
212
|
sync_server_storage_id: defaultStorageId,
|
|
@@ -259,278 +227,201 @@ export async function init(
|
|
|
259
227
|
};
|
|
260
228
|
await configManager.save(config);
|
|
261
229
|
|
|
262
|
-
|
|
263
|
-
ProgressMessages.syncServer(defaultSyncServer);
|
|
264
|
-
ProgressMessages.storageId(defaultStorageId);
|
|
265
|
-
|
|
266
|
-
// Step 4: Initialize Automerge repo and create root directory document
|
|
267
|
-
spinner.text = "Creating root directory document...";
|
|
230
|
+
out.update("Creating root directory");
|
|
268
231
|
const repo = await createRepo(resolvedPath, {
|
|
269
232
|
enableNetwork: true,
|
|
270
|
-
syncServer: syncServer,
|
|
271
|
-
syncServerStorageId: syncServerStorageId,
|
|
233
|
+
syncServer: options.syncServer,
|
|
234
|
+
syncServerStorageId: options.syncServerStorageId,
|
|
272
235
|
});
|
|
273
236
|
|
|
274
|
-
// Create the root directory document
|
|
275
237
|
const rootDoc: DirectoryDocument = {
|
|
276
238
|
"@patchwork": { type: "folder" },
|
|
277
239
|
docs: [],
|
|
278
240
|
};
|
|
279
241
|
const rootHandle = repo.create(rootDoc);
|
|
280
242
|
|
|
281
|
-
|
|
282
|
-
ProgressMessages.rootUrl(rootHandle.url);
|
|
283
|
-
|
|
284
|
-
// Step 5: Scan existing files
|
|
285
|
-
spinner.text = "Scanning existing files...";
|
|
243
|
+
out.update("Scanning existing files");
|
|
286
244
|
const syncEngine = new SyncEngine(
|
|
287
245
|
repo,
|
|
288
246
|
resolvedPath,
|
|
289
247
|
config.defaults.exclude_patterns,
|
|
290
|
-
true,
|
|
248
|
+
true,
|
|
291
249
|
config.sync_server_storage_id
|
|
292
250
|
);
|
|
293
251
|
|
|
294
|
-
// Get file count for progress
|
|
295
|
-
const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
296
|
-
const fileCount = dirEntries.filter((dirent: any) =>
|
|
297
|
-
dirent.isFile()
|
|
298
|
-
).length;
|
|
299
|
-
|
|
300
|
-
if (fileCount > 0) {
|
|
301
|
-
console.log(chalk.gray(` ✓ Found ${fileCount} existing files`));
|
|
302
|
-
spinner.text = `Creating initial snapshot with ${fileCount} files...`;
|
|
303
|
-
} else {
|
|
304
|
-
spinner.text = "Creating initial empty snapshot...";
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Step 6: Set the root directory URL before creating initial snapshot
|
|
308
252
|
await syncEngine.setRootDirectoryUrl(rootHandle.url);
|
|
253
|
+
const result = await syncEngine.sync(false);
|
|
309
254
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const startTime = Date.now();
|
|
313
|
-
await syncEngine.sync(false);
|
|
314
|
-
const duration = Date.now() - startTime;
|
|
255
|
+
out.update("Writing to disk");
|
|
256
|
+
await safeRepoShutdown(repo, "init");
|
|
315
257
|
|
|
316
|
-
|
|
258
|
+
out.done();
|
|
317
259
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
260
|
+
out.pair("Sync", defaultSyncServer);
|
|
261
|
+
if (result.filesChanged > 0) {
|
|
262
|
+
out.pair("Files", `${result.filesChanged} added`);
|
|
263
|
+
}
|
|
264
|
+
out.success("INITIALIZED", rootHandle.url);
|
|
322
265
|
|
|
323
|
-
|
|
266
|
+
// Show timing breakdown if debug mode is enabled
|
|
267
|
+
if (
|
|
268
|
+
options.debug &&
|
|
269
|
+
result.timings &&
|
|
270
|
+
Object.keys(result.timings).length > 0
|
|
271
|
+
) {
|
|
272
|
+
const total = result.timings.total || 0;
|
|
273
|
+
out.log("");
|
|
274
|
+
out.special("TIMING", "");
|
|
275
|
+
out.obj(result.timings, prettifyKey, (value, key) =>
|
|
276
|
+
formatTimingValue(value, key, total, result.timings)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
324
279
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
console.log(` 🔗 Sync server: ${chalk.blue(defaultSyncServer)}`);
|
|
328
|
-
console.log(
|
|
329
|
-
`\n${chalk.green("Initialization complete!")} Run ${chalk.cyan(
|
|
330
|
-
"pushwork sync"
|
|
331
|
-
)} to start syncing.`
|
|
332
|
-
);
|
|
280
|
+
out.log("");
|
|
281
|
+
out.log(`Run 'pushwork sync' to start synchronizing`);
|
|
333
282
|
} catch (error) {
|
|
334
|
-
|
|
335
|
-
|
|
283
|
+
out.error("FAILED", "Initialization failed");
|
|
284
|
+
out.log(` ${error}`);
|
|
285
|
+
out.exit(1);
|
|
336
286
|
}
|
|
287
|
+
process.exit();
|
|
337
288
|
}
|
|
338
289
|
|
|
339
290
|
/**
|
|
340
291
|
* Run bidirectional sync
|
|
341
292
|
*/
|
|
342
|
-
export async function sync(
|
|
343
|
-
|
|
293
|
+
export async function sync(
|
|
294
|
+
targetPath = ".",
|
|
295
|
+
options: SyncOptions
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const out = new Output();
|
|
344
298
|
|
|
345
299
|
try {
|
|
346
|
-
|
|
347
|
-
spinner.text = "Setting up sync context...";
|
|
348
|
-
const { repo, syncEngine, config, workingDir } =
|
|
349
|
-
await setupCommandContext();
|
|
350
|
-
|
|
351
|
-
ProgressMessages.directoryFound();
|
|
352
|
-
ProgressMessages.configLoaded();
|
|
353
|
-
ProgressMessages.syncServer(
|
|
354
|
-
config?.sync_server || "wss://sync3.automerge.org"
|
|
355
|
-
);
|
|
356
|
-
ProgressMessages.repoConnected();
|
|
300
|
+
out.task("Syncing");
|
|
357
301
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
ProgressMessages.rootUrl(syncStatus.snapshot.rootDirectoryUrl);
|
|
362
|
-
}
|
|
302
|
+
const { repo, syncEngine, workingDir } = await setupCommandContext(
|
|
303
|
+
targetPath
|
|
304
|
+
);
|
|
363
305
|
|
|
364
306
|
if (options.dryRun) {
|
|
365
|
-
|
|
366
|
-
spinner.text = "Analyzing changes (dry run)...";
|
|
367
|
-
const startTime = Date.now();
|
|
307
|
+
out.update("Analyzing changes");
|
|
368
308
|
const preview = await syncEngine.previewChanges();
|
|
369
|
-
const analysisTime = Date.now() - startTime;
|
|
370
|
-
|
|
371
|
-
spinner.succeed("Change analysis completed");
|
|
372
309
|
|
|
373
|
-
|
|
374
|
-
console.log(chalk.gray(` Directory: ${workingDir}`));
|
|
375
|
-
console.log(chalk.gray(` Analysis time: ${analysisTime}ms`));
|
|
310
|
+
out.done();
|
|
376
311
|
|
|
377
312
|
if (preview.changes.length === 0 && preview.moves.length === 0) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
);
|
|
313
|
+
out.info("No changes detected");
|
|
314
|
+
out.log("Everything is already in sync");
|
|
381
315
|
return;
|
|
382
316
|
}
|
|
383
317
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (preview.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
c.changeType === "local_only" || c.changeType === "both_changed"
|
|
391
|
-
).length;
|
|
392
|
-
const remoteChanges = preview.changes.filter(
|
|
393
|
-
(c) =>
|
|
394
|
-
c.changeType === "remote_only" || c.changeType === "both_changed"
|
|
395
|
-
).length;
|
|
396
|
-
const conflicts = preview.changes.filter(
|
|
397
|
-
(c) => c.changeType === "both_changed"
|
|
398
|
-
).length;
|
|
399
|
-
|
|
400
|
-
console.log(
|
|
401
|
-
`\n${chalk.bold("📁 File Changes:")} (${
|
|
402
|
-
preview.changes.length
|
|
403
|
-
} total)`
|
|
404
|
-
);
|
|
405
|
-
if (localChanges > 0) {
|
|
406
|
-
console.log(` ${chalk.green("📤")} Local changes: ${localChanges}`);
|
|
407
|
-
}
|
|
408
|
-
if (remoteChanges > 0) {
|
|
409
|
-
console.log(` ${chalk.blue("📥")} Remote changes: ${remoteChanges}`);
|
|
410
|
-
}
|
|
411
|
-
if (conflicts > 0) {
|
|
412
|
-
console.log(` ${chalk.yellow("⚠️")} Conflicts: ${conflicts}`);
|
|
413
|
-
}
|
|
318
|
+
out.info("CHANGES", "Pending");
|
|
319
|
+
out.pair("Directory", workingDir);
|
|
320
|
+
out.pair("Changes", preview.changes.length.toString());
|
|
321
|
+
if (preview.moves.length > 0) {
|
|
322
|
+
out.pair("Moves", preview.moves.length.toString());
|
|
323
|
+
}
|
|
414
324
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
if (preview.changes.length > 10) {
|
|
429
|
-
console.log(
|
|
430
|
-
` ${chalk.gray(
|
|
431
|
-
`... and ${preview.changes.length - 10} more files`
|
|
432
|
-
)}`
|
|
433
|
-
);
|
|
434
|
-
}
|
|
325
|
+
out.log("");
|
|
326
|
+
out.log("Files:");
|
|
327
|
+
for (const change of preview.changes.slice(0, 10)) {
|
|
328
|
+
const prefix =
|
|
329
|
+
change.changeType === "local_only"
|
|
330
|
+
? "[local] "
|
|
331
|
+
: change.changeType === "remote_only"
|
|
332
|
+
? "[remote] "
|
|
333
|
+
: "[conflict]";
|
|
334
|
+
out.log(` ${prefix} ${change.path}`);
|
|
335
|
+
}
|
|
336
|
+
if (preview.changes.length > 10) {
|
|
337
|
+
out.log(` ... and ${preview.changes.length - 10} more`);
|
|
435
338
|
}
|
|
436
339
|
|
|
437
340
|
if (preview.moves.length > 0) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
341
|
+
out.log("");
|
|
342
|
+
out.log("Moves:");
|
|
441
343
|
for (const move of preview.moves.slice(0, 5)) {
|
|
442
|
-
|
|
443
|
-
const confidence =
|
|
444
|
-
move.confidence === "auto"
|
|
445
|
-
? chalk.green("Auto")
|
|
446
|
-
: move.confidence === "prompt"
|
|
447
|
-
? chalk.yellow("Prompt")
|
|
448
|
-
: chalk.red("Low");
|
|
449
|
-
console.log(` 🔄 ${move.fromPath} → ${move.toPath} (${confidence})`);
|
|
344
|
+
out.log(` ${move.fromPath} → ${move.toPath}`);
|
|
450
345
|
}
|
|
451
346
|
if (preview.moves.length > 5) {
|
|
452
|
-
|
|
453
|
-
` ${chalk.gray(`... and ${preview.moves.length - 5} more moves`)}`
|
|
454
|
-
);
|
|
347
|
+
out.log(` ... and ${preview.moves.length - 5} more`);
|
|
455
348
|
}
|
|
456
349
|
}
|
|
457
350
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
);
|
|
351
|
+
out.log("");
|
|
352
|
+
out.log("Run without --dry-run to apply these changes");
|
|
461
353
|
} else {
|
|
462
|
-
|
|
463
|
-
spinner.text = "Detecting changes...";
|
|
464
|
-
const startTime = Date.now();
|
|
465
|
-
|
|
354
|
+
out.update("Synchronizing");
|
|
466
355
|
const result = await syncEngine.sync(false);
|
|
467
|
-
const totalTime = Date.now() - startTime;
|
|
468
356
|
|
|
469
|
-
|
|
470
|
-
|
|
357
|
+
out.update("Writing to disk");
|
|
358
|
+
await safeRepoShutdown(repo, "sync");
|
|
471
359
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
360
|
+
if (result.success) {
|
|
361
|
+
if (result.filesChanged === 0 && result.directoriesChanged === 0) {
|
|
362
|
+
out.done();
|
|
363
|
+
out.success("Already in sync");
|
|
364
|
+
} else {
|
|
365
|
+
out.done();
|
|
366
|
+
out.success(
|
|
367
|
+
"SYNCED",
|
|
368
|
+
`${result.filesChanged} file${
|
|
369
|
+
result.filesChanged !== 1 ? "s" : ""
|
|
370
|
+
} updated`
|
|
371
|
+
);
|
|
372
|
+
out.pair("Files", result.filesChanged.toString());
|
|
373
|
+
}
|
|
478
374
|
|
|
479
375
|
if (result.warnings.length > 0) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
);
|
|
376
|
+
out.log("");
|
|
377
|
+
out.warn("WARNINGS", `${result.warnings.length} warnings`);
|
|
483
378
|
for (const warning of result.warnings.slice(0, 5)) {
|
|
484
|
-
|
|
379
|
+
out.log(` ${warning}`);
|
|
485
380
|
}
|
|
486
381
|
if (result.warnings.length > 5) {
|
|
487
|
-
|
|
488
|
-
` ${chalk.gray(
|
|
489
|
-
`... and ${result.warnings.length - 5} more warnings`
|
|
490
|
-
)}`
|
|
491
|
-
);
|
|
382
|
+
out.log(` ... and ${result.warnings.length - 5} more`);
|
|
492
383
|
}
|
|
493
384
|
}
|
|
494
385
|
|
|
495
|
-
|
|
496
|
-
|
|
386
|
+
// Show timing breakdown if debug mode is enabled
|
|
387
|
+
if (
|
|
388
|
+
options.debug &&
|
|
389
|
+
result.timings &&
|
|
390
|
+
Object.keys(result.timings).length > 0
|
|
391
|
+
) {
|
|
392
|
+
const total = result.timings.total || 0;
|
|
393
|
+
out.log("");
|
|
394
|
+
out.special("TIMING", "");
|
|
395
|
+
out.obj(result.timings, prettifyKey, (value, key) =>
|
|
396
|
+
formatTimingValue(value, key, total, result.timings)
|
|
397
|
+
);
|
|
497
398
|
}
|
|
498
|
-
|
|
499
|
-
// Ensure all changes are flushed to disk
|
|
500
|
-
spinner.text = "Flushing changes to disk...";
|
|
501
|
-
await safeRepoShutdown(repo, "sync");
|
|
502
|
-
ProgressMessages.changesWritten();
|
|
503
399
|
} else {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
400
|
+
out.done("partial", false);
|
|
401
|
+
out.warn(
|
|
402
|
+
"PARTIAL",
|
|
403
|
+
`${result.filesChanged} updated, ${result.errors.length} errors`
|
|
508
404
|
);
|
|
405
|
+
out.pair("Files", result.filesChanged.toString());
|
|
406
|
+
out.pair("Errors", result.errors.length.toString());
|
|
407
|
+
|
|
408
|
+
out.log("");
|
|
509
409
|
for (const error of result.errors.slice(0, 5)) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
);
|
|
410
|
+
out.error("ERROR", error.path);
|
|
411
|
+
out.log(` ${error.error.message}`);
|
|
412
|
+
out.log("");
|
|
513
413
|
}
|
|
514
414
|
if (result.errors.length > 5) {
|
|
515
|
-
|
|
516
|
-
` ${chalk.gray(`... and ${result.errors.length - 5} more errors`)}`
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (result.filesChanged > 0 || result.directoriesChanged > 0) {
|
|
521
|
-
console.log(`\n${chalk.yellow("⚠️ Partial sync completed:")}`);
|
|
522
|
-
console.log(` 📄 Files changed: ${result.filesChanged}`);
|
|
523
|
-
console.log(` 📁 Directories changed: ${result.directoriesChanged}`);
|
|
415
|
+
out.log(`... and ${result.errors.length - 5} more errors`);
|
|
524
416
|
}
|
|
525
|
-
|
|
526
|
-
// Still try to flush any partial changes
|
|
527
|
-
await safeRepoShutdown(repo, "sync-error");
|
|
528
417
|
}
|
|
529
418
|
}
|
|
530
419
|
} catch (error) {
|
|
531
|
-
|
|
532
|
-
|
|
420
|
+
out.error("FAILED", "Sync failed");
|
|
421
|
+
out.log(` ${error}`);
|
|
422
|
+
out.exit(1);
|
|
533
423
|
}
|
|
424
|
+
process.exit();
|
|
534
425
|
}
|
|
535
426
|
|
|
536
427
|
/**
|
|
@@ -540,8 +431,11 @@ export async function diff(
|
|
|
540
431
|
targetPath = ".",
|
|
541
432
|
options: DiffOptions
|
|
542
433
|
): Promise<void> {
|
|
434
|
+
const out = new Output();
|
|
435
|
+
|
|
543
436
|
try {
|
|
544
|
-
|
|
437
|
+
out.task("Analyzing changes");
|
|
438
|
+
|
|
545
439
|
const { repo, syncEngine } = await setupCommandContext(
|
|
546
440
|
targetPath,
|
|
547
441
|
undefined,
|
|
@@ -550,214 +444,153 @@ export async function diff(
|
|
|
550
444
|
);
|
|
551
445
|
const preview = await syncEngine.previewChanges();
|
|
552
446
|
|
|
447
|
+
out.done();
|
|
448
|
+
|
|
553
449
|
if (options.nameOnly) {
|
|
554
|
-
// Show only file names
|
|
555
450
|
for (const change of preview.changes) {
|
|
556
451
|
console.log(change.path);
|
|
557
452
|
}
|
|
558
453
|
return;
|
|
559
454
|
}
|
|
560
455
|
|
|
561
|
-
// Show root directory URL for context
|
|
562
|
-
const diffStatus = await syncEngine.getStatus();
|
|
563
|
-
if (diffStatus.snapshot?.rootDirectoryUrl) {
|
|
564
|
-
console.log(
|
|
565
|
-
chalk.gray(`Root URL: ${diffStatus.snapshot.rootDirectoryUrl}`)
|
|
566
|
-
);
|
|
567
|
-
console.log("");
|
|
568
|
-
}
|
|
569
|
-
|
|
570
456
|
if (preview.changes.length === 0) {
|
|
571
|
-
|
|
457
|
+
out.success("No changes detected");
|
|
458
|
+
await safeRepoShutdown(repo, "diff");
|
|
459
|
+
out.exit();
|
|
572
460
|
return;
|
|
573
461
|
}
|
|
574
462
|
|
|
575
|
-
|
|
463
|
+
out.warn(`${preview.changes.length} changes detected`);
|
|
576
464
|
|
|
577
465
|
for (const change of preview.changes) {
|
|
578
|
-
const
|
|
466
|
+
const prefix =
|
|
579
467
|
change.changeType === "local_only"
|
|
580
|
-
?
|
|
468
|
+
? "[local] "
|
|
581
469
|
: change.changeType === "remote_only"
|
|
582
|
-
?
|
|
583
|
-
:
|
|
584
|
-
|
|
585
|
-
|
|
470
|
+
? "[remote] "
|
|
471
|
+
: "[conflict]";
|
|
472
|
+
|
|
473
|
+
if (!options.tool) {
|
|
474
|
+
try {
|
|
475
|
+
// Get old content (from snapshot/remote)
|
|
476
|
+
const oldContent = change.remoteContent || "";
|
|
477
|
+
// Get new content (current local)
|
|
478
|
+
const newContent = change.localContent || "";
|
|
479
|
+
|
|
480
|
+
// Convert binary content to string representation if needed
|
|
481
|
+
const oldText =
|
|
482
|
+
typeof oldContent === "string"
|
|
483
|
+
? oldContent
|
|
484
|
+
: `<binary content: ${oldContent.length} bytes>`;
|
|
485
|
+
const newText =
|
|
486
|
+
typeof newContent === "string"
|
|
487
|
+
? newContent
|
|
488
|
+
: `<binary content: ${newContent.length} bytes>`;
|
|
489
|
+
|
|
490
|
+
// Generate unified diff
|
|
491
|
+
const diffResult = diffLib.createPatch(
|
|
492
|
+
change.path,
|
|
493
|
+
oldText,
|
|
494
|
+
newText,
|
|
495
|
+
"previous",
|
|
496
|
+
"current"
|
|
497
|
+
);
|
|
586
498
|
|
|
587
|
-
|
|
499
|
+
// Skip the header lines and process the diff
|
|
500
|
+
const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
|
|
501
|
+
|
|
502
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
503
|
+
out.log(`${prefix}${change.path} (content identical)`, "cyan");
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
588
506
|
|
|
589
|
-
|
|
590
|
-
|
|
507
|
+
// Extract first hunk header and show inline with path
|
|
508
|
+
let firstHunk = "";
|
|
509
|
+
let diffLines = lines;
|
|
510
|
+
if (lines[0]?.startsWith("@@")) {
|
|
511
|
+
firstHunk = ` ${lines[0]}`;
|
|
512
|
+
diffLines = lines.slice(1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
out.log(`${prefix}${change.path}${firstHunk}`, "cyan");
|
|
516
|
+
|
|
517
|
+
for (const line of diffLines) {
|
|
518
|
+
if (line.startsWith("@@")) {
|
|
519
|
+
// Additional hunk headers
|
|
520
|
+
out.log(line, "dim");
|
|
521
|
+
} else if (line.startsWith("+")) {
|
|
522
|
+
// Added line
|
|
523
|
+
out.log(line, "green");
|
|
524
|
+
} else if (line.startsWith("-")) {
|
|
525
|
+
// Removed line
|
|
526
|
+
out.log(line, "red");
|
|
527
|
+
} else if (line.startsWith(" ") || line === "") {
|
|
528
|
+
// Context line or empty
|
|
529
|
+
out.log(line, "dim");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
out.log(`${prefix}${change.path} (diff error: ${error})`, "cyan");
|
|
534
|
+
}
|
|
591
535
|
} else {
|
|
592
|
-
|
|
593
|
-
await showContentDiff(change);
|
|
536
|
+
out.log(`${prefix} ${change.path}`);
|
|
594
537
|
}
|
|
595
538
|
}
|
|
596
539
|
|
|
597
|
-
// Cleanup repo resources
|
|
598
540
|
await safeRepoShutdown(repo, "diff");
|
|
599
541
|
} catch (error) {
|
|
600
|
-
|
|
601
|
-
|
|
542
|
+
out.error(`Diff failed: ${error}`);
|
|
543
|
+
out.exit(1);
|
|
602
544
|
}
|
|
603
545
|
}
|
|
604
546
|
|
|
605
547
|
/**
|
|
606
548
|
* Show sync status
|
|
607
549
|
*/
|
|
608
|
-
export async function status(
|
|
550
|
+
export async function status(
|
|
551
|
+
targetPath: string = ".",
|
|
552
|
+
options: StatusOptions = {}
|
|
553
|
+
): Promise<void> {
|
|
554
|
+
const out = new Output();
|
|
555
|
+
|
|
609
556
|
try {
|
|
610
|
-
|
|
557
|
+
out.task("Loading status");
|
|
611
558
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
process.cwd(),
|
|
559
|
+
const { repo, syncEngine, workingDir, config } = await setupCommandContext(
|
|
560
|
+
targetPath,
|
|
615
561
|
undefined,
|
|
616
562
|
undefined,
|
|
617
563
|
false
|
|
618
564
|
);
|
|
619
565
|
const syncStatus = await syncEngine.getStatus();
|
|
620
566
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
console.log(chalk.bold("📊 Sync Status Report"));
|
|
624
|
-
console.log(`${"=".repeat(50)}`);
|
|
567
|
+
out.done();
|
|
625
568
|
|
|
626
|
-
|
|
627
|
-
console.log(`\n${chalk.bold("📁 Directory Information:")}`);
|
|
628
|
-
console.log(` 📂 Path: ${chalk.blue(workingDir)}`);
|
|
629
|
-
console.log(` 🔧 Config: ${path.join(workingDir, ".pushwork")}`);
|
|
569
|
+
out.info("STATUS", workingDir);
|
|
630
570
|
|
|
631
|
-
// Show root directory URL if available
|
|
632
571
|
if (syncStatus.snapshot?.rootDirectoryUrl) {
|
|
633
|
-
|
|
634
|
-
` 🔗 Root URL: ${chalk.cyan(syncStatus.snapshot.rootDirectoryUrl)}`
|
|
635
|
-
);
|
|
636
|
-
|
|
637
|
-
// Try to show lastSyncAt from root directory document
|
|
638
|
-
try {
|
|
639
|
-
const rootHandle = await repo.find<DirectoryDocument>(
|
|
640
|
-
syncStatus.snapshot.rootDirectoryUrl
|
|
641
|
-
);
|
|
642
|
-
const rootDoc = await rootHandle.doc();
|
|
643
|
-
if (rootDoc?.lastSyncAt) {
|
|
644
|
-
const lastSyncDate = new Date(rootDoc.lastSyncAt);
|
|
645
|
-
const timeSince = Date.now() - rootDoc.lastSyncAt;
|
|
646
|
-
const timeAgo =
|
|
647
|
-
timeSince < 60000
|
|
648
|
-
? `${Math.floor(timeSince / 1000)}s ago`
|
|
649
|
-
: timeSince < 3600000
|
|
650
|
-
? `${Math.floor(timeSince / 60000)}m ago`
|
|
651
|
-
: `${Math.floor(timeSince / 3600000)}h ago`;
|
|
652
|
-
console.log(
|
|
653
|
-
` 🕒 Root last touched: ${chalk.green(
|
|
654
|
-
lastSyncDate.toLocaleString()
|
|
655
|
-
)} (${chalk.gray(timeAgo)})`
|
|
656
|
-
);
|
|
657
|
-
} else {
|
|
658
|
-
console.log(` 🕒 Root last touched: ${chalk.yellow("Never")}`);
|
|
659
|
-
}
|
|
660
|
-
} catch (error) {
|
|
661
|
-
console.log(
|
|
662
|
-
` 🕒 Root last touched: ${chalk.gray("Unable to determine")}`
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
} else {
|
|
666
|
-
console.log(` 🔗 Root URL: ${chalk.yellow("Not set")}`);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Sync timing
|
|
670
|
-
if (syncStatus.lastSync) {
|
|
671
|
-
const timeSince = Date.now() - syncStatus.lastSync.getTime();
|
|
672
|
-
const timeAgo =
|
|
673
|
-
timeSince < 60000
|
|
674
|
-
? `${Math.floor(timeSince / 1000)}s ago`
|
|
675
|
-
: timeSince < 3600000
|
|
676
|
-
? `${Math.floor(timeSince / 60000)}m ago`
|
|
677
|
-
: `${Math.floor(timeSince / 3600000)}h ago`;
|
|
678
|
-
|
|
679
|
-
console.log(`\n${chalk.bold("⏱️ Sync Timing:")}`);
|
|
680
|
-
console.log(
|
|
681
|
-
` 🕐 Last sync: ${chalk.green(syncStatus.lastSync.toLocaleString())}`
|
|
682
|
-
);
|
|
683
|
-
console.log(` ⏳ Time since: ${chalk.gray(timeAgo)}`);
|
|
684
|
-
} else {
|
|
685
|
-
console.log(`\n${chalk.bold("⏱️ Sync Timing:")}`);
|
|
686
|
-
console.log(` 🕐 Last sync: ${chalk.yellow("Never synced")}`);
|
|
687
|
-
console.log(
|
|
688
|
-
` 💡 Run ${chalk.cyan("pushwork sync")} to perform initial sync`
|
|
689
|
-
);
|
|
572
|
+
out.pair("URL", syncStatus.snapshot.rootDirectoryUrl);
|
|
690
573
|
}
|
|
691
574
|
|
|
692
|
-
// Change status
|
|
693
|
-
console.log(`\n${chalk.bold("📝 Change Status:")}`);
|
|
694
|
-
if (syncStatus.hasChanges) {
|
|
695
|
-
console.log(
|
|
696
|
-
` 📄 Pending changes: ${chalk.yellow(syncStatus.changeCount)}`
|
|
697
|
-
);
|
|
698
|
-
console.log(` 🔄 Status: ${chalk.yellow("Sync needed")}`);
|
|
699
|
-
console.log(` 💡 Run ${chalk.cyan("pushwork diff")} to see details`);
|
|
700
|
-
} else {
|
|
701
|
-
console.log(` 📄 Pending changes: ${chalk.green("None")}`);
|
|
702
|
-
console.log(` ✅ Status: ${chalk.green("Up to date")}`);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Configuration
|
|
706
|
-
console.log(`\n${chalk.bold("⚙️ Configuration:")}`);
|
|
707
|
-
|
|
708
|
-
const statusConfigManager2 = new ConfigManager(workingDir);
|
|
709
|
-
const statusConfig2 = await statusConfigManager2.load();
|
|
710
|
-
|
|
711
|
-
if (statusConfig2?.sync_server) {
|
|
712
|
-
console.log(` 🔗 Sync server: ${chalk.blue(statusConfig2.sync_server)}`);
|
|
713
|
-
} else {
|
|
714
|
-
console.log(
|
|
715
|
-
` 🔗 Sync server: ${chalk.blue("wss://sync3.automerge.org")} (default)`
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
console.log(
|
|
720
|
-
` ⚡ Auto sync: ${
|
|
721
|
-
statusConfig2?.sync?.auto_sync
|
|
722
|
-
? chalk.green("Enabled")
|
|
723
|
-
: chalk.gray("Disabled")
|
|
724
|
-
}`
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
// Snapshot information
|
|
728
575
|
if (syncStatus.snapshot) {
|
|
729
576
|
const fileCount = syncStatus.snapshot.files.size;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
console.log(`\n${chalk.bold("📊 Repository Statistics:")}`);
|
|
733
|
-
console.log(` 📄 Tracked files: ${chalk.yellow(fileCount)}`);
|
|
734
|
-
console.log(` 📁 Tracked directories: ${chalk.yellow(dirCount)}`);
|
|
735
|
-
console.log(
|
|
736
|
-
` 🏷️ Snapshot timestamp: ${chalk.gray(
|
|
737
|
-
new Date(syncStatus.snapshot.timestamp).toLocaleString()
|
|
738
|
-
)}`
|
|
739
|
-
);
|
|
577
|
+
out.pair("Files", `${fileCount} tracked`);
|
|
740
578
|
}
|
|
741
579
|
|
|
742
|
-
|
|
743
|
-
|
|
580
|
+
out.pair("Sync", config?.sync_server || "wss://sync3.automerge.org");
|
|
581
|
+
|
|
744
582
|
if (syncStatus.hasChanges) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
748
|
-
console.log(` ${chalk.cyan("pushwork sync")} - Apply changes`);
|
|
583
|
+
out.pair("Changes", `${syncStatus.changeCount} pending`);
|
|
584
|
+
out.log("");
|
|
585
|
+
out.log("Run 'pushwork diff' to see changes");
|
|
749
586
|
} else {
|
|
750
|
-
|
|
751
|
-
` ${chalk.cyan("pushwork sync")} - Check for remote changes`
|
|
752
|
-
);
|
|
587
|
+
out.pair("Status", "up to date");
|
|
753
588
|
}
|
|
754
|
-
console.log(` ${chalk.cyan("pushwork log")} - View sync history`);
|
|
755
589
|
|
|
756
|
-
// Cleanup repo resources
|
|
757
590
|
await safeRepoShutdown(repo, "status");
|
|
758
591
|
} catch (error) {
|
|
759
|
-
|
|
760
|
-
|
|
592
|
+
out.error(`Status check failed: ${error}`);
|
|
593
|
+
out.exit(1);
|
|
761
594
|
}
|
|
762
595
|
}
|
|
763
596
|
|
|
@@ -768,47 +601,30 @@ export async function log(
|
|
|
768
601
|
targetPath = ".",
|
|
769
602
|
options: LogOptions
|
|
770
603
|
): Promise<void> {
|
|
771
|
-
|
|
772
|
-
// Setup shared context with network disabled for log check
|
|
773
|
-
const {
|
|
774
|
-
repo: logRepo,
|
|
775
|
-
syncEngine: logSyncEngine,
|
|
776
|
-
workingDir,
|
|
777
|
-
} = await setupCommandContext(targetPath, undefined, undefined, false);
|
|
778
|
-
const logStatus = await logSyncEngine.getStatus();
|
|
779
|
-
|
|
780
|
-
if (logStatus.snapshot?.rootDirectoryUrl) {
|
|
781
|
-
console.log(
|
|
782
|
-
chalk.gray(`Root URL: ${logStatus.snapshot.rootDirectoryUrl}`)
|
|
783
|
-
);
|
|
784
|
-
console.log("");
|
|
785
|
-
}
|
|
604
|
+
const out = new Output();
|
|
786
605
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
606
|
+
try {
|
|
607
|
+
const { repo: logRepo, workingDir } = await setupCommandContext(
|
|
608
|
+
targetPath,
|
|
609
|
+
undefined,
|
|
610
|
+
undefined,
|
|
611
|
+
false
|
|
612
|
+
);
|
|
791
613
|
|
|
792
|
-
//
|
|
614
|
+
// TODO: Implement history tracking
|
|
793
615
|
const snapshotPath = path.join(workingDir, ".pushwork", "snapshot.json");
|
|
794
616
|
if (await pathExists(snapshotPath)) {
|
|
795
617
|
const stats = await fs.stat(snapshotPath);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
console.log(`${stats.mtime.toISOString()} - Last sync`);
|
|
799
|
-
} else {
|
|
800
|
-
console.log(`Last sync: ${chalk.green(stats.mtime.toISOString())}`);
|
|
801
|
-
console.log(`Snapshot size: ${stats.size} bytes`);
|
|
802
|
-
}
|
|
618
|
+
out.info("HISTORY", "Sync history (stub)");
|
|
619
|
+
out.pair("Last sync", stats.mtime.toISOString());
|
|
803
620
|
} else {
|
|
804
|
-
|
|
621
|
+
out.info("No sync history found");
|
|
805
622
|
}
|
|
806
623
|
|
|
807
|
-
// Cleanup repo resources
|
|
808
624
|
await safeRepoShutdown(logRepo, "log");
|
|
809
625
|
} catch (error) {
|
|
810
|
-
|
|
811
|
-
|
|
626
|
+
out.error(`Log failed: ${error}`);
|
|
627
|
+
out.exit(1);
|
|
812
628
|
}
|
|
813
629
|
}
|
|
814
630
|
|
|
@@ -820,22 +636,18 @@ export async function checkout(
|
|
|
820
636
|
targetPath = ".",
|
|
821
637
|
options: CheckoutOptions
|
|
822
638
|
): Promise<void> {
|
|
639
|
+
const out = new Output();
|
|
640
|
+
|
|
823
641
|
try {
|
|
824
|
-
// Setup shared context
|
|
825
642
|
const { workingDir } = await setupCommandContext(targetPath);
|
|
826
643
|
|
|
827
644
|
// TODO: Implement checkout functionality
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
// 3. Updating the snapshot
|
|
832
|
-
|
|
833
|
-
console.log(chalk.yellow(`Checkout functionality not yet implemented`));
|
|
834
|
-
console.log(`Would restore to sync: ${syncId}`);
|
|
835
|
-
console.log(`Target path: ${workingDir}`);
|
|
645
|
+
out.warn("NOT IMPLEMENTED", "Checkout not yet implemented");
|
|
646
|
+
out.pair("Sync ID", syncId);
|
|
647
|
+
out.pair("Path", workingDir);
|
|
836
648
|
} catch (error) {
|
|
837
|
-
|
|
838
|
-
|
|
649
|
+
out.error(`Checkout failed: ${error}`);
|
|
650
|
+
out.exit(1);
|
|
839
651
|
}
|
|
840
652
|
}
|
|
841
653
|
|
|
@@ -847,22 +659,22 @@ export async function clone(
|
|
|
847
659
|
targetPath: string,
|
|
848
660
|
options: CloneOptions
|
|
849
661
|
): Promise<void> {
|
|
850
|
-
|
|
662
|
+
// Validate sync server options
|
|
663
|
+
validateSyncServerOptions(options.syncServer, options.syncServerStorageId);
|
|
664
|
+
|
|
665
|
+
const out = new Output();
|
|
851
666
|
|
|
852
667
|
try {
|
|
853
668
|
const resolvedPath = path.resolve(targetPath);
|
|
854
669
|
|
|
855
|
-
|
|
856
|
-
spinner.text = "Setting up target directory...";
|
|
670
|
+
out.task(`Cloning ${rootUrl}`);
|
|
857
671
|
|
|
858
672
|
// Check if directory exists and handle --force
|
|
859
673
|
if (await pathExists(resolvedPath)) {
|
|
860
674
|
const files = await fs.readdir(resolvedPath);
|
|
861
675
|
if (files.length > 0 && !options.force) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
);
|
|
865
|
-
return;
|
|
676
|
+
out.error("Target directory is not empty. Use --force to overwrite");
|
|
677
|
+
out.exit(1);
|
|
866
678
|
}
|
|
867
679
|
} else {
|
|
868
680
|
await ensureDirectoryExists(resolvedPath);
|
|
@@ -872,26 +684,17 @@ export async function clone(
|
|
|
872
684
|
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
873
685
|
if (await pathExists(syncToolDir)) {
|
|
874
686
|
if (!options.force) {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
);
|
|
878
|
-
return;
|
|
687
|
+
out.error("Directory already initialized. Use --force to overwrite");
|
|
688
|
+
out.exit(1);
|
|
879
689
|
}
|
|
880
|
-
// Clean up existing sync directory
|
|
881
690
|
await fs.rm(syncToolDir, { recursive: true, force: true });
|
|
882
691
|
}
|
|
883
692
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
// Step 2: Create sync directories
|
|
887
|
-
spinner.text = "Creating .pushwork directory...";
|
|
693
|
+
out.update("Creating sync directory");
|
|
888
694
|
await ensureDirectoryExists(syncToolDir);
|
|
889
695
|
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
890
696
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// Step 3: Configuration setup
|
|
894
|
-
spinner.text = "Setting up configuration...";
|
|
697
|
+
out.update("Setting up configuration");
|
|
895
698
|
const configManager = new ConfigManager(resolvedPath);
|
|
896
699
|
const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
|
|
897
700
|
const defaultStorageId =
|
|
@@ -916,86 +719,64 @@ export async function clone(
|
|
|
916
719
|
};
|
|
917
720
|
await configManager.save(config);
|
|
918
721
|
|
|
919
|
-
|
|
920
|
-
ProgressMessages.syncServer(defaultSyncServer);
|
|
921
|
-
ProgressMessages.storageId(defaultStorageId);
|
|
922
|
-
|
|
923
|
-
// Step 4: Initialize Automerge repo and connect to root directory
|
|
924
|
-
spinner.text = "Connecting to root directory document...";
|
|
722
|
+
out.update("Connecting to sync server");
|
|
925
723
|
const repo = await createRepo(resolvedPath, {
|
|
926
724
|
enableNetwork: true,
|
|
927
725
|
syncServer: options.syncServer,
|
|
928
726
|
syncServerStorageId: options.syncServerStorageId,
|
|
929
727
|
});
|
|
930
728
|
|
|
931
|
-
|
|
932
|
-
ProgressMessages.rootUrl(rootUrl);
|
|
933
|
-
|
|
934
|
-
// Step 5: Initialize sync engine and pull existing structure
|
|
935
|
-
spinner.text = "Downloading directory structure...";
|
|
729
|
+
out.update("Downloading files");
|
|
936
730
|
const syncEngine = new SyncEngine(
|
|
937
731
|
repo,
|
|
938
732
|
resolvedPath,
|
|
939
733
|
config.defaults.exclude_patterns,
|
|
940
|
-
true,
|
|
734
|
+
true,
|
|
941
735
|
defaultStorageId
|
|
942
736
|
);
|
|
943
737
|
|
|
944
|
-
// Set the root directory URL to connect to the cloned repository
|
|
945
738
|
await syncEngine.setRootDirectoryUrl(rootUrl as AutomergeUrl);
|
|
739
|
+
const result = await syncEngine.sync(false);
|
|
946
740
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
await syncEngine.sync(false);
|
|
950
|
-
const duration = Date.now() - startTime;
|
|
741
|
+
out.update("Writing to disk");
|
|
742
|
+
await safeRepoShutdown(repo, "clone");
|
|
951
743
|
|
|
952
|
-
|
|
744
|
+
out.done();
|
|
953
745
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
spinner.succeed(`Cloned sync directory to ${chalk.green(resolvedPath)}`);
|
|
960
|
-
|
|
961
|
-
console.log(`\n${chalk.bold("📂 Directory Cloned!")}`);
|
|
962
|
-
console.log(` 📁 Directory: ${chalk.blue(resolvedPath)}`);
|
|
963
|
-
console.log(` 🔗 Root URL: ${chalk.cyan(rootUrl)}`);
|
|
964
|
-
console.log(` 🔗 Sync server: ${chalk.blue(defaultSyncServer)}`);
|
|
965
|
-
console.log(
|
|
966
|
-
`\n${chalk.green("Clone complete!")} Run ${chalk.cyan(
|
|
967
|
-
"pushwork sync"
|
|
968
|
-
)} to stay in sync.`
|
|
969
|
-
);
|
|
746
|
+
out.pair("Path", resolvedPath);
|
|
747
|
+
out.pair("Files", `${result.filesChanged} downloaded`);
|
|
748
|
+
out.pair("Sync", defaultSyncServer);
|
|
749
|
+
out.success("CLONED", rootUrl);
|
|
970
750
|
} catch (error) {
|
|
971
|
-
|
|
972
|
-
|
|
751
|
+
out.error("FAILED", "Clone failed");
|
|
752
|
+
out.log(` ${error}`);
|
|
753
|
+
out.exit(1);
|
|
973
754
|
}
|
|
755
|
+
process.exit();
|
|
974
756
|
}
|
|
975
757
|
|
|
976
758
|
/**
|
|
977
759
|
* Get the root URL for the current pushwork repository
|
|
978
760
|
*/
|
|
979
|
-
export async function url(
|
|
761
|
+
export async function url(
|
|
762
|
+
targetPath: string = ".",
|
|
763
|
+
options: UrlOptions = {}
|
|
764
|
+
): Promise<void> {
|
|
765
|
+
const out = new Output();
|
|
766
|
+
|
|
980
767
|
try {
|
|
981
768
|
const resolvedPath = path.resolve(targetPath);
|
|
982
|
-
|
|
983
|
-
// Check if initialized
|
|
984
769
|
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
770
|
+
|
|
985
771
|
if (!(await pathExists(syncToolDir))) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
process.exit(1);
|
|
772
|
+
out.error("Directory not initialized for sync");
|
|
773
|
+
out.exit(1);
|
|
989
774
|
}
|
|
990
775
|
|
|
991
|
-
// Load the snapshot directly to get the URL without all the verbose output
|
|
992
776
|
const snapshotPath = path.join(syncToolDir, "snapshot.json");
|
|
993
777
|
if (!(await pathExists(snapshotPath))) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
chalk.gray("The repository may not be properly initialized")
|
|
997
|
-
);
|
|
998
|
-
process.exit(1);
|
|
778
|
+
out.error("No snapshot found");
|
|
779
|
+
out.exit(1);
|
|
999
780
|
}
|
|
1000
781
|
|
|
1001
782
|
const snapshotData = await fs.readFile(snapshotPath, "utf-8");
|
|
@@ -1005,98 +786,72 @@ export async function url(targetPath = "."): Promise<void> {
|
|
|
1005
786
|
// Output just the URL for easy use in scripts
|
|
1006
787
|
console.log(snapshot.rootDirectoryUrl);
|
|
1007
788
|
} else {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
chalk.gray("The repository may not be properly initialized")
|
|
1011
|
-
);
|
|
1012
|
-
process.exit(1);
|
|
789
|
+
out.error("No root URL found in snapshot");
|
|
790
|
+
out.exit(1);
|
|
1013
791
|
}
|
|
1014
792
|
} catch (error) {
|
|
1015
|
-
|
|
1016
|
-
|
|
793
|
+
out.error(`Failed to get URL: ${error}`);
|
|
794
|
+
out.exit(1);
|
|
1017
795
|
}
|
|
1018
796
|
}
|
|
1019
797
|
|
|
1020
798
|
export async function commit(
|
|
1021
799
|
targetPath: string,
|
|
1022
|
-
|
|
800
|
+
options: CommitOptions = {}
|
|
1023
801
|
): Promise<void> {
|
|
1024
|
-
const
|
|
1025
|
-
let repo: Repo | undefined;
|
|
802
|
+
const out = new Output();
|
|
1026
803
|
|
|
1027
804
|
try {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
805
|
+
out.task("Committing local changes");
|
|
806
|
+
|
|
807
|
+
const { repo, syncEngine } = await setupCommandContext(
|
|
1031
808
|
targetPath,
|
|
1032
809
|
undefined,
|
|
1033
810
|
undefined,
|
|
1034
811
|
false
|
|
1035
812
|
);
|
|
1036
|
-
repo = context.repo;
|
|
1037
|
-
const syncEngine = context.syncEngine;
|
|
1038
|
-
spinner.succeed("Connected to repository");
|
|
1039
|
-
|
|
1040
|
-
// Run local commit only
|
|
1041
|
-
spinner.text = "Committing local changes...";
|
|
1042
|
-
const startTime = Date.now();
|
|
1043
|
-
const result = await syncEngine.commitLocal(dryRun);
|
|
1044
|
-
const duration = Date.now() - startTime;
|
|
1045
|
-
|
|
1046
|
-
if (repo) {
|
|
1047
|
-
await safeRepoShutdown(repo, "commit");
|
|
1048
|
-
}
|
|
1049
|
-
spinner.succeed(`Commit completed in ${duration}ms`);
|
|
1050
813
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
console.log(` 📄 Files committed: ${result.filesChanged}`);
|
|
1054
|
-
console.log(` 📁 Directories committed: ${result.directoriesChanged}`);
|
|
1055
|
-
console.log(` ⏱️ Total time: ${duration}ms`);
|
|
814
|
+
const result = await syncEngine.commitLocal(options.dryRun || false);
|
|
815
|
+
await safeRepoShutdown(repo, "commit");
|
|
1056
816
|
|
|
1057
|
-
|
|
1058
|
-
console.log(chalk.yellow("\n⚠️ Warnings:"));
|
|
1059
|
-
result.warnings.forEach((warning: string) =>
|
|
1060
|
-
console.log(chalk.yellow(` • ${warning}`))
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
817
|
+
out.done();
|
|
1063
818
|
|
|
1064
819
|
if (result.errors.length > 0) {
|
|
1065
|
-
|
|
1066
|
-
result.errors.forEach((error) =>
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
)
|
|
1071
|
-
)
|
|
1072
|
-
);
|
|
1073
|
-
process.exit(1);
|
|
820
|
+
out.error("ERRORS", `${result.errors.length} errors`);
|
|
821
|
+
result.errors.forEach((error) => {
|
|
822
|
+
out.log(` ${error.path}: ${error.error.message}`);
|
|
823
|
+
});
|
|
824
|
+
out.exit(1);
|
|
1074
825
|
}
|
|
1075
826
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
);
|
|
1079
|
-
|
|
1080
|
-
if (
|
|
1081
|
-
|
|
827
|
+
out.success("COMMITTED", `${result.filesChanged} files committed`);
|
|
828
|
+
out.pair("Files", result.filesChanged.toString());
|
|
829
|
+
out.pair("Directories", result.directoriesChanged.toString());
|
|
830
|
+
|
|
831
|
+
if (result.warnings.length > 0) {
|
|
832
|
+
out.log("");
|
|
833
|
+
out.warn("WARNINGS", `${result.warnings.length} warnings`);
|
|
834
|
+
result.warnings.forEach((warning: string) => out.log(` ${warning}`));
|
|
1082
835
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
836
|
+
} catch (error) {
|
|
837
|
+
out.error(`Commit failed: ${error}`);
|
|
838
|
+
out.exit(1);
|
|
1086
839
|
}
|
|
840
|
+
process.exit();
|
|
1087
841
|
}
|
|
1088
842
|
|
|
1089
843
|
/**
|
|
1090
844
|
* Debug command to inspect internal document state
|
|
1091
845
|
*/
|
|
1092
846
|
export async function debug(
|
|
1093
|
-
targetPath = ".",
|
|
1094
|
-
options:
|
|
847
|
+
targetPath: string = ".",
|
|
848
|
+
options: DebugOptions = {}
|
|
1095
849
|
): Promise<void> {
|
|
850
|
+
const out = new Output();
|
|
851
|
+
|
|
1096
852
|
try {
|
|
1097
|
-
|
|
853
|
+
out.task("Loading debug info");
|
|
1098
854
|
|
|
1099
|
-
// Setup shared context with network disabled for debug check
|
|
1100
855
|
const { repo, syncEngine, workingDir } = await setupCommandContext(
|
|
1101
856
|
targetPath,
|
|
1102
857
|
undefined,
|
|
@@ -1105,21 +860,12 @@ export async function debug(
|
|
|
1105
860
|
);
|
|
1106
861
|
const debugStatus = await syncEngine.getStatus();
|
|
1107
862
|
|
|
1108
|
-
|
|
863
|
+
out.done("done");
|
|
1109
864
|
|
|
1110
|
-
|
|
1111
|
-
console.log(`${"=".repeat(50)}`);
|
|
1112
|
-
|
|
1113
|
-
// Directory information
|
|
1114
|
-
console.log(`\n${chalk.bold("📁 Directory Information:")}`);
|
|
1115
|
-
console.log(` 📂 Path: ${chalk.blue(workingDir)}`);
|
|
1116
|
-
console.log(` 🔧 Config: ${path.join(workingDir, ".pushwork")}`);
|
|
865
|
+
out.info("DEBUG", workingDir);
|
|
1117
866
|
|
|
1118
867
|
if (debugStatus.snapshot?.rootDirectoryUrl) {
|
|
1119
|
-
|
|
1120
|
-
console.log(
|
|
1121
|
-
` 🔗 URL: ${chalk.cyan(debugStatus.snapshot.rootDirectoryUrl)}`
|
|
1122
|
-
);
|
|
868
|
+
out.pair("URL", debugStatus.snapshot.rootDirectoryUrl);
|
|
1123
869
|
|
|
1124
870
|
try {
|
|
1125
871
|
const rootHandle = await repo.find<DirectoryDocument>(
|
|
@@ -1128,79 +874,156 @@ export async function debug(
|
|
|
1128
874
|
const rootDoc = await rootHandle.doc();
|
|
1129
875
|
|
|
1130
876
|
if (rootDoc) {
|
|
1131
|
-
|
|
1132
|
-
console.log(` 📄 Entries: ${rootDoc.docs.length}`);
|
|
1133
|
-
console.log(` 🏷️ Type: ${rootDoc["@patchwork"].type}`);
|
|
1134
|
-
|
|
877
|
+
out.pair("Entries", rootDoc.docs.length.toString());
|
|
1135
878
|
if (rootDoc.lastSyncAt) {
|
|
1136
879
|
const lastSyncDate = new Date(rootDoc.lastSyncAt);
|
|
1137
|
-
|
|
1138
|
-
` 🕒 Last Sync At: ${chalk.green(lastSyncDate.toISOString())}`
|
|
1139
|
-
);
|
|
1140
|
-
console.log(
|
|
1141
|
-
` 🕒 Last Sync Timestamp: ${chalk.gray(rootDoc.lastSyncAt)}`
|
|
1142
|
-
);
|
|
1143
|
-
} else {
|
|
1144
|
-
console.log(` 🕒 Last Sync At: ${chalk.yellow("Never set")}`);
|
|
880
|
+
out.pair("Last sync", lastSyncDate.toISOString());
|
|
1145
881
|
}
|
|
1146
882
|
|
|
1147
883
|
if (options.verbose) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
884
|
+
out.log("");
|
|
885
|
+
out.log("Document:");
|
|
886
|
+
out.log(JSON.stringify(rootDoc, null, 2));
|
|
887
|
+
out.log("");
|
|
888
|
+
out.log("Heads:");
|
|
889
|
+
out.log(JSON.stringify(rootHandle.heads(), null, 2));
|
|
1153
890
|
}
|
|
1154
|
-
|
|
1155
|
-
console.log(`\n 📁 Directory Entries:`);
|
|
1156
|
-
rootDoc.docs.forEach((entry: any, index: number) => {
|
|
1157
|
-
console.log(
|
|
1158
|
-
` ${index + 1}. ${entry.name} (${entry.type}) -> ${entry.url}`
|
|
1159
|
-
);
|
|
1160
|
-
});
|
|
1161
|
-
} else {
|
|
1162
|
-
console.log(` ❌ Unable to load root document`);
|
|
1163
891
|
}
|
|
1164
892
|
} catch (error) {
|
|
1165
|
-
|
|
893
|
+
out.warn(`Error loading root document: ${error}`);
|
|
1166
894
|
}
|
|
1167
|
-
} else {
|
|
1168
|
-
console.log(`\n${chalk.bold("🗂️ Root Directory Document:")}`);
|
|
1169
|
-
console.log(` ❌ No root directory URL set`);
|
|
1170
895
|
}
|
|
1171
896
|
|
|
1172
|
-
// Snapshot information
|
|
1173
897
|
if (debugStatus.snapshot) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
console.log(
|
|
1177
|
-
` 📁 Tracked directories: ${debugStatus.snapshot.directories.size}`
|
|
1178
|
-
);
|
|
1179
|
-
console.log(
|
|
1180
|
-
` 🏷️ Timestamp: ${new Date(
|
|
1181
|
-
debugStatus.snapshot.timestamp
|
|
1182
|
-
).toISOString()}`
|
|
1183
|
-
);
|
|
1184
|
-
console.log(` 📂 Root path: ${debugStatus.snapshot.rootPath}`);
|
|
898
|
+
out.pair("Files", debugStatus.snapshot.files.size.toString());
|
|
899
|
+
out.pair("Directories", debugStatus.snapshot.directories.size.toString());
|
|
1185
900
|
|
|
1186
901
|
if (options.verbose) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
console.log(`\n 📋 All Tracked Directories:`);
|
|
1193
|
-
debugStatus.snapshot.directories.forEach((entry, path) => {
|
|
1194
|
-
console.log(` ${path} -> ${entry.url}`);
|
|
902
|
+
out.log("");
|
|
903
|
+
out.log("All tracked files:");
|
|
904
|
+
debugStatus.snapshot.files.forEach((entry, filePath) => {
|
|
905
|
+
out.log(` ${filePath} -> ${entry.url}`);
|
|
1195
906
|
});
|
|
1196
907
|
}
|
|
1197
908
|
}
|
|
1198
909
|
|
|
1199
|
-
// Cleanup repo resources
|
|
1200
910
|
await safeRepoShutdown(repo, "debug");
|
|
1201
911
|
} catch (error) {
|
|
1202
|
-
|
|
1203
|
-
|
|
912
|
+
out.error(`Debug failed: ${error}`);
|
|
913
|
+
out.exit(1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* List tracked files
|
|
919
|
+
*/
|
|
920
|
+
export async function ls(
|
|
921
|
+
targetPath: string = ".",
|
|
922
|
+
options: ListOptions = {}
|
|
923
|
+
): Promise<void> {
|
|
924
|
+
const out = new Output();
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const { repo, syncEngine } = await setupCommandContext(
|
|
928
|
+
targetPath,
|
|
929
|
+
undefined,
|
|
930
|
+
undefined,
|
|
931
|
+
false
|
|
932
|
+
);
|
|
933
|
+
const syncStatus = await syncEngine.getStatus();
|
|
934
|
+
|
|
935
|
+
if (!syncStatus.snapshot) {
|
|
936
|
+
out.error("No snapshot found");
|
|
937
|
+
await safeRepoShutdown(repo, "ls");
|
|
938
|
+
out.exit(1);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const files = Array.from(syncStatus.snapshot.files.entries()).sort(
|
|
943
|
+
([pathA], [pathB]) => pathA.localeCompare(pathB)
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
if (files.length === 0) {
|
|
947
|
+
out.info("No tracked files");
|
|
948
|
+
await safeRepoShutdown(repo, "ls");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (options.long) {
|
|
953
|
+
// Long format with URLs
|
|
954
|
+
for (const [filePath, entry] of files) {
|
|
955
|
+
const url = entry?.url || "unknown";
|
|
956
|
+
console.log(`${filePath} -> ${url}`);
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
// Simple list
|
|
960
|
+
for (const [filePath] of files) {
|
|
961
|
+
console.log(filePath);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
await safeRepoShutdown(repo, "ls");
|
|
966
|
+
} catch (error) {
|
|
967
|
+
out.error(`List failed: ${error}`);
|
|
968
|
+
out.exit(1);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* View or edit configuration
|
|
974
|
+
*/
|
|
975
|
+
export async function config(
|
|
976
|
+
targetPath: string = ".",
|
|
977
|
+
options: ConfigOptions = {}
|
|
978
|
+
): Promise<void> {
|
|
979
|
+
const out = new Output();
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
const resolvedPath = path.resolve(targetPath);
|
|
983
|
+
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
984
|
+
|
|
985
|
+
if (!(await pathExists(syncToolDir))) {
|
|
986
|
+
out.error("Directory not initialized for sync");
|
|
987
|
+
out.exit(1);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
991
|
+
const config = await configManager.getMerged();
|
|
992
|
+
|
|
993
|
+
if (options.list) {
|
|
994
|
+
// List all configuration
|
|
995
|
+
out.info("CONFIGURATION", "Full configuration");
|
|
996
|
+
out.log(JSON.stringify(config, null, 2));
|
|
997
|
+
} else if (options.get) {
|
|
998
|
+
// Get specific config value
|
|
999
|
+
const keys = options.get.split(".");
|
|
1000
|
+
let value: any = config;
|
|
1001
|
+
for (const key of keys) {
|
|
1002
|
+
value = value?.[key];
|
|
1003
|
+
}
|
|
1004
|
+
if (value !== undefined) {
|
|
1005
|
+
console.log(
|
|
1006
|
+
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
|
1007
|
+
);
|
|
1008
|
+
} else {
|
|
1009
|
+
out.error(`Config key not found: ${options.get}`);
|
|
1010
|
+
out.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
} else {
|
|
1013
|
+
// Show basic config info
|
|
1014
|
+
out.info("CONFIGURATION", resolvedPath);
|
|
1015
|
+
out.pair("Sync server", config.sync_server || "default");
|
|
1016
|
+
out.pair("Sync enabled", config.sync_enabled ? "yes" : "no");
|
|
1017
|
+
out.pair(
|
|
1018
|
+
"Exclusions",
|
|
1019
|
+
config.defaults.exclude_patterns.length.toString()
|
|
1020
|
+
);
|
|
1021
|
+
out.log("");
|
|
1022
|
+
out.log("Use --list to see full configuration");
|
|
1023
|
+
}
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
out.error(`Config failed: ${error}`);
|
|
1026
|
+
out.exit(1);
|
|
1204
1027
|
}
|
|
1205
1028
|
}
|
|
1206
1029
|
|