pushwork 1.1.4 → 2.0.0-a.sub.1
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/CLAUDE.md +9 -5
- package/dist/cli.js +48 -55
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +5 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +262 -263
- package/dist/commands.js.map +1 -1
- package/dist/core/change-detection.d.ts +1 -1
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +66 -103
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +14 -57
- package/dist/core/config.js.map +1 -1
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/move-detection.d.ts +2 -2
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +9 -13
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/snapshot.d.ts +1 -1
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/snapshot.js +9 -46
- package/dist/core/snapshot.js.map +1 -1
- package/dist/core/sync-engine.d.ts +8 -2
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +171 -175
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -20
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts +7 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +1 -5
- package/dist/types/config.js.map +1 -1
- package/dist/types/documents.js +4 -7
- package/dist/types/documents.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -19
- package/dist/types/index.js.map +1 -1
- package/dist/types/snapshot.js +1 -2
- package/dist/utils/content.js +4 -8
- package/dist/utils/content.js.map +1 -1
- package/dist/utils/directory.js +5 -9
- package/dist/utils/directory.js.map +1 -1
- package/dist/utils/fs.d.ts +1 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +34 -84
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -20
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mime-types.js +5 -43
- package/dist/utils/mime-types.js.map +1 -1
- package/dist/utils/network-sync.d.ts +13 -8
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +65 -137
- package/dist/utils/network-sync.js.map +1 -1
- package/dist/utils/node-polyfills.d.ts +9 -0
- package/dist/utils/node-polyfills.d.ts.map +1 -0
- package/dist/utils/node-polyfills.js +9 -0
- package/dist/utils/node-polyfills.js.map +1 -0
- package/dist/utils/output.js +32 -39
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/repo-factory.d.ts +8 -2
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +38 -47
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.js +1 -5
- package/dist/utils/string-similarity.js.map +1 -1
- package/dist/utils/text-diff.js +5 -43
- package/dist/utils/text-diff.js.map +1 -1
- package/dist/utils/trace.js +6 -11
- package/dist/utils/trace.js.map +1 -1
- package/package.json +7 -5
- package/src/cli.ts +25 -34
- package/src/commands.ts +75 -11
- package/src/core/change-detection.ts +4 -4
- package/src/core/config.ts +2 -12
- package/src/core/index.ts +5 -5
- package/src/core/move-detection.ts +4 -4
- package/src/core/snapshot.ts +3 -3
- package/src/core/sync-engine.ts +82 -50
- package/src/index.ts +4 -4
- package/src/types/config.ts +8 -8
- package/src/types/index.ts +3 -3
- package/src/utils/directory.ts +1 -1
- package/src/utils/fs.ts +6 -4
- package/src/utils/index.ts +4 -4
- package/src/utils/network-sync.ts +62 -115
- package/src/utils/node-polyfills.ts +8 -0
- package/src/utils/repo-factory.ts +55 -10
- package/src/utils/trace.ts +1 -1
- package/tsconfig.json +2 -1
package/dist/commands.js
CHANGED
|
@@ -1,82 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.init = init;
|
|
40
|
-
exports.sync = sync;
|
|
41
|
-
exports.diff = diff;
|
|
42
|
-
exports.status = status;
|
|
43
|
-
exports.log = log;
|
|
44
|
-
exports.checkout = checkout;
|
|
45
|
-
exports.clone = clone;
|
|
46
|
-
exports.url = url;
|
|
47
|
-
exports.rm = rm;
|
|
48
|
-
exports.commit = commit;
|
|
49
|
-
exports.ls = ls;
|
|
50
|
-
exports.config = config;
|
|
51
|
-
exports.watch = watch;
|
|
52
|
-
exports.root = root;
|
|
53
|
-
const path = __importStar(require("path"));
|
|
54
|
-
const fs = __importStar(require("fs/promises"));
|
|
55
|
-
const fsSync = __importStar(require("fs"));
|
|
56
|
-
const diffLib = __importStar(require("diff"));
|
|
57
|
-
const child_process_1 = require("child_process");
|
|
58
|
-
const core_1 = require("./core");
|
|
59
|
-
const utils_1 = require("./utils");
|
|
60
|
-
const config_1 = require("./core/config");
|
|
61
|
-
const repo_factory_1 = require("./utils/repo-factory");
|
|
62
|
-
const output_1 = require("./utils/output");
|
|
63
|
-
const network_sync_1 = require("./utils/network-sync");
|
|
64
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as fsSync from "fs";
|
|
4
|
+
import * as diffLib from "diff";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { SyncEngine } from "./core/index.js";
|
|
7
|
+
import { pathExists, ensureDirectoryExists, formatRelativePath } from "./utils/index.js";
|
|
8
|
+
import { ConfigManager } from "./core/config.js";
|
|
9
|
+
import { createRepo, createEphemeralRepo } from "./utils/repo-factory.js";
|
|
10
|
+
import { out } from "./utils/output.js";
|
|
11
|
+
import { waitForSync } from "./utils/network-sync.js";
|
|
12
|
+
import { readDocContent } from "./utils/text-diff.js";
|
|
13
|
+
import { DEFAULT_SYNC_SERVER } from "./types/config.js";
|
|
14
|
+
import chalk from "chalk";
|
|
65
15
|
/**
|
|
66
16
|
* Initialize repository directory structure and configuration
|
|
67
17
|
* Shared logic for init and clone commands
|
|
68
18
|
*/
|
|
69
19
|
async function initializeRepository(resolvedPath, overrides) {
|
|
70
20
|
// Create .pushwork directory structure
|
|
71
|
-
const syncToolDir = path.join(resolvedPath,
|
|
72
|
-
await
|
|
73
|
-
await
|
|
21
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
22
|
+
await ensureDirectoryExists(syncToolDir);
|
|
23
|
+
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
74
24
|
// Create configuration with overrides
|
|
75
|
-
const configManager = new
|
|
25
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
76
26
|
const config = await configManager.initializeWithOverrides(overrides);
|
|
77
27
|
// Create repository and sync engine
|
|
78
|
-
const repo = await
|
|
79
|
-
const syncEngine = new
|
|
28
|
+
const repo = await createRepo(resolvedPath, config);
|
|
29
|
+
const syncEngine = new SyncEngine(repo, resolvedPath, config);
|
|
80
30
|
return { config, repo, syncEngine };
|
|
81
31
|
}
|
|
82
32
|
/**
|
|
@@ -86,12 +36,12 @@ async function initializeRepository(resolvedPath, overrides) {
|
|
|
86
36
|
async function setupCommandContext(workingDir = process.cwd(), options) {
|
|
87
37
|
const resolvedPath = path.resolve(workingDir);
|
|
88
38
|
// Check if initialized
|
|
89
|
-
const syncToolDir = path.join(resolvedPath,
|
|
90
|
-
if (!(await
|
|
39
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
40
|
+
if (!(await pathExists(syncToolDir))) {
|
|
91
41
|
throw new Error('Directory not initialized for sync. Run "pushwork init" first.');
|
|
92
42
|
}
|
|
93
43
|
// Load configuration
|
|
94
|
-
const configManager = new
|
|
44
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
95
45
|
let config;
|
|
96
46
|
if (options?.forceDefaults) {
|
|
97
47
|
// Force mode: use defaults, only preserving root_directory_url from local config
|
|
@@ -109,9 +59,9 @@ async function setupCommandContext(workingDir = process.cwd(), options) {
|
|
|
109
59
|
config = { ...config, sync_enabled: options.syncEnabled };
|
|
110
60
|
}
|
|
111
61
|
// Create repo with config
|
|
112
|
-
const repo = await
|
|
62
|
+
const repo = await createRepo(resolvedPath, config);
|
|
113
63
|
// Create sync engine
|
|
114
|
-
const syncEngine = new
|
|
64
|
+
const syncEngine = new SyncEngine(repo, resolvedPath, config);
|
|
115
65
|
return {
|
|
116
66
|
repo,
|
|
117
67
|
syncEngine,
|
|
@@ -156,24 +106,23 @@ async function safeRepoShutdown(repo) {
|
|
|
156
106
|
/**
|
|
157
107
|
* Initialize sync in a directory
|
|
158
108
|
*/
|
|
159
|
-
async function init(targetPath, options = {}) {
|
|
109
|
+
export async function init(targetPath, options = {}) {
|
|
160
110
|
const resolvedPath = path.resolve(targetPath);
|
|
161
|
-
|
|
162
|
-
await
|
|
111
|
+
out.task(`Initializing`);
|
|
112
|
+
await ensureDirectoryExists(resolvedPath);
|
|
163
113
|
// Check if already initialized
|
|
164
|
-
const syncToolDir = path.join(resolvedPath,
|
|
165
|
-
if (await
|
|
166
|
-
|
|
167
|
-
|
|
114
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
115
|
+
if (await pathExists(syncToolDir)) {
|
|
116
|
+
out.error("Directory already initialized for sync");
|
|
117
|
+
out.exit(1);
|
|
168
118
|
}
|
|
169
119
|
// Initialize repository with optional CLI overrides
|
|
170
|
-
|
|
120
|
+
out.update("Setting up repository");
|
|
171
121
|
const { repo, syncEngine, config } = await initializeRepository(resolvedPath, {
|
|
172
122
|
sync_server: options.syncServer,
|
|
173
|
-
sync_server_storage_id: options.syncServerStorageId,
|
|
174
123
|
});
|
|
175
124
|
// Create new root directory document
|
|
176
|
-
|
|
125
|
+
out.update("Creating root directory");
|
|
177
126
|
const dirName = path.basename(resolvedPath);
|
|
178
127
|
const rootDoc = {
|
|
179
128
|
"@patchwork": { type: "folder" },
|
|
@@ -187,31 +136,31 @@ async function init(targetPath, options = {}) {
|
|
|
187
136
|
// Wait for root document to sync to server if sync is enabled
|
|
188
137
|
// This ensures the document is uploaded before we exit
|
|
189
138
|
// waitForSync() verifies the server has the document by comparing local and remote heads
|
|
190
|
-
if (config.sync_enabled && config.
|
|
191
|
-
|
|
192
|
-
const { failed } = await
|
|
139
|
+
if (config.sync_enabled && config.sync_server) {
|
|
140
|
+
out.update("Syncing to server");
|
|
141
|
+
const { failed } = await waitForSync([rootHandle]);
|
|
193
142
|
if (failed.length > 0) {
|
|
194
|
-
|
|
143
|
+
out.taskLine("Root document failed to sync to server", true);
|
|
195
144
|
// Continue anyway - the document is created locally and will sync later
|
|
196
145
|
}
|
|
197
146
|
}
|
|
198
147
|
// Run initial sync to capture existing files
|
|
199
|
-
|
|
148
|
+
out.update("Running initial sync");
|
|
200
149
|
const result = await syncEngine.sync();
|
|
201
|
-
|
|
150
|
+
out.update("Writing to disk");
|
|
202
151
|
await safeRepoShutdown(repo);
|
|
203
|
-
|
|
204
|
-
|
|
152
|
+
out.done("Initialized");
|
|
153
|
+
out.successBlock("INITIALIZED", rootHandle.url);
|
|
205
154
|
if (result.filesChanged > 0) {
|
|
206
|
-
|
|
155
|
+
out.info(`Synced ${result.filesChanged} ${plural("file", result.filesChanged)}`);
|
|
207
156
|
}
|
|
208
157
|
process.exit();
|
|
209
158
|
}
|
|
210
159
|
/**
|
|
211
160
|
* Run bidirectional sync
|
|
212
161
|
*/
|
|
213
|
-
async function sync(targetPath = ".", options) {
|
|
214
|
-
|
|
162
|
+
export async function sync(targetPath = ".", options) {
|
|
163
|
+
out.task(options.nuclear
|
|
215
164
|
? "Nuclear syncing"
|
|
216
165
|
: options.gentle
|
|
217
166
|
? "Gentle syncing"
|
|
@@ -223,78 +172,78 @@ async function sync(targetPath = ".", options) {
|
|
|
223
172
|
await syncEngine.nuclearReset();
|
|
224
173
|
}
|
|
225
174
|
if (options.dryRun) {
|
|
226
|
-
|
|
175
|
+
out.update("Analyzing changes");
|
|
227
176
|
const preview = await syncEngine.previewChanges();
|
|
228
177
|
if (preview.changes.length === 0 && preview.moves.length === 0) {
|
|
229
|
-
|
|
178
|
+
out.done("Already synced");
|
|
230
179
|
return;
|
|
231
180
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
181
|
+
out.done();
|
|
182
|
+
out.infoBlock("CHANGES");
|
|
183
|
+
out.obj({
|
|
235
184
|
Changes: preview.changes.length.toString(),
|
|
236
185
|
Moves: preview.moves.length > 0 ? preview.moves.length.toString() : undefined,
|
|
237
186
|
});
|
|
238
|
-
|
|
239
|
-
|
|
187
|
+
out.log("");
|
|
188
|
+
out.log("Files:");
|
|
240
189
|
for (const change of preview.changes.slice(0, 10)) {
|
|
241
190
|
const prefix = change.changeType === "local_only"
|
|
242
191
|
? "[local] "
|
|
243
192
|
: change.changeType === "remote_only"
|
|
244
193
|
? "[remote] "
|
|
245
194
|
: "[conflict]";
|
|
246
|
-
|
|
195
|
+
out.log(` ${prefix} ${change.path}`);
|
|
247
196
|
}
|
|
248
197
|
if (preview.changes.length > 10) {
|
|
249
|
-
|
|
198
|
+
out.log(` ... and ${preview.changes.length - 10} more`);
|
|
250
199
|
}
|
|
251
200
|
if (preview.moves.length > 0) {
|
|
252
|
-
|
|
253
|
-
|
|
201
|
+
out.log("");
|
|
202
|
+
out.log("Moves:");
|
|
254
203
|
for (const move of preview.moves.slice(0, 5)) {
|
|
255
|
-
|
|
204
|
+
out.log(` ${move.fromPath} → ${move.toPath}`);
|
|
256
205
|
}
|
|
257
206
|
if (preview.moves.length > 5) {
|
|
258
|
-
|
|
207
|
+
out.log(` ... and ${preview.moves.length - 5} more`);
|
|
259
208
|
}
|
|
260
209
|
}
|
|
261
|
-
|
|
262
|
-
|
|
210
|
+
out.log("");
|
|
211
|
+
out.log("Run without --dry-run to apply these changes");
|
|
263
212
|
}
|
|
264
213
|
else {
|
|
265
214
|
const result = await syncEngine.sync();
|
|
266
|
-
|
|
215
|
+
out.taskLine("Writing to disk");
|
|
267
216
|
await safeRepoShutdown(repo);
|
|
268
217
|
if (result.success) {
|
|
269
|
-
|
|
218
|
+
out.done("Synced");
|
|
270
219
|
if (result.filesChanged === 0 && result.directoriesChanged === 0) {
|
|
271
220
|
}
|
|
272
221
|
else {
|
|
273
|
-
|
|
222
|
+
out.successBlock("SYNCED", `${result.filesChanged} ${plural("file", result.filesChanged)}`);
|
|
274
223
|
}
|
|
275
224
|
if (result.warnings.length > 0) {
|
|
276
|
-
|
|
277
|
-
|
|
225
|
+
out.log("");
|
|
226
|
+
out.warnBlock("WARNINGS", `${result.warnings.length} warnings`);
|
|
278
227
|
for (const warning of result.warnings.slice(0, 5)) {
|
|
279
|
-
|
|
228
|
+
out.log(` ${warning}`);
|
|
280
229
|
}
|
|
281
230
|
if (result.warnings.length > 5) {
|
|
282
|
-
|
|
231
|
+
out.log(` ... and ${result.warnings.length - 5} more`);
|
|
283
232
|
}
|
|
284
233
|
}
|
|
285
234
|
}
|
|
286
235
|
else {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
236
|
+
out.done("partial", false);
|
|
237
|
+
out.warnBlock("PARTIAL", `${result.filesChanged} updated, ${result.errors.length} errors`);
|
|
238
|
+
out.obj({
|
|
290
239
|
Files: result.filesChanged,
|
|
291
240
|
Errors: result.errors.length,
|
|
292
241
|
});
|
|
293
242
|
result.errors
|
|
294
243
|
.slice(0, 5)
|
|
295
|
-
.forEach((error) =>
|
|
244
|
+
.forEach((error) => out.error(`${error.path}: ${error.error.message}`));
|
|
296
245
|
if (result.errors.length > 5) {
|
|
297
|
-
|
|
246
|
+
out.warn(`... and ${result.errors.length - 5} more errors`);
|
|
298
247
|
}
|
|
299
248
|
}
|
|
300
249
|
}
|
|
@@ -303,24 +252,24 @@ async function sync(targetPath = ".", options) {
|
|
|
303
252
|
/**
|
|
304
253
|
* Show differences between local and remote
|
|
305
254
|
*/
|
|
306
|
-
async function diff(targetPath = ".", options) {
|
|
307
|
-
|
|
255
|
+
export async function diff(targetPath = ".", options) {
|
|
256
|
+
out.task("Analyzing changes");
|
|
308
257
|
const { repo, syncEngine } = await setupCommandContext(targetPath, { syncEnabled: false });
|
|
309
258
|
const preview = await syncEngine.previewChanges();
|
|
310
|
-
|
|
259
|
+
out.done();
|
|
311
260
|
if (options.nameOnly) {
|
|
312
261
|
for (const change of preview.changes) {
|
|
313
|
-
|
|
262
|
+
out.log(change.path);
|
|
314
263
|
}
|
|
315
264
|
return;
|
|
316
265
|
}
|
|
317
266
|
if (preview.changes.length === 0) {
|
|
318
|
-
|
|
267
|
+
out.success("No changes detected");
|
|
319
268
|
await safeRepoShutdown(repo);
|
|
320
|
-
|
|
269
|
+
out.exit();
|
|
321
270
|
return;
|
|
322
271
|
}
|
|
323
|
-
|
|
272
|
+
out.warn(`${preview.changes.length} changes detected`);
|
|
324
273
|
for (const change of preview.changes) {
|
|
325
274
|
const prefix = change.changeType === "local_only"
|
|
326
275
|
? "[local] "
|
|
@@ -344,7 +293,7 @@ async function diff(targetPath = ".", options) {
|
|
|
344
293
|
// Skip the header lines and process the diff
|
|
345
294
|
const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
|
|
346
295
|
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
347
|
-
|
|
296
|
+
out.log(`${prefix}${change.path} (content identical)`, "cyan");
|
|
348
297
|
continue;
|
|
349
298
|
}
|
|
350
299
|
// Extract first hunk header and show inline with path
|
|
@@ -354,28 +303,28 @@ async function diff(targetPath = ".", options) {
|
|
|
354
303
|
firstHunk = ` ${lines[0]}`;
|
|
355
304
|
diffLines = lines.slice(1);
|
|
356
305
|
}
|
|
357
|
-
|
|
306
|
+
out.log(`${prefix}${change.path}${firstHunk}`, "cyan");
|
|
358
307
|
for (const line of diffLines) {
|
|
359
308
|
if (line.startsWith("@@")) {
|
|
360
309
|
// Additional hunk headers
|
|
361
|
-
|
|
310
|
+
out.log(line, "dim");
|
|
362
311
|
}
|
|
363
312
|
else if (line.startsWith("+")) {
|
|
364
313
|
// Added line
|
|
365
|
-
|
|
314
|
+
out.log(line, "green");
|
|
366
315
|
}
|
|
367
316
|
else if (line.startsWith("-")) {
|
|
368
317
|
// Removed line
|
|
369
|
-
|
|
318
|
+
out.log(line, "red");
|
|
370
319
|
}
|
|
371
320
|
else if (line.startsWith(" ") || line === "") {
|
|
372
321
|
// Context line or empty
|
|
373
|
-
|
|
322
|
+
out.log(line, "dim");
|
|
374
323
|
}
|
|
375
324
|
}
|
|
376
325
|
}
|
|
377
326
|
catch (error) {
|
|
378
|
-
|
|
327
|
+
out.log(`${prefix}${change.path} (diff error: ${error})`, "cyan");
|
|
379
328
|
}
|
|
380
329
|
}
|
|
381
330
|
await safeRepoShutdown(repo);
|
|
@@ -383,10 +332,10 @@ async function diff(targetPath = ".", options) {
|
|
|
383
332
|
/**
|
|
384
333
|
* Show sync status
|
|
385
334
|
*/
|
|
386
|
-
async function status(targetPath = ".", options = {}) {
|
|
335
|
+
export async function status(targetPath = ".", options = {}) {
|
|
387
336
|
const { repo, syncEngine, config } = await setupCommandContext(targetPath, { syncEnabled: false });
|
|
388
337
|
const syncStatus = await syncEngine.getStatus();
|
|
389
|
-
|
|
338
|
+
out.infoBlock("STATUS");
|
|
390
339
|
const statusInfo = {};
|
|
391
340
|
const fileCount = syncStatus.snapshot?.files.size || 0;
|
|
392
341
|
statusInfo["URL"] = syncStatus.snapshot?.rootDirectoryUrl;
|
|
@@ -409,61 +358,61 @@ async function status(targetPath = ".", options = {}) {
|
|
|
409
358
|
}
|
|
410
359
|
}
|
|
411
360
|
catch (error) {
|
|
412
|
-
|
|
361
|
+
out.warn(`Warning: Could not load detailed info: ${error}`);
|
|
413
362
|
}
|
|
414
363
|
}
|
|
415
364
|
statusInfo["Changes"] = syncStatus.hasChanges
|
|
416
365
|
? `${syncStatus.changeCount} pending`
|
|
417
366
|
: undefined;
|
|
418
367
|
statusInfo["Status"] = !syncStatus.hasChanges ? "up to date" : undefined;
|
|
419
|
-
|
|
368
|
+
out.obj(statusInfo);
|
|
420
369
|
// Show verbose details if requested
|
|
421
370
|
if (options.verbose && syncStatus.snapshot?.rootDirectoryUrl) {
|
|
422
371
|
const rootHandle = await repo.find(syncStatus.snapshot.rootDirectoryUrl);
|
|
423
372
|
const rootDoc = await rootHandle.doc();
|
|
424
373
|
if (rootDoc) {
|
|
425
|
-
|
|
426
|
-
|
|
374
|
+
out.infoBlock("HEADS");
|
|
375
|
+
out.arr(rootHandle.heads());
|
|
427
376
|
if (syncStatus.snapshot && syncStatus.snapshot.files.size > 0) {
|
|
428
|
-
|
|
377
|
+
out.infoBlock("TRACKED FILES");
|
|
429
378
|
const filesObj = {};
|
|
430
379
|
syncStatus.snapshot.files.forEach((entry, filePath) => {
|
|
431
380
|
filesObj[filePath] = entry.url;
|
|
432
381
|
});
|
|
433
|
-
|
|
382
|
+
out.obj(filesObj);
|
|
434
383
|
}
|
|
435
384
|
}
|
|
436
385
|
}
|
|
437
386
|
if (syncStatus.hasChanges && !options.verbose) {
|
|
438
|
-
|
|
387
|
+
out.info("Run 'pushwork diff' to see changes");
|
|
439
388
|
}
|
|
440
389
|
await safeRepoShutdown(repo);
|
|
441
390
|
}
|
|
442
391
|
/**
|
|
443
392
|
* Show sync history
|
|
444
393
|
*/
|
|
445
|
-
async function log(targetPath = ".", _options) {
|
|
394
|
+
export async function log(targetPath = ".", _options) {
|
|
446
395
|
const { repo: logRepo, workingDir } = await setupCommandContext(targetPath, { syncEnabled: false });
|
|
447
396
|
// TODO: Implement history tracking
|
|
448
|
-
const snapshotPath = path.join(workingDir,
|
|
449
|
-
if (await
|
|
397
|
+
const snapshotPath = path.join(workingDir, ConfigManager.CONFIG_DIR, "snapshot.json");
|
|
398
|
+
if (await pathExists(snapshotPath)) {
|
|
450
399
|
const stats = await fs.stat(snapshotPath);
|
|
451
|
-
|
|
452
|
-
|
|
400
|
+
out.infoBlock("HISTORY", "Sync history (stub)");
|
|
401
|
+
out.obj({ "Last sync": stats.mtime.toISOString() });
|
|
453
402
|
}
|
|
454
403
|
else {
|
|
455
|
-
|
|
404
|
+
out.info("No sync history found");
|
|
456
405
|
}
|
|
457
406
|
await safeRepoShutdown(logRepo);
|
|
458
407
|
}
|
|
459
408
|
/**
|
|
460
409
|
* Checkout/restore from previous sync
|
|
461
410
|
*/
|
|
462
|
-
async function checkout(syncId, targetPath = ".", _options) {
|
|
411
|
+
export async function checkout(syncId, targetPath = ".", _options) {
|
|
463
412
|
const { workingDir } = await setupCommandContext(targetPath);
|
|
464
413
|
// TODO: Implement checkout functionality
|
|
465
|
-
|
|
466
|
-
|
|
414
|
+
out.warnBlock("NOT IMPLEMENTED", "Checkout not yet implemented");
|
|
415
|
+
out.obj({
|
|
467
416
|
"Sync ID": syncId,
|
|
468
417
|
Path: workingDir,
|
|
469
418
|
});
|
|
@@ -471,150 +420,149 @@ async function checkout(syncId, targetPath = ".", _options) {
|
|
|
471
420
|
/**
|
|
472
421
|
* Clone an existing synced directory from an AutomergeUrl
|
|
473
422
|
*/
|
|
474
|
-
async function clone(rootUrl, targetPath, options) {
|
|
423
|
+
export async function clone(rootUrl, targetPath, options) {
|
|
475
424
|
// Validate that rootUrl is actually an Automerge URL
|
|
476
425
|
if (!rootUrl.startsWith("automerge:")) {
|
|
477
|
-
|
|
426
|
+
out.error(`Invalid Automerge URL: ${rootUrl}\n` +
|
|
478
427
|
`Expected format: automerge:XXXXX\n` +
|
|
479
428
|
`Usage: pushwork clone <automerge-url> <path>`);
|
|
480
|
-
|
|
429
|
+
out.exit(1);
|
|
481
430
|
}
|
|
482
431
|
const resolvedPath = path.resolve(targetPath);
|
|
483
|
-
|
|
432
|
+
out.task(`Cloning ${rootUrl}`);
|
|
484
433
|
// Check if directory exists and handle --force
|
|
485
|
-
if (await
|
|
434
|
+
if (await pathExists(resolvedPath)) {
|
|
486
435
|
const files = await fs.readdir(resolvedPath);
|
|
487
436
|
if (files.length > 0 && !options.force) {
|
|
488
|
-
|
|
489
|
-
|
|
437
|
+
out.error("Target directory is not empty. Use --force to overwrite");
|
|
438
|
+
out.exit(1);
|
|
490
439
|
}
|
|
491
440
|
}
|
|
492
441
|
else {
|
|
493
|
-
await
|
|
442
|
+
await ensureDirectoryExists(resolvedPath);
|
|
494
443
|
}
|
|
495
444
|
// Check if already initialized
|
|
496
|
-
const syncToolDir = path.join(resolvedPath,
|
|
497
|
-
if (await
|
|
445
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
446
|
+
if (await pathExists(syncToolDir)) {
|
|
498
447
|
if (!options.force) {
|
|
499
|
-
|
|
500
|
-
|
|
448
|
+
out.error("Directory already initialized. Use --force to overwrite");
|
|
449
|
+
out.exit(1);
|
|
501
450
|
}
|
|
502
451
|
await fs.rm(syncToolDir, { recursive: true, force: true });
|
|
503
452
|
}
|
|
504
453
|
// Initialize repository with optional CLI overrides
|
|
505
|
-
|
|
454
|
+
out.update("Setting up repository");
|
|
506
455
|
const { config, repo, syncEngine } = await initializeRepository(resolvedPath, {
|
|
507
456
|
sync_server: options.syncServer,
|
|
508
|
-
sync_server_storage_id: options.syncServerStorageId,
|
|
509
457
|
});
|
|
510
458
|
// Connect to existing root directory and download files
|
|
511
|
-
|
|
459
|
+
out.update("Downloading files");
|
|
512
460
|
await syncEngine.setRootDirectoryUrl(rootUrl);
|
|
513
461
|
const result = await syncEngine.sync();
|
|
514
|
-
|
|
462
|
+
out.update("Writing to disk");
|
|
515
463
|
await safeRepoShutdown(repo);
|
|
516
|
-
|
|
517
|
-
|
|
464
|
+
out.done();
|
|
465
|
+
out.obj({
|
|
518
466
|
Path: resolvedPath,
|
|
519
467
|
Files: `${result.filesChanged} downloaded`,
|
|
520
468
|
Sync: config.sync_server,
|
|
521
469
|
});
|
|
522
|
-
|
|
470
|
+
out.successBlock("CLONED", rootUrl);
|
|
523
471
|
process.exit();
|
|
524
472
|
}
|
|
525
473
|
/**
|
|
526
474
|
* Get the root URL for the current pushwork repository
|
|
527
475
|
*/
|
|
528
|
-
async function url(targetPath = ".") {
|
|
476
|
+
export async function url(targetPath = ".") {
|
|
529
477
|
const resolvedPath = path.resolve(targetPath);
|
|
530
|
-
const syncToolDir = path.join(resolvedPath,
|
|
531
|
-
if (!(await
|
|
532
|
-
|
|
533
|
-
|
|
478
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
479
|
+
if (!(await pathExists(syncToolDir))) {
|
|
480
|
+
out.error("Directory not initialized for sync");
|
|
481
|
+
out.exit(1);
|
|
534
482
|
}
|
|
535
483
|
const snapshotPath = path.join(syncToolDir, "snapshot.json");
|
|
536
|
-
if (!(await
|
|
537
|
-
|
|
538
|
-
|
|
484
|
+
if (!(await pathExists(snapshotPath))) {
|
|
485
|
+
out.error("No snapshot found");
|
|
486
|
+
out.exit(1);
|
|
539
487
|
}
|
|
540
488
|
const snapshotData = await fs.readFile(snapshotPath, "utf-8");
|
|
541
489
|
const snapshot = JSON.parse(snapshotData);
|
|
542
490
|
if (snapshot.rootDirectoryUrl) {
|
|
543
491
|
// Output just the URL for easy use in scripts
|
|
544
|
-
|
|
492
|
+
out.log(snapshot.rootDirectoryUrl);
|
|
545
493
|
}
|
|
546
494
|
else {
|
|
547
|
-
|
|
548
|
-
|
|
495
|
+
out.error("No root URL found in snapshot");
|
|
496
|
+
out.exit(1);
|
|
549
497
|
}
|
|
550
498
|
}
|
|
551
499
|
/**
|
|
552
500
|
* Remove local pushwork data and log URL for recovery
|
|
553
501
|
*/
|
|
554
|
-
async function rm(targetPath = ".") {
|
|
502
|
+
export async function rm(targetPath = ".") {
|
|
555
503
|
const resolvedPath = path.resolve(targetPath);
|
|
556
|
-
const syncToolDir = path.join(resolvedPath,
|
|
557
|
-
if (!(await
|
|
558
|
-
|
|
559
|
-
|
|
504
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
505
|
+
if (!(await pathExists(syncToolDir))) {
|
|
506
|
+
out.error("Directory not initialized for sync");
|
|
507
|
+
out.exit(1);
|
|
560
508
|
}
|
|
561
509
|
// Read the URL before deletion for recovery
|
|
562
510
|
let recoveryUrl = "";
|
|
563
511
|
const snapshotPath = path.join(syncToolDir, "snapshot.json");
|
|
564
|
-
if (await
|
|
512
|
+
if (await pathExists(snapshotPath)) {
|
|
565
513
|
try {
|
|
566
514
|
const snapshotData = await fs.readFile(snapshotPath, "utf-8");
|
|
567
515
|
const snapshot = JSON.parse(snapshotData);
|
|
568
516
|
recoveryUrl = snapshot.rootDirectoryUrl || null;
|
|
569
517
|
}
|
|
570
518
|
catch (error) {
|
|
571
|
-
|
|
572
|
-
|
|
519
|
+
out.error(`Remove failed: ${error}`);
|
|
520
|
+
out.exit(1);
|
|
573
521
|
return;
|
|
574
522
|
}
|
|
575
523
|
}
|
|
576
|
-
|
|
524
|
+
out.task("Removing local pushwork data");
|
|
577
525
|
await fs.rm(syncToolDir, { recursive: true, force: true });
|
|
578
|
-
|
|
579
|
-
|
|
526
|
+
out.done();
|
|
527
|
+
out.warnBlock("REMOVED", recoveryUrl);
|
|
580
528
|
process.exit();
|
|
581
529
|
}
|
|
582
|
-
async function commit(targetPath, _options = {}) {
|
|
583
|
-
|
|
530
|
+
export async function commit(targetPath, _options = {}) {
|
|
531
|
+
out.task("Committing local changes");
|
|
584
532
|
const { repo, syncEngine } = await setupCommandContext(targetPath, { syncEnabled: false });
|
|
585
533
|
const result = await syncEngine.commitLocal();
|
|
586
534
|
await safeRepoShutdown(repo);
|
|
587
|
-
|
|
535
|
+
out.done();
|
|
588
536
|
if (result.errors.length > 0) {
|
|
589
|
-
|
|
590
|
-
result.errors.forEach((error) =>
|
|
591
|
-
|
|
537
|
+
out.errorBlock("ERROR", `${result.errors.length} errors`);
|
|
538
|
+
result.errors.forEach((error) => out.error(error));
|
|
539
|
+
out.exit(1);
|
|
592
540
|
}
|
|
593
|
-
|
|
594
|
-
|
|
541
|
+
out.successBlock("COMMITTED", `${result.filesChanged} files`);
|
|
542
|
+
out.obj({
|
|
595
543
|
Files: result.filesChanged,
|
|
596
544
|
Directories: result.directoriesChanged,
|
|
597
545
|
});
|
|
598
546
|
if (result.warnings.length > 0) {
|
|
599
|
-
result.warnings.forEach((warning) =>
|
|
547
|
+
result.warnings.forEach((warning) => out.warn(warning));
|
|
600
548
|
}
|
|
601
549
|
process.exit();
|
|
602
550
|
}
|
|
603
551
|
/**
|
|
604
552
|
* List tracked files
|
|
605
553
|
*/
|
|
606
|
-
async function ls(targetPath = ".", options = {}) {
|
|
554
|
+
export async function ls(targetPath = ".", options = {}) {
|
|
607
555
|
const { repo, syncEngine } = await setupCommandContext(targetPath, { syncEnabled: false });
|
|
608
556
|
const syncStatus = await syncEngine.getStatus();
|
|
609
557
|
if (!syncStatus.snapshot) {
|
|
610
|
-
|
|
558
|
+
out.error("No snapshot found");
|
|
611
559
|
await safeRepoShutdown(repo);
|
|
612
|
-
|
|
560
|
+
out.exit(1);
|
|
613
561
|
return;
|
|
614
562
|
}
|
|
615
563
|
const files = Array.from(syncStatus.snapshot.files.entries()).sort(([pathA], [pathB]) => pathA.localeCompare(pathB));
|
|
616
564
|
if (files.length === 0) {
|
|
617
|
-
|
|
565
|
+
out.info("No tracked files");
|
|
618
566
|
await safeRepoShutdown(repo);
|
|
619
567
|
return;
|
|
620
568
|
}
|
|
@@ -622,13 +570,13 @@ async function ls(targetPath = ".", options = {}) {
|
|
|
622
570
|
// Long format with URLs
|
|
623
571
|
for (const [filePath, entry] of files) {
|
|
624
572
|
const url = entry?.url || "unknown";
|
|
625
|
-
|
|
573
|
+
out.log(`${filePath} -> ${url}`);
|
|
626
574
|
}
|
|
627
575
|
}
|
|
628
576
|
else {
|
|
629
577
|
// Simple list
|
|
630
578
|
for (const [filePath] of files) {
|
|
631
|
-
|
|
579
|
+
out.log(filePath);
|
|
632
580
|
}
|
|
633
581
|
}
|
|
634
582
|
await safeRepoShutdown(repo);
|
|
@@ -636,19 +584,19 @@ async function ls(targetPath = ".", options = {}) {
|
|
|
636
584
|
/**
|
|
637
585
|
* View or edit configuration
|
|
638
586
|
*/
|
|
639
|
-
async function config(targetPath = ".", options = {}) {
|
|
587
|
+
export async function config(targetPath = ".", options = {}) {
|
|
640
588
|
const resolvedPath = path.resolve(targetPath);
|
|
641
|
-
const syncToolDir = path.join(resolvedPath,
|
|
642
|
-
if (!(await
|
|
643
|
-
|
|
644
|
-
|
|
589
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
590
|
+
if (!(await pathExists(syncToolDir))) {
|
|
591
|
+
out.error("Directory not initialized for sync");
|
|
592
|
+
out.exit(1);
|
|
645
593
|
}
|
|
646
|
-
const configManager = new
|
|
594
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
647
595
|
const config = await configManager.getMerged();
|
|
648
596
|
if (options.list) {
|
|
649
597
|
// List all configuration
|
|
650
|
-
|
|
651
|
-
|
|
598
|
+
out.infoBlock("CONFIGURATION", "Full configuration");
|
|
599
|
+
out.log(JSON.stringify(config, null, 2));
|
|
652
600
|
}
|
|
653
601
|
else if (options.get) {
|
|
654
602
|
// Get specific config value
|
|
@@ -658,44 +606,44 @@ async function config(targetPath = ".", options = {}) {
|
|
|
658
606
|
value = value?.[key];
|
|
659
607
|
}
|
|
660
608
|
if (value !== undefined) {
|
|
661
|
-
|
|
609
|
+
out.log(typeof value === "object" ? JSON.stringify(value, null, 2) : value);
|
|
662
610
|
}
|
|
663
611
|
else {
|
|
664
|
-
|
|
665
|
-
|
|
612
|
+
out.error(`Config key not found: ${options.get}`);
|
|
613
|
+
out.exit(1);
|
|
666
614
|
}
|
|
667
615
|
}
|
|
668
616
|
else {
|
|
669
617
|
// Show basic config info
|
|
670
|
-
|
|
671
|
-
|
|
618
|
+
out.infoBlock("CONFIGURATION");
|
|
619
|
+
out.obj({
|
|
672
620
|
"Sync server": config.sync_server || "default",
|
|
673
621
|
"Sync enabled": config.sync_enabled ? "yes" : "no",
|
|
674
622
|
Exclusions: config.exclude_patterns?.length,
|
|
675
623
|
});
|
|
676
|
-
|
|
677
|
-
|
|
624
|
+
out.log("");
|
|
625
|
+
out.log("Use --list to see full configuration");
|
|
678
626
|
}
|
|
679
627
|
}
|
|
680
628
|
/**
|
|
681
629
|
* Watch a directory and sync after build script completes
|
|
682
630
|
*/
|
|
683
|
-
async function watch(targetPath = ".", options = {}) {
|
|
631
|
+
export async function watch(targetPath = ".", options = {}) {
|
|
684
632
|
const script = options.script || "pnpm build";
|
|
685
633
|
const watchDir = options.watchDir || "src"; // Default to watching 'src' directory
|
|
686
634
|
const verbose = options.verbose || false;
|
|
687
635
|
const { repo, syncEngine, workingDir } = await setupCommandContext(targetPath);
|
|
688
636
|
const absoluteWatchDir = path.resolve(workingDir, watchDir);
|
|
689
637
|
// Check if watch directory exists
|
|
690
|
-
if (!(await
|
|
691
|
-
|
|
638
|
+
if (!(await pathExists(absoluteWatchDir))) {
|
|
639
|
+
out.error(`Watch directory does not exist: ${watchDir}`);
|
|
692
640
|
await safeRepoShutdown(repo);
|
|
693
|
-
|
|
641
|
+
out.exit(1);
|
|
694
642
|
return;
|
|
695
643
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
644
|
+
out.spicyBlock("WATCHING", `${chalk.underline(formatRelativePath(watchDir))} for changes...`);
|
|
645
|
+
out.info(`Build script: ${script}`);
|
|
646
|
+
out.info(`Working directory: ${workingDir}`);
|
|
699
647
|
let isProcessing = false;
|
|
700
648
|
let pendingChange = false;
|
|
701
649
|
// Function to run build and sync
|
|
@@ -707,14 +655,14 @@ async function watch(targetPath = ".", options = {}) {
|
|
|
707
655
|
isProcessing = true;
|
|
708
656
|
pendingChange = false;
|
|
709
657
|
try {
|
|
710
|
-
|
|
658
|
+
out.spicy(`[${new Date().toLocaleTimeString()}] Changes detected...`);
|
|
711
659
|
// Run build script
|
|
712
660
|
const buildResult = await runScript(script, workingDir, verbose);
|
|
713
661
|
if (!buildResult.success) {
|
|
714
|
-
|
|
662
|
+
out.warn("Build script failed");
|
|
715
663
|
if (buildResult.output) {
|
|
716
|
-
|
|
717
|
-
|
|
664
|
+
out.log("");
|
|
665
|
+
out.log(buildResult.output);
|
|
718
666
|
}
|
|
719
667
|
isProcessing = false;
|
|
720
668
|
if (pendingChange) {
|
|
@@ -722,38 +670,38 @@ async function watch(targetPath = ".", options = {}) {
|
|
|
722
670
|
}
|
|
723
671
|
return;
|
|
724
672
|
}
|
|
725
|
-
|
|
673
|
+
out.info("Build completed...");
|
|
726
674
|
// Run sync
|
|
727
|
-
|
|
675
|
+
out.task("Syncing");
|
|
728
676
|
const result = await syncEngine.sync();
|
|
729
677
|
if (result.success) {
|
|
730
678
|
if (result.filesChanged === 0 && result.directoriesChanged === 0) {
|
|
731
|
-
|
|
679
|
+
out.done("Already synced");
|
|
732
680
|
}
|
|
733
681
|
else {
|
|
734
|
-
|
|
682
|
+
out.done(`Synced ${result.filesChanged} ${plural("file", result.filesChanged)}`);
|
|
735
683
|
}
|
|
736
684
|
}
|
|
737
685
|
else {
|
|
738
|
-
|
|
686
|
+
out.warn(`⚠ Partial sync: ${result.filesChanged} updated, ${result.errors.length} errors`);
|
|
739
687
|
result.errors
|
|
740
688
|
.slice(0, 3)
|
|
741
|
-
.forEach((error) =>
|
|
689
|
+
.forEach((error) => out.error(` ${error.path}: ${error.error.message}`));
|
|
742
690
|
if (result.errors.length > 3) {
|
|
743
|
-
|
|
691
|
+
out.warn(` ... and ${result.errors.length - 3} more errors`);
|
|
744
692
|
}
|
|
745
693
|
}
|
|
746
694
|
if (result.warnings.length > 0) {
|
|
747
695
|
result.warnings
|
|
748
696
|
.slice(0, 3)
|
|
749
|
-
.forEach((warning) =>
|
|
697
|
+
.forEach((warning) => out.warn(` ${warning}`));
|
|
750
698
|
if (result.warnings.length > 3) {
|
|
751
|
-
|
|
699
|
+
out.warn(` ... and ${result.warnings.length - 3} more warnings`);
|
|
752
700
|
}
|
|
753
701
|
}
|
|
754
702
|
}
|
|
755
703
|
catch (error) {
|
|
756
|
-
|
|
704
|
+
out.error(`Error during build/sync: ${error}`);
|
|
757
705
|
}
|
|
758
706
|
finally {
|
|
759
707
|
isProcessing = false;
|
|
@@ -771,11 +719,11 @@ async function watch(targetPath = ".", options = {}) {
|
|
|
771
719
|
});
|
|
772
720
|
// Handle graceful shutdown
|
|
773
721
|
const shutdown = async () => {
|
|
774
|
-
|
|
775
|
-
|
|
722
|
+
out.log("");
|
|
723
|
+
out.info("Shutting down...");
|
|
776
724
|
watcher.close();
|
|
777
725
|
await safeRepoShutdown(repo);
|
|
778
|
-
|
|
726
|
+
out.rainbow("Goodbye!");
|
|
779
727
|
process.exit(0);
|
|
780
728
|
};
|
|
781
729
|
process.on("SIGINT", shutdown);
|
|
@@ -791,7 +739,7 @@ async function watch(targetPath = ".", options = {}) {
|
|
|
791
739
|
async function runScript(script, cwd, verbose) {
|
|
792
740
|
return new Promise((resolve) => {
|
|
793
741
|
const [command, ...args] = script.split(" ");
|
|
794
|
-
const child =
|
|
742
|
+
const child = spawn(command, args, {
|
|
795
743
|
cwd,
|
|
796
744
|
stdio: verbose ? "inherit" : "pipe", // Show output directly if verbose, otherwise capture
|
|
797
745
|
shell: true,
|
|
@@ -813,7 +761,7 @@ async function runScript(script, cwd, verbose) {
|
|
|
813
761
|
});
|
|
814
762
|
});
|
|
815
763
|
child.on("error", (error) => {
|
|
816
|
-
|
|
764
|
+
out.error(`Failed to run script: ${error.message}`);
|
|
817
765
|
resolve({
|
|
818
766
|
success: false,
|
|
819
767
|
output: !verbose ? output : undefined,
|
|
@@ -824,22 +772,22 @@ async function runScript(script, cwd, verbose) {
|
|
|
824
772
|
/**
|
|
825
773
|
* Set root directory URL for an existing or new pushwork directory
|
|
826
774
|
*/
|
|
827
|
-
async function root(rootUrl, targetPath = ".", options = {}) {
|
|
775
|
+
export async function root(rootUrl, targetPath = ".", options = {}) {
|
|
828
776
|
if (!rootUrl.startsWith("automerge:")) {
|
|
829
|
-
|
|
777
|
+
out.error(`Invalid Automerge URL: ${rootUrl}\n` +
|
|
830
778
|
`Expected format: automerge:XXXXX`);
|
|
831
|
-
|
|
779
|
+
out.exit(1);
|
|
832
780
|
}
|
|
833
781
|
const resolvedPath = path.resolve(targetPath);
|
|
834
|
-
const syncToolDir = path.join(resolvedPath,
|
|
835
|
-
if (await
|
|
782
|
+
const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
|
|
783
|
+
if (await pathExists(syncToolDir)) {
|
|
836
784
|
if (!options.force) {
|
|
837
|
-
|
|
838
|
-
|
|
785
|
+
out.error("Directory already initialized for pushwork. Use --force to overwrite");
|
|
786
|
+
out.exit(1);
|
|
839
787
|
}
|
|
840
788
|
}
|
|
841
|
-
await
|
|
842
|
-
await
|
|
789
|
+
await ensureDirectoryExists(syncToolDir);
|
|
790
|
+
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
843
791
|
// Create minimal snapshot with just the root URL
|
|
844
792
|
const snapshotPath = path.join(syncToolDir, "snapshot.json");
|
|
845
793
|
const snapshot = {
|
|
@@ -851,9 +799,60 @@ async function root(rootUrl, targetPath = ".", options = {}) {
|
|
|
851
799
|
};
|
|
852
800
|
await fs.writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
853
801
|
// Ensure config exists
|
|
854
|
-
const configManager = new
|
|
802
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
855
803
|
await configManager.initializeWithOverrides({});
|
|
856
|
-
|
|
804
|
+
out.successBlock("ROOT SET", rootUrl);
|
|
805
|
+
process.exit();
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Read a file document by its Automerge URL and print its content
|
|
809
|
+
*/
|
|
810
|
+
export async function read(docUrl, options = {}) {
|
|
811
|
+
if (!docUrl.startsWith("automerge:")) {
|
|
812
|
+
out.error(`Invalid Automerge URL: ${docUrl}\n` +
|
|
813
|
+
`Expected format: automerge:XXXXX`);
|
|
814
|
+
out.exit(1);
|
|
815
|
+
}
|
|
816
|
+
let repo;
|
|
817
|
+
if (options.remote) {
|
|
818
|
+
// Create an ephemeral repo with Subduction to fetch from sync server
|
|
819
|
+
repo = await createEphemeralRepo(DEFAULT_SYNC_SERVER);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
// Read from local pushwork storage
|
|
823
|
+
const ctx = await setupCommandContext(".", { syncEnabled: false });
|
|
824
|
+
repo = ctx.repo;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
const handle = await repo.find(docUrl);
|
|
828
|
+
const doc = await handle.doc();
|
|
829
|
+
if (!doc) {
|
|
830
|
+
out.error("Document not found or unavailable");
|
|
831
|
+
await safeRepoShutdown(repo);
|
|
832
|
+
out.exit(1);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const content = readDocContent(doc.content);
|
|
836
|
+
if (content === null) {
|
|
837
|
+
out.error("Document has no content");
|
|
838
|
+
await safeRepoShutdown(repo);
|
|
839
|
+
out.exit(1);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (content instanceof Uint8Array) {
|
|
843
|
+
process.stdout.write(content);
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
process.stdout.write(content);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
out.error(`Failed to read document: ${error}`);
|
|
851
|
+
await safeRepoShutdown(repo);
|
|
852
|
+
out.exit(1);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
await safeRepoShutdown(repo);
|
|
857
856
|
process.exit();
|
|
858
857
|
}
|
|
859
858
|
function plural(word, count) {
|