pushwork 1.0.5 → 1.0.7
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 +87 -335
- package/dist/.pushwork/automerge/3P/Dm3ekE2pmjGnWvDaG3vSR7ww98/snapshot/aa2349c94955ea561f698720142f9d884a6872d9f82dc332d578c216beb0df0e +0 -0
- package/dist/.pushwork/automerge/st/orage-adapter-id +1 -0
- package/dist/.pushwork/config.json +15 -0
- package/dist/.pushwork/snapshot.json +7 -0
- package/dist/cli.js +208 -213
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +51 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +799 -0
- package/dist/commands.js.map +1 -0
- package/dist/core/change-detection.d.ts +2 -23
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +73 -115
- package/dist/core/change-detection.js.map +1 -1
- package/dist/{config/index.d.ts → core/config.d.ts} +13 -3
- package/dist/core/config.d.ts.map +1 -0
- package/dist/{config/index.js → core/config.js} +55 -73
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/move-detection.d.ts +4 -3
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +8 -7
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/snapshot.d.ts +0 -4
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/snapshot.js +2 -11
- package/dist/core/snapshot.js.map +1 -1
- package/dist/core/sync-engine.d.ts +5 -11
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +211 -308
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -6
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts +24 -88
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/documents.d.ts +15 -2
- package/dist/types/documents.d.ts.map +1 -1
- package/dist/types/documents.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/snapshot.d.ts +0 -21
- package/dist/types/snapshot.d.ts.map +1 -1
- package/dist/types/snapshot.js +0 -14
- package/dist/types/snapshot.js.map +1 -1
- package/dist/utils/content.d.ts.map +1 -1
- package/dist/utils/content.js +2 -6
- package/dist/utils/content.js.map +1 -1
- package/dist/utils/directory.d.ts +10 -0
- package/dist/utils/directory.d.ts.map +1 -0
- package/dist/utils/directory.js +37 -0
- package/dist/utils/directory.js.map +1 -0
- package/dist/utils/fs.d.ts +15 -2
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +54 -20
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mime-types.d.ts.map +1 -1
- package/dist/utils/mime-types.js +11 -4
- package/dist/utils/mime-types.js.map +1 -1
- package/dist/utils/network-sync.d.ts +0 -6
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +55 -99
- package/dist/utils/network-sync.js.map +1 -1
- package/dist/utils/output.d.ts +129 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +375 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +2 -6
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +8 -31
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.js +2 -2
- package/dist/utils/string-similarity.js.map +1 -1
- package/dist/utils/trace.d.ts +19 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +68 -0
- package/dist/utils/trace.js.map +1 -0
- package/package.json +11 -11
- package/src/cli.ts +276 -308
- package/src/commands.ts +988 -0
- package/src/core/change-detection.ts +182 -240
- package/src/{config/index.ts → core/config.ts} +65 -82
- package/src/core/index.ts +1 -1
- package/src/core/move-detection.ts +10 -8
- package/src/core/snapshot.ts +2 -12
- package/src/core/sync-engine.ts +237 -427
- package/src/index.ts +0 -10
- package/src/types/config.ts +28 -93
- package/src/types/documents.ts +16 -2
- package/src/types/index.ts +0 -5
- package/src/types/snapshot.ts +0 -23
- package/src/utils/content.ts +2 -6
- package/src/utils/directory.ts +50 -0
- package/src/utils/fs.ts +58 -23
- package/src/utils/index.ts +1 -5
- package/src/utils/mime-types.ts +12 -4
- package/src/utils/network-sync.ts +79 -137
- package/src/utils/output.ts +450 -0
- package/src/utils/repo-factory.ts +13 -44
- package/src/utils/string-similarity.ts +2 -2
- package/src/utils/trace.ts +70 -0
- package/test/integration/exclude-patterns.test.ts +6 -15
- package/test/integration/fuzzer.test.ts +308 -391
- package/test/integration/init-sync.test.ts +89 -0
- package/test/integration/sync-deletion.test.ts +2 -61
- package/test/integration/sync-flow.test.ts +4 -24
- package/test/jest.setup.ts +34 -0
- package/test/unit/deletion-behavior.test.ts +3 -14
- package/test/unit/enhanced-mime-detection.test.ts +0 -22
- package/test/unit/snapshot.test.ts +2 -29
- package/test/unit/sync-convergence.test.ts +3 -198
- package/test/unit/sync-timing.test.ts +0 -44
- package/test/unit/utils.test.ts +0 -2
- package/tsconfig.json +3 -3
- package/bench/filesystem.bench.ts +0 -78
- package/bench/hashing.bench.ts +0 -60
- package/bench/move-detection.bench.ts +0 -130
- package/bench/runner.ts +0 -49
- package/dist/browser/browser-sync-engine.d.ts +0 -64
- package/dist/browser/browser-sync-engine.d.ts.map +0 -1
- package/dist/browser/browser-sync-engine.js +0 -303
- package/dist/browser/browser-sync-engine.js.map +0 -1
- package/dist/browser/filesystem-adapter.d.ts +0 -84
- package/dist/browser/filesystem-adapter.d.ts.map +0 -1
- package/dist/browser/filesystem-adapter.js +0 -413
- package/dist/browser/filesystem-adapter.js.map +0 -1
- package/dist/browser/index.d.ts +0 -36
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -90
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/types.d.ts +0 -70
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/browser/types.js +0 -6
- package/dist/browser/types.js.map +0 -1
- package/dist/cli/commands.d.ts +0 -67
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -794
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -19
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/output.d.ts +0 -75
- package/dist/cli/output.d.ts.map +0 -1
- package/dist/cli/output.js +0 -182
- package/dist/cli/output.js.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js.map +0 -1
- package/dist/config/remote-manager.d.ts +0 -65
- package/dist/config/remote-manager.d.ts.map +0 -1
- package/dist/config/remote-manager.js +0 -243
- package/dist/config/remote-manager.js.map +0 -1
- package/dist/core/isomorphic-snapshot.d.ts +0 -58
- package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
- package/dist/core/isomorphic-snapshot.js +0 -204
- package/dist/core/isomorphic-snapshot.js.map +0 -1
- package/dist/platform/browser-filesystem.d.ts +0 -26
- package/dist/platform/browser-filesystem.d.ts.map +0 -1
- package/dist/platform/browser-filesystem.js +0 -91
- package/dist/platform/browser-filesystem.js.map +0 -1
- package/dist/platform/filesystem.d.ts +0 -29
- package/dist/platform/filesystem.d.ts.map +0 -1
- package/dist/platform/filesystem.js +0 -65
- package/dist/platform/filesystem.js.map +0 -1
- package/dist/platform/node-filesystem.d.ts +0 -21
- package/dist/platform/node-filesystem.d.ts.map +0 -1
- package/dist/platform/node-filesystem.js +0 -93
- package/dist/platform/node-filesystem.js.map +0 -1
- package/dist/utils/content-similarity.d.ts +0 -53
- package/dist/utils/content-similarity.d.ts.map +0 -1
- package/dist/utils/content-similarity.js +0 -155
- package/dist/utils/content-similarity.js.map +0 -1
- package/dist/utils/fs-browser.d.ts +0 -57
- package/dist/utils/fs-browser.d.ts.map +0 -1
- package/dist/utils/fs-browser.js +0 -311
- package/dist/utils/fs-browser.js.map +0 -1
- package/dist/utils/fs-node.d.ts +0 -53
- package/dist/utils/fs-node.d.ts.map +0 -1
- package/dist/utils/fs-node.js +0 -220
- package/dist/utils/fs-node.js.map +0 -1
- package/dist/utils/isomorphic.d.ts +0 -29
- package/dist/utils/isomorphic.d.ts.map +0 -1
- package/dist/utils/isomorphic.js +0 -139
- package/dist/utils/isomorphic.js.map +0 -1
- package/dist/utils/pure.d.ts +0 -25
- package/dist/utils/pure.d.ts.map +0 -1
- package/dist/utils/pure.js +0 -112
- package/dist/utils/pure.js.map +0 -1
- package/src/cli/commands.ts +0 -1030
- package/src/cli/index.ts +0 -2
- package/src/cli/output.ts +0 -244
- package/test/README-TESTING-GAPS.md +0 -174
package/src/cli/commands.ts
DELETED
|
@@ -1,1030 +0,0 @@
|
|
|
1
|
-
import * as path from "path";
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import { Repo, AutomergeUrl } from "@automerge/automerge-repo";
|
|
4
|
-
import * as diffLib from "diff";
|
|
5
|
-
import {
|
|
6
|
-
CloneOptions,
|
|
7
|
-
SyncOptions,
|
|
8
|
-
DiffOptions,
|
|
9
|
-
LogOptions,
|
|
10
|
-
CheckoutOptions,
|
|
11
|
-
InitOptions,
|
|
12
|
-
CommitOptions,
|
|
13
|
-
StatusOptions,
|
|
14
|
-
UrlOptions,
|
|
15
|
-
ListOptions,
|
|
16
|
-
ConfigOptions,
|
|
17
|
-
DebugOptions,
|
|
18
|
-
DirectoryConfig,
|
|
19
|
-
DirectoryDocument,
|
|
20
|
-
} from "../types";
|
|
21
|
-
import { SyncEngine } from "../core";
|
|
22
|
-
import { pathExists, ensureDirectoryExists } from "../utils";
|
|
23
|
-
import { ConfigManager } from "../config";
|
|
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
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Shared context that commands can use
|
|
60
|
-
*/
|
|
61
|
-
export interface CommandContext {
|
|
62
|
-
repo: Repo;
|
|
63
|
-
syncEngine: SyncEngine;
|
|
64
|
-
config: DirectoryConfig;
|
|
65
|
-
workingDir: string;
|
|
66
|
-
}
|
|
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
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Shared pre-action that ensures repository and sync engine are properly initialized
|
|
93
|
-
* This function always works, with or without network connectivity
|
|
94
|
-
*/
|
|
95
|
-
export async function setupCommandContext(
|
|
96
|
-
workingDir: string = process.cwd(),
|
|
97
|
-
customSyncServer?: string,
|
|
98
|
-
customStorageId?: string,
|
|
99
|
-
enableNetwork: boolean = true
|
|
100
|
-
): Promise<CommandContext> {
|
|
101
|
-
const resolvedPath = path.resolve(workingDir);
|
|
102
|
-
|
|
103
|
-
// Check if initialized
|
|
104
|
-
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
105
|
-
if (!(await pathExists(syncToolDir))) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
'Directory not initialized for sync. Run "pushwork init" first.'
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Load configuration
|
|
112
|
-
const configManager = new ConfigManager(resolvedPath);
|
|
113
|
-
const config = await configManager.getMerged();
|
|
114
|
-
|
|
115
|
-
// Create repo with configurable network setting
|
|
116
|
-
const repo = await createRepo(resolvedPath, {
|
|
117
|
-
enableNetwork,
|
|
118
|
-
syncServer: customSyncServer,
|
|
119
|
-
syncServerStorageId: customStorageId,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Create sync engine with configurable network sync
|
|
123
|
-
const syncEngine = new SyncEngine(
|
|
124
|
-
repo,
|
|
125
|
-
resolvedPath,
|
|
126
|
-
config.defaults.exclude_patterns,
|
|
127
|
-
enableNetwork,
|
|
128
|
-
config.sync_server_storage_id
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
repo,
|
|
133
|
-
syncEngine,
|
|
134
|
-
config,
|
|
135
|
-
workingDir: resolvedPath,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Safely shutdown a repository with proper error handling
|
|
141
|
-
*/
|
|
142
|
-
export async function safeRepoShutdown(
|
|
143
|
-
repo: Repo,
|
|
144
|
-
context?: string
|
|
145
|
-
): Promise<void> {
|
|
146
|
-
try {
|
|
147
|
-
await repo.shutdown();
|
|
148
|
-
} catch (shutdownError) {
|
|
149
|
-
// WebSocket errors during shutdown are common and non-critical
|
|
150
|
-
// Silently ignore them - they don't affect data integrity
|
|
151
|
-
const errorMessage =
|
|
152
|
-
shutdownError instanceof Error
|
|
153
|
-
? shutdownError.message
|
|
154
|
-
: String(shutdownError);
|
|
155
|
-
|
|
156
|
-
// Ignore WebSocket-related errors entirely
|
|
157
|
-
if (
|
|
158
|
-
errorMessage.includes("WebSocket") ||
|
|
159
|
-
errorMessage.includes("connection was established") ||
|
|
160
|
-
errorMessage.includes("was closed")
|
|
161
|
-
) {
|
|
162
|
-
// Silently ignore WebSocket shutdown errors
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Only warn about truly unexpected shutdown errors
|
|
167
|
-
console.warn(
|
|
168
|
-
`Warning: Repository shutdown failed${
|
|
169
|
-
context ? ` (${context})` : ""
|
|
170
|
-
}: ${shutdownError}`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Initialize sync in a directory
|
|
177
|
-
*/
|
|
178
|
-
export async function init(
|
|
179
|
-
targetPath: string,
|
|
180
|
-
options: InitOptions = {}
|
|
181
|
-
): Promise<void> {
|
|
182
|
-
// Validate sync server options
|
|
183
|
-
validateSyncServerOptions(options.syncServer, options.syncServerStorageId);
|
|
184
|
-
|
|
185
|
-
const out = new Output();
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const resolvedPath = path.resolve(targetPath);
|
|
189
|
-
|
|
190
|
-
out.task(`Initializing ${resolvedPath}`);
|
|
191
|
-
|
|
192
|
-
await ensureDirectoryExists(resolvedPath);
|
|
193
|
-
|
|
194
|
-
// Check if already initialized
|
|
195
|
-
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
196
|
-
if (await pathExists(syncToolDir)) {
|
|
197
|
-
out.error("Directory already initialized for sync");
|
|
198
|
-
out.exit(1);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
out.update("Creating sync directory");
|
|
202
|
-
await ensureDirectoryExists(syncToolDir);
|
|
203
|
-
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
204
|
-
|
|
205
|
-
out.update("Setting up configuration");
|
|
206
|
-
const configManager = new ConfigManager(resolvedPath);
|
|
207
|
-
const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
|
|
208
|
-
const defaultStorageId =
|
|
209
|
-
options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
210
|
-
const config: DirectoryConfig = {
|
|
211
|
-
sync_server: defaultSyncServer,
|
|
212
|
-
sync_server_storage_id: defaultStorageId,
|
|
213
|
-
sync_enabled: true,
|
|
214
|
-
defaults: {
|
|
215
|
-
exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
|
|
216
|
-
large_file_threshold: "100MB",
|
|
217
|
-
},
|
|
218
|
-
diff: {
|
|
219
|
-
show_binary: false,
|
|
220
|
-
},
|
|
221
|
-
sync: {
|
|
222
|
-
move_detection_threshold: 0.8,
|
|
223
|
-
prompt_threshold: 0.5,
|
|
224
|
-
auto_sync: false,
|
|
225
|
-
parallel_operations: 4,
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
await configManager.save(config);
|
|
229
|
-
|
|
230
|
-
out.update("Creating root directory");
|
|
231
|
-
const repo = await createRepo(resolvedPath, {
|
|
232
|
-
enableNetwork: true,
|
|
233
|
-
syncServer: options.syncServer,
|
|
234
|
-
syncServerStorageId: options.syncServerStorageId,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const rootDoc: DirectoryDocument = {
|
|
238
|
-
"@patchwork": { type: "folder" },
|
|
239
|
-
docs: [],
|
|
240
|
-
};
|
|
241
|
-
const rootHandle = repo.create(rootDoc);
|
|
242
|
-
|
|
243
|
-
out.update("Scanning existing files");
|
|
244
|
-
const syncEngine = new SyncEngine(
|
|
245
|
-
repo,
|
|
246
|
-
resolvedPath,
|
|
247
|
-
config.defaults.exclude_patterns,
|
|
248
|
-
true,
|
|
249
|
-
config.sync_server_storage_id
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
await syncEngine.setRootDirectoryUrl(rootHandle.url);
|
|
253
|
-
const result = await syncEngine.sync(false);
|
|
254
|
-
|
|
255
|
-
out.update("Writing to disk");
|
|
256
|
-
await safeRepoShutdown(repo, "init");
|
|
257
|
-
|
|
258
|
-
out.done();
|
|
259
|
-
|
|
260
|
-
out.pair("Sync", defaultSyncServer);
|
|
261
|
-
if (result.filesChanged > 0) {
|
|
262
|
-
out.pair("Files", `${result.filesChanged} added`);
|
|
263
|
-
}
|
|
264
|
-
out.success("INITIALIZED", rootHandle.url);
|
|
265
|
-
|
|
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
|
-
}
|
|
279
|
-
|
|
280
|
-
out.log("");
|
|
281
|
-
out.log(`Run 'pushwork sync' to start synchronizing`);
|
|
282
|
-
} catch (error) {
|
|
283
|
-
out.error("FAILED", "Initialization failed");
|
|
284
|
-
out.log(` ${error}`);
|
|
285
|
-
out.exit(1);
|
|
286
|
-
}
|
|
287
|
-
process.exit();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Run bidirectional sync
|
|
292
|
-
*/
|
|
293
|
-
export async function sync(
|
|
294
|
-
targetPath = ".",
|
|
295
|
-
options: SyncOptions
|
|
296
|
-
): Promise<void> {
|
|
297
|
-
const out = new Output();
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
out.task("Syncing");
|
|
301
|
-
|
|
302
|
-
const { repo, syncEngine, workingDir } = await setupCommandContext(
|
|
303
|
-
targetPath
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (options.dryRun) {
|
|
307
|
-
out.update("Analyzing changes");
|
|
308
|
-
const preview = await syncEngine.previewChanges();
|
|
309
|
-
|
|
310
|
-
out.done();
|
|
311
|
-
|
|
312
|
-
if (preview.changes.length === 0 && preview.moves.length === 0) {
|
|
313
|
-
out.info("No changes detected");
|
|
314
|
-
out.log("Everything is already in sync");
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
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
|
-
}
|
|
324
|
-
|
|
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`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (preview.moves.length > 0) {
|
|
341
|
-
out.log("");
|
|
342
|
-
out.log("Moves:");
|
|
343
|
-
for (const move of preview.moves.slice(0, 5)) {
|
|
344
|
-
out.log(` ${move.fromPath} → ${move.toPath}`);
|
|
345
|
-
}
|
|
346
|
-
if (preview.moves.length > 5) {
|
|
347
|
-
out.log(` ... and ${preview.moves.length - 5} more`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
out.log("");
|
|
352
|
-
out.log("Run without --dry-run to apply these changes");
|
|
353
|
-
} else {
|
|
354
|
-
out.update("Synchronizing");
|
|
355
|
-
const result = await syncEngine.sync(false);
|
|
356
|
-
|
|
357
|
-
out.update("Writing to disk");
|
|
358
|
-
await safeRepoShutdown(repo, "sync");
|
|
359
|
-
|
|
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
|
-
}
|
|
374
|
-
|
|
375
|
-
if (result.warnings.length > 0) {
|
|
376
|
-
out.log("");
|
|
377
|
-
out.warn("WARNINGS", `${result.warnings.length} warnings`);
|
|
378
|
-
for (const warning of result.warnings.slice(0, 5)) {
|
|
379
|
-
out.log(` ${warning}`);
|
|
380
|
-
}
|
|
381
|
-
if (result.warnings.length > 5) {
|
|
382
|
-
out.log(` ... and ${result.warnings.length - 5} more`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
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
|
-
);
|
|
398
|
-
}
|
|
399
|
-
} else {
|
|
400
|
-
out.done("partial", false);
|
|
401
|
-
out.warn(
|
|
402
|
-
"PARTIAL",
|
|
403
|
-
`${result.filesChanged} updated, ${result.errors.length} errors`
|
|
404
|
-
);
|
|
405
|
-
out.pair("Files", result.filesChanged.toString());
|
|
406
|
-
out.pair("Errors", result.errors.length.toString());
|
|
407
|
-
|
|
408
|
-
out.log("");
|
|
409
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
410
|
-
out.error("ERROR", error.path);
|
|
411
|
-
out.log(` ${error.error.message}`);
|
|
412
|
-
out.log("");
|
|
413
|
-
}
|
|
414
|
-
if (result.errors.length > 5) {
|
|
415
|
-
out.log(`... and ${result.errors.length - 5} more errors`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
} catch (error) {
|
|
420
|
-
out.error("FAILED", "Sync failed");
|
|
421
|
-
out.log(` ${error}`);
|
|
422
|
-
out.exit(1);
|
|
423
|
-
}
|
|
424
|
-
process.exit();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Show differences between local and remote
|
|
429
|
-
*/
|
|
430
|
-
export async function diff(
|
|
431
|
-
targetPath = ".",
|
|
432
|
-
options: DiffOptions
|
|
433
|
-
): Promise<void> {
|
|
434
|
-
const out = new Output();
|
|
435
|
-
|
|
436
|
-
try {
|
|
437
|
-
out.task("Analyzing changes");
|
|
438
|
-
|
|
439
|
-
const { repo, syncEngine } = await setupCommandContext(
|
|
440
|
-
targetPath,
|
|
441
|
-
undefined,
|
|
442
|
-
undefined,
|
|
443
|
-
false
|
|
444
|
-
);
|
|
445
|
-
const preview = await syncEngine.previewChanges();
|
|
446
|
-
|
|
447
|
-
out.done();
|
|
448
|
-
|
|
449
|
-
if (options.nameOnly) {
|
|
450
|
-
for (const change of preview.changes) {
|
|
451
|
-
console.log(change.path);
|
|
452
|
-
}
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (preview.changes.length === 0) {
|
|
457
|
-
out.success("No changes detected");
|
|
458
|
-
await safeRepoShutdown(repo, "diff");
|
|
459
|
-
out.exit();
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
out.warn(`${preview.changes.length} changes detected`);
|
|
464
|
-
|
|
465
|
-
for (const change of preview.changes) {
|
|
466
|
-
const prefix =
|
|
467
|
-
change.changeType === "local_only"
|
|
468
|
-
? "[local] "
|
|
469
|
-
: change.changeType === "remote_only"
|
|
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
|
-
);
|
|
498
|
-
|
|
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
|
-
}
|
|
506
|
-
|
|
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
|
-
}
|
|
535
|
-
} else {
|
|
536
|
-
out.log(`${prefix} ${change.path}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
await safeRepoShutdown(repo, "diff");
|
|
541
|
-
} catch (error) {
|
|
542
|
-
out.error(`Diff failed: ${error}`);
|
|
543
|
-
out.exit(1);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Show sync status
|
|
549
|
-
*/
|
|
550
|
-
export async function status(
|
|
551
|
-
targetPath: string = ".",
|
|
552
|
-
options: StatusOptions = {}
|
|
553
|
-
): Promise<void> {
|
|
554
|
-
const out = new Output();
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
out.task("Loading status");
|
|
558
|
-
|
|
559
|
-
const { repo, syncEngine, workingDir, config } = await setupCommandContext(
|
|
560
|
-
targetPath,
|
|
561
|
-
undefined,
|
|
562
|
-
undefined,
|
|
563
|
-
false
|
|
564
|
-
);
|
|
565
|
-
const syncStatus = await syncEngine.getStatus();
|
|
566
|
-
|
|
567
|
-
out.done();
|
|
568
|
-
|
|
569
|
-
out.info("STATUS", workingDir);
|
|
570
|
-
|
|
571
|
-
if (syncStatus.snapshot?.rootDirectoryUrl) {
|
|
572
|
-
out.pair("URL", syncStatus.snapshot.rootDirectoryUrl);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (syncStatus.snapshot) {
|
|
576
|
-
const fileCount = syncStatus.snapshot.files.size;
|
|
577
|
-
out.pair("Files", `${fileCount} tracked`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
out.pair("Sync", config?.sync_server || "wss://sync3.automerge.org");
|
|
581
|
-
|
|
582
|
-
if (syncStatus.hasChanges) {
|
|
583
|
-
out.pair("Changes", `${syncStatus.changeCount} pending`);
|
|
584
|
-
out.log("");
|
|
585
|
-
out.log("Run 'pushwork diff' to see changes");
|
|
586
|
-
} else {
|
|
587
|
-
out.pair("Status", "up to date");
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
await safeRepoShutdown(repo, "status");
|
|
591
|
-
} catch (error) {
|
|
592
|
-
out.error(`Status check failed: ${error}`);
|
|
593
|
-
out.exit(1);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Show sync history
|
|
599
|
-
*/
|
|
600
|
-
export async function log(
|
|
601
|
-
targetPath = ".",
|
|
602
|
-
options: LogOptions
|
|
603
|
-
): Promise<void> {
|
|
604
|
-
const out = new Output();
|
|
605
|
-
|
|
606
|
-
try {
|
|
607
|
-
const { repo: logRepo, workingDir } = await setupCommandContext(
|
|
608
|
-
targetPath,
|
|
609
|
-
undefined,
|
|
610
|
-
undefined,
|
|
611
|
-
false
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
// TODO: Implement history tracking
|
|
615
|
-
const snapshotPath = path.join(workingDir, ".pushwork", "snapshot.json");
|
|
616
|
-
if (await pathExists(snapshotPath)) {
|
|
617
|
-
const stats = await fs.stat(snapshotPath);
|
|
618
|
-
out.info("HISTORY", "Sync history (stub)");
|
|
619
|
-
out.pair("Last sync", stats.mtime.toISOString());
|
|
620
|
-
} else {
|
|
621
|
-
out.info("No sync history found");
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
await safeRepoShutdown(logRepo, "log");
|
|
625
|
-
} catch (error) {
|
|
626
|
-
out.error(`Log failed: ${error}`);
|
|
627
|
-
out.exit(1);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Checkout/restore from previous sync
|
|
633
|
-
*/
|
|
634
|
-
export async function checkout(
|
|
635
|
-
syncId: string,
|
|
636
|
-
targetPath = ".",
|
|
637
|
-
options: CheckoutOptions
|
|
638
|
-
): Promise<void> {
|
|
639
|
-
const out = new Output();
|
|
640
|
-
|
|
641
|
-
try {
|
|
642
|
-
const { workingDir } = await setupCommandContext(targetPath);
|
|
643
|
-
|
|
644
|
-
// TODO: Implement checkout functionality
|
|
645
|
-
out.warn("NOT IMPLEMENTED", "Checkout not yet implemented");
|
|
646
|
-
out.pair("Sync ID", syncId);
|
|
647
|
-
out.pair("Path", workingDir);
|
|
648
|
-
} catch (error) {
|
|
649
|
-
out.error(`Checkout failed: ${error}`);
|
|
650
|
-
out.exit(1);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Clone an existing synced directory from an AutomergeUrl
|
|
656
|
-
*/
|
|
657
|
-
export async function clone(
|
|
658
|
-
rootUrl: string,
|
|
659
|
-
targetPath: string,
|
|
660
|
-
options: CloneOptions
|
|
661
|
-
): Promise<void> {
|
|
662
|
-
// Validate sync server options
|
|
663
|
-
validateSyncServerOptions(options.syncServer, options.syncServerStorageId);
|
|
664
|
-
|
|
665
|
-
const out = new Output();
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
const resolvedPath = path.resolve(targetPath);
|
|
669
|
-
|
|
670
|
-
out.task(`Cloning ${rootUrl}`);
|
|
671
|
-
|
|
672
|
-
// Check if directory exists and handle --force
|
|
673
|
-
if (await pathExists(resolvedPath)) {
|
|
674
|
-
const files = await fs.readdir(resolvedPath);
|
|
675
|
-
if (files.length > 0 && !options.force) {
|
|
676
|
-
out.error("Target directory is not empty. Use --force to overwrite");
|
|
677
|
-
out.exit(1);
|
|
678
|
-
}
|
|
679
|
-
} else {
|
|
680
|
-
await ensureDirectoryExists(resolvedPath);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Check if already initialized
|
|
684
|
-
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
685
|
-
if (await pathExists(syncToolDir)) {
|
|
686
|
-
if (!options.force) {
|
|
687
|
-
out.error("Directory already initialized. Use --force to overwrite");
|
|
688
|
-
out.exit(1);
|
|
689
|
-
}
|
|
690
|
-
await fs.rm(syncToolDir, { recursive: true, force: true });
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
out.update("Creating sync directory");
|
|
694
|
-
await ensureDirectoryExists(syncToolDir);
|
|
695
|
-
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
696
|
-
|
|
697
|
-
out.update("Setting up configuration");
|
|
698
|
-
const configManager = new ConfigManager(resolvedPath);
|
|
699
|
-
const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
|
|
700
|
-
const defaultStorageId =
|
|
701
|
-
options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
702
|
-
const config: DirectoryConfig = {
|
|
703
|
-
sync_server: defaultSyncServer,
|
|
704
|
-
sync_server_storage_id: defaultStorageId,
|
|
705
|
-
sync_enabled: true,
|
|
706
|
-
defaults: {
|
|
707
|
-
exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
|
|
708
|
-
large_file_threshold: "100MB",
|
|
709
|
-
},
|
|
710
|
-
diff: {
|
|
711
|
-
show_binary: false,
|
|
712
|
-
},
|
|
713
|
-
sync: {
|
|
714
|
-
move_detection_threshold: 0.8,
|
|
715
|
-
prompt_threshold: 0.5,
|
|
716
|
-
auto_sync: false,
|
|
717
|
-
parallel_operations: 4,
|
|
718
|
-
},
|
|
719
|
-
};
|
|
720
|
-
await configManager.save(config);
|
|
721
|
-
|
|
722
|
-
out.update("Connecting to sync server");
|
|
723
|
-
const repo = await createRepo(resolvedPath, {
|
|
724
|
-
enableNetwork: true,
|
|
725
|
-
syncServer: options.syncServer,
|
|
726
|
-
syncServerStorageId: options.syncServerStorageId,
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
out.update("Downloading files");
|
|
730
|
-
const syncEngine = new SyncEngine(
|
|
731
|
-
repo,
|
|
732
|
-
resolvedPath,
|
|
733
|
-
config.defaults.exclude_patterns,
|
|
734
|
-
true,
|
|
735
|
-
defaultStorageId
|
|
736
|
-
);
|
|
737
|
-
|
|
738
|
-
await syncEngine.setRootDirectoryUrl(rootUrl as AutomergeUrl);
|
|
739
|
-
const result = await syncEngine.sync(false);
|
|
740
|
-
|
|
741
|
-
out.update("Writing to disk");
|
|
742
|
-
await safeRepoShutdown(repo, "clone");
|
|
743
|
-
|
|
744
|
-
out.done();
|
|
745
|
-
|
|
746
|
-
out.pair("Path", resolvedPath);
|
|
747
|
-
out.pair("Files", `${result.filesChanged} downloaded`);
|
|
748
|
-
out.pair("Sync", defaultSyncServer);
|
|
749
|
-
out.success("CLONED", rootUrl);
|
|
750
|
-
} catch (error) {
|
|
751
|
-
out.error("FAILED", "Clone failed");
|
|
752
|
-
out.log(` ${error}`);
|
|
753
|
-
out.exit(1);
|
|
754
|
-
}
|
|
755
|
-
process.exit();
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Get the root URL for the current pushwork repository
|
|
760
|
-
*/
|
|
761
|
-
export async function url(
|
|
762
|
-
targetPath: string = ".",
|
|
763
|
-
options: UrlOptions = {}
|
|
764
|
-
): Promise<void> {
|
|
765
|
-
const out = new Output();
|
|
766
|
-
|
|
767
|
-
try {
|
|
768
|
-
const resolvedPath = path.resolve(targetPath);
|
|
769
|
-
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
770
|
-
|
|
771
|
-
if (!(await pathExists(syncToolDir))) {
|
|
772
|
-
out.error("Directory not initialized for sync");
|
|
773
|
-
out.exit(1);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const snapshotPath = path.join(syncToolDir, "snapshot.json");
|
|
777
|
-
if (!(await pathExists(snapshotPath))) {
|
|
778
|
-
out.error("No snapshot found");
|
|
779
|
-
out.exit(1);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const snapshotData = await fs.readFile(snapshotPath, "utf-8");
|
|
783
|
-
const snapshot = JSON.parse(snapshotData);
|
|
784
|
-
|
|
785
|
-
if (snapshot.rootDirectoryUrl) {
|
|
786
|
-
// Output just the URL for easy use in scripts
|
|
787
|
-
console.log(snapshot.rootDirectoryUrl);
|
|
788
|
-
} else {
|
|
789
|
-
out.error("No root URL found in snapshot");
|
|
790
|
-
out.exit(1);
|
|
791
|
-
}
|
|
792
|
-
} catch (error) {
|
|
793
|
-
out.error(`Failed to get URL: ${error}`);
|
|
794
|
-
out.exit(1);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
export async function commit(
|
|
799
|
-
targetPath: string,
|
|
800
|
-
options: CommitOptions = {}
|
|
801
|
-
): Promise<void> {
|
|
802
|
-
const out = new Output();
|
|
803
|
-
|
|
804
|
-
try {
|
|
805
|
-
out.task("Committing local changes");
|
|
806
|
-
|
|
807
|
-
const { repo, syncEngine } = await setupCommandContext(
|
|
808
|
-
targetPath,
|
|
809
|
-
undefined,
|
|
810
|
-
undefined,
|
|
811
|
-
false
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
const result = await syncEngine.commitLocal(options.dryRun || false);
|
|
815
|
-
await safeRepoShutdown(repo, "commit");
|
|
816
|
-
|
|
817
|
-
out.done();
|
|
818
|
-
|
|
819
|
-
if (result.errors.length > 0) {
|
|
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);
|
|
825
|
-
}
|
|
826
|
-
|
|
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}`));
|
|
835
|
-
}
|
|
836
|
-
} catch (error) {
|
|
837
|
-
out.error(`Commit failed: ${error}`);
|
|
838
|
-
out.exit(1);
|
|
839
|
-
}
|
|
840
|
-
process.exit();
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Debug command to inspect internal document state
|
|
845
|
-
*/
|
|
846
|
-
export async function debug(
|
|
847
|
-
targetPath: string = ".",
|
|
848
|
-
options: DebugOptions = {}
|
|
849
|
-
): Promise<void> {
|
|
850
|
-
const out = new Output();
|
|
851
|
-
|
|
852
|
-
try {
|
|
853
|
-
out.task("Loading debug info");
|
|
854
|
-
|
|
855
|
-
const { repo, syncEngine, workingDir } = await setupCommandContext(
|
|
856
|
-
targetPath,
|
|
857
|
-
undefined,
|
|
858
|
-
undefined,
|
|
859
|
-
false
|
|
860
|
-
);
|
|
861
|
-
const debugStatus = await syncEngine.getStatus();
|
|
862
|
-
|
|
863
|
-
out.done("done");
|
|
864
|
-
|
|
865
|
-
out.info("DEBUG", workingDir);
|
|
866
|
-
|
|
867
|
-
if (debugStatus.snapshot?.rootDirectoryUrl) {
|
|
868
|
-
out.pair("URL", debugStatus.snapshot.rootDirectoryUrl);
|
|
869
|
-
|
|
870
|
-
try {
|
|
871
|
-
const rootHandle = await repo.find<DirectoryDocument>(
|
|
872
|
-
debugStatus.snapshot.rootDirectoryUrl
|
|
873
|
-
);
|
|
874
|
-
const rootDoc = await rootHandle.doc();
|
|
875
|
-
|
|
876
|
-
if (rootDoc) {
|
|
877
|
-
out.pair("Entries", rootDoc.docs.length.toString());
|
|
878
|
-
if (rootDoc.lastSyncAt) {
|
|
879
|
-
const lastSyncDate = new Date(rootDoc.lastSyncAt);
|
|
880
|
-
out.pair("Last sync", lastSyncDate.toISOString());
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
if (options.verbose) {
|
|
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));
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
} catch (error) {
|
|
893
|
-
out.warn(`Error loading root document: ${error}`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
if (debugStatus.snapshot) {
|
|
898
|
-
out.pair("Files", debugStatus.snapshot.files.size.toString());
|
|
899
|
-
out.pair("Directories", debugStatus.snapshot.directories.size.toString());
|
|
900
|
-
|
|
901
|
-
if (options.verbose) {
|
|
902
|
-
out.log("");
|
|
903
|
-
out.log("All tracked files:");
|
|
904
|
-
debugStatus.snapshot.files.forEach((entry, filePath) => {
|
|
905
|
-
out.log(` ${filePath} -> ${entry.url}`);
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
await safeRepoShutdown(repo, "debug");
|
|
911
|
-
} catch (error) {
|
|
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);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// TODO: Add push and pull commands later
|