pushwork 1.0.0
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 +460 -0
- package/dist/browser/browser-sync-engine.d.ts +64 -0
- package/dist/browser/browser-sync-engine.d.ts.map +1 -0
- package/dist/browser/browser-sync-engine.js +303 -0
- package/dist/browser/browser-sync-engine.js.map +1 -0
- package/dist/browser/filesystem-adapter.d.ts +84 -0
- package/dist/browser/filesystem-adapter.d.ts.map +1 -0
- package/dist/browser/filesystem-adapter.js +413 -0
- package/dist/browser/filesystem-adapter.js.map +1 -0
- package/dist/browser/index.d.ts +36 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +90 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/types.d.ts +70 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +6 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cli/commands.d.ts +71 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +794 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +199 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +314 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/change-detection.d.ts +78 -0
- package/dist/core/change-detection.d.ts.map +1 -0
- package/dist/core/change-detection.js +370 -0
- package/dist/core/change-detection.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +22 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/isomorphic-snapshot.d.ts +58 -0
- package/dist/core/isomorphic-snapshot.d.ts.map +1 -0
- package/dist/core/isomorphic-snapshot.js +204 -0
- package/dist/core/isomorphic-snapshot.js.map +1 -0
- package/dist/core/move-detection.d.ts +72 -0
- package/dist/core/move-detection.d.ts.map +1 -0
- package/dist/core/move-detection.js +200 -0
- package/dist/core/move-detection.js.map +1 -0
- package/dist/core/snapshot.d.ts +109 -0
- package/dist/core/snapshot.d.ts.map +1 -0
- package/dist/core/snapshot.js +263 -0
- package/dist/core/snapshot.js.map +1 -0
- package/dist/core/sync-engine.d.ts +110 -0
- package/dist/core/sync-engine.d.ts.map +1 -0
- package/dist/core/sync-engine.js +817 -0
- package/dist/core/sync-engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/browser-filesystem.d.ts +26 -0
- package/dist/platform/browser-filesystem.d.ts.map +1 -0
- package/dist/platform/browser-filesystem.js +91 -0
- package/dist/platform/browser-filesystem.js.map +1 -0
- package/dist/platform/filesystem.d.ts +29 -0
- package/dist/platform/filesystem.d.ts.map +1 -0
- package/dist/platform/filesystem.js +65 -0
- package/dist/platform/filesystem.js.map +1 -0
- package/dist/platform/node-filesystem.d.ts +21 -0
- package/dist/platform/node-filesystem.d.ts.map +1 -0
- package/dist/platform/node-filesystem.js +93 -0
- package/dist/platform/node-filesystem.js.map +1 -0
- package/dist/types/config.d.ts +119 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/documents.d.ts +70 -0
- package/dist/types/documents.d.ts.map +1 -0
- package/dist/types/documents.js +23 -0
- package/dist/types/documents.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/snapshot.d.ts +81 -0
- package/dist/types/snapshot.d.ts.map +1 -0
- package/dist/types/snapshot.js +17 -0
- package/dist/types/snapshot.js.map +1 -0
- package/dist/utils/content-similarity.d.ts +53 -0
- package/dist/utils/content-similarity.d.ts.map +1 -0
- package/dist/utils/content-similarity.js +155 -0
- package/dist/utils/content-similarity.js.map +1 -0
- package/dist/utils/content.d.ts +5 -0
- package/dist/utils/content.d.ts.map +1 -0
- package/dist/utils/content.js +30 -0
- package/dist/utils/content.js.map +1 -0
- package/dist/utils/fs-browser.d.ts +57 -0
- package/dist/utils/fs-browser.d.ts.map +1 -0
- package/dist/utils/fs-browser.js +311 -0
- package/dist/utils/fs-browser.js.map +1 -0
- package/dist/utils/fs-node.d.ts +53 -0
- package/dist/utils/fs-node.d.ts.map +1 -0
- package/dist/utils/fs-node.js +220 -0
- package/dist/utils/fs-node.js.map +1 -0
- package/dist/utils/fs.d.ts +62 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +293 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/isomorphic.d.ts +29 -0
- package/dist/utils/isomorphic.d.ts.map +1 -0
- package/dist/utils/isomorphic.js +139 -0
- package/dist/utils/isomorphic.js.map +1 -0
- package/dist/utils/mime-types.d.ts +13 -0
- package/dist/utils/mime-types.d.ts.map +1 -0
- package/dist/utils/mime-types.js +240 -0
- package/dist/utils/mime-types.js.map +1 -0
- package/dist/utils/network-sync.d.ts +12 -0
- package/dist/utils/network-sync.d.ts.map +1 -0
- package/dist/utils/network-sync.js +149 -0
- package/dist/utils/network-sync.js.map +1 -0
- package/dist/utils/pure.d.ts +25 -0
- package/dist/utils/pure.d.ts.map +1 -0
- package/dist/utils/pure.js +112 -0
- package/dist/utils/pure.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +11 -0
- package/dist/utils/repo-factory.d.ts.map +1 -0
- package/dist/utils/repo-factory.js +77 -0
- package/dist/utils/repo-factory.js.map +1 -0
- package/package.json +83 -0
- package/src/cli/commands.ts +1053 -0
- package/src/cli/index.ts +2 -0
- package/src/cli.ts +287 -0
- package/src/config/index.ts +334 -0
- package/src/core/change-detection.ts +484 -0
- package/src/core/index.ts +5 -0
- package/src/core/move-detection.ts +269 -0
- package/src/core/snapshot.ts +285 -0
- package/src/core/sync-engine.ts +1167 -0
- package/src/index.ts +14 -0
- package/src/types/config.ts +130 -0
- package/src/types/documents.ts +72 -0
- package/src/types/index.ts +8 -0
- package/src/types/snapshot.ts +88 -0
- package/src/utils/content-similarity.ts +194 -0
- package/src/utils/content.ts +28 -0
- package/src/utils/fs.ts +289 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/mime-types.ts +236 -0
- package/src/utils/network-sync.ts +153 -0
- package/src/utils/repo-factory.ts +58 -0
- package/test/README-TESTING-GAPS.md +174 -0
- package/test/integration/README.md +328 -0
- package/test/integration/clone-test.sh +310 -0
- package/test/integration/conflict-resolution-test.sh +309 -0
- package/test/integration/deletion-behavior-test.sh +487 -0
- package/test/integration/deletion-sync-test-simple.sh +193 -0
- package/test/integration/deletion-sync-test.sh +297 -0
- package/test/integration/exclude-patterns.test.ts +152 -0
- package/test/integration/full-integration-test.sh +363 -0
- package/test/integration/sync-deletion.test.ts +339 -0
- package/test/integration/sync-flow.test.ts +309 -0
- package/test/run-tests.sh +225 -0
- package/test/unit/content-similarity.test.ts +236 -0
- package/test/unit/deletion-behavior.test.ts +260 -0
- package/test/unit/enhanced-mime-detection.test.ts +266 -0
- package/test/unit/snapshot.test.ts +431 -0
- package/test/unit/sync-timing.test.ts +178 -0
- package/test/unit/utils.test.ts +368 -0
- package/tools/browser-sync/README.md +116 -0
- package/tools/browser-sync/package.json +44 -0
- package/tools/browser-sync/patchwork.json +1 -0
- package/tools/browser-sync/pnpm-lock.yaml +4202 -0
- package/tools/browser-sync/src/components/BrowserSyncTool.tsx +599 -0
- package/tools/browser-sync/src/index.ts +20 -0
- package/tools/browser-sync/src/polyfills.ts +31 -0
- package/tools/browser-sync/src/styles.css +290 -0
- package/tools/browser-sync/src/types.ts +27 -0
- package/tools/browser-sync/vite.config.ts +25 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import { Repo, StorageId, AutomergeUrl } from "@automerge/automerge-repo";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import * as diffLib from "diff";
|
|
7
|
+
import {
|
|
8
|
+
InitOptions,
|
|
9
|
+
CloneOptions,
|
|
10
|
+
SyncOptions,
|
|
11
|
+
DiffOptions,
|
|
12
|
+
LogOptions,
|
|
13
|
+
CheckoutOptions,
|
|
14
|
+
DirectoryConfig,
|
|
15
|
+
DirectoryDocument,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { SyncEngine } from "../core";
|
|
18
|
+
import { DetectedChange } from "../core/change-detection";
|
|
19
|
+
import { pathExists, ensureDirectoryExists } from "../utils";
|
|
20
|
+
import { ConfigManager } from "../config";
|
|
21
|
+
import { createRepo } from "../utils/repo-factory";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shared context that commands can use
|
|
25
|
+
*/
|
|
26
|
+
export interface CommandContext {
|
|
27
|
+
repo: Repo;
|
|
28
|
+
syncEngine: SyncEngine;
|
|
29
|
+
config: DirectoryConfig;
|
|
30
|
+
workingDir: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shared pre-action that ensures repository and sync engine are properly initialized
|
|
35
|
+
* This function always works, with or without network connectivity
|
|
36
|
+
*/
|
|
37
|
+
export async function setupCommandContext(
|
|
38
|
+
workingDir: string = process.cwd(),
|
|
39
|
+
customSyncServer?: string,
|
|
40
|
+
customStorageId?: string,
|
|
41
|
+
enableNetwork: boolean = true
|
|
42
|
+
): Promise<CommandContext> {
|
|
43
|
+
const resolvedPath = path.resolve(workingDir);
|
|
44
|
+
|
|
45
|
+
// Check if initialized
|
|
46
|
+
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
47
|
+
if (!(await pathExists(syncToolDir))) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Directory not initialized for sync. Run "pushwork init" first.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load configuration
|
|
54
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
55
|
+
const config = await configManager.getMerged();
|
|
56
|
+
|
|
57
|
+
// Create repo with configurable network setting
|
|
58
|
+
const repo = await createRepo(resolvedPath, {
|
|
59
|
+
enableNetwork,
|
|
60
|
+
syncServer: customSyncServer,
|
|
61
|
+
syncServerStorageId: customStorageId,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create sync engine with configurable network sync
|
|
65
|
+
const syncEngine = new SyncEngine(
|
|
66
|
+
repo,
|
|
67
|
+
resolvedPath,
|
|
68
|
+
config.defaults.exclude_patterns,
|
|
69
|
+
enableNetwork,
|
|
70
|
+
config.sync_server_storage_id
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
repo,
|
|
75
|
+
syncEngine,
|
|
76
|
+
config,
|
|
77
|
+
workingDir: resolvedPath,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Safely shutdown a repository with proper error handling
|
|
83
|
+
*/
|
|
84
|
+
export async function safeRepoShutdown(
|
|
85
|
+
repo: Repo,
|
|
86
|
+
context?: string
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
try {
|
|
89
|
+
await repo.shutdown();
|
|
90
|
+
} catch (shutdownError) {
|
|
91
|
+
// WebSocket errors during shutdown are common and non-critical
|
|
92
|
+
// Only warn about unexpected shutdown errors
|
|
93
|
+
const errorMessage =
|
|
94
|
+
shutdownError instanceof Error
|
|
95
|
+
? shutdownError.message
|
|
96
|
+
: String(shutdownError);
|
|
97
|
+
if (
|
|
98
|
+
!errorMessage.includes("WebSocket") &&
|
|
99
|
+
!errorMessage.includes("connection was established")
|
|
100
|
+
) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`Warning: Repository shutdown failed${
|
|
103
|
+
context ? ` (${context})` : ""
|
|
104
|
+
}: ${shutdownError}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Common progress message helpers
|
|
112
|
+
*/
|
|
113
|
+
export const ProgressMessages = {
|
|
114
|
+
// Setup messages
|
|
115
|
+
directoryFound: () => console.log(chalk.gray(" ā Sync directory found")),
|
|
116
|
+
configLoaded: () => console.log(chalk.gray(" ā Configuration loaded")),
|
|
117
|
+
repoConnected: () => console.log(chalk.gray(" ā Connected to repository")),
|
|
118
|
+
|
|
119
|
+
// Configuration display
|
|
120
|
+
syncServer: (server: string) =>
|
|
121
|
+
console.log(chalk.gray(` ā Sync server: ${server}`)),
|
|
122
|
+
storageId: (id: string) => console.log(chalk.gray(` ā Storage ID: ${id}`)),
|
|
123
|
+
rootUrl: (url: string) => console.log(chalk.gray(` ā Root URL: ${url}`)),
|
|
124
|
+
|
|
125
|
+
// Operation completion
|
|
126
|
+
changesWritten: () =>
|
|
127
|
+
console.log(chalk.gray(" ā All changes written to disk")),
|
|
128
|
+
syncCompleted: (duration: number) =>
|
|
129
|
+
console.log(chalk.gray(` ā Initial sync completed in ${duration}ms`)),
|
|
130
|
+
directoryStructureCreated: () =>
|
|
131
|
+
console.log(chalk.gray(" ā Created sync directory structure")),
|
|
132
|
+
configSaved: () => console.log(chalk.gray(" ā Saved configuration")),
|
|
133
|
+
repoCreated: () =>
|
|
134
|
+
console.log(chalk.gray(" ā Created Automerge repository")),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Show actual content diff for a changed file
|
|
139
|
+
*/
|
|
140
|
+
async function showContentDiff(change: DetectedChange): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
// Get old content (from snapshot/remote)
|
|
143
|
+
const oldContent = change.remoteContent || "";
|
|
144
|
+
|
|
145
|
+
// Get new content (current local)
|
|
146
|
+
const newContent = change.localContent || "";
|
|
147
|
+
|
|
148
|
+
// Convert binary content to string representation if needed
|
|
149
|
+
const oldText =
|
|
150
|
+
typeof oldContent === "string"
|
|
151
|
+
? oldContent
|
|
152
|
+
: `<binary content: ${oldContent.length} bytes>`;
|
|
153
|
+
const newText =
|
|
154
|
+
typeof newContent === "string"
|
|
155
|
+
? newContent
|
|
156
|
+
: `<binary content: ${newContent.length} bytes>`;
|
|
157
|
+
|
|
158
|
+
// Generate unified diff
|
|
159
|
+
const diffResult = diffLib.createPatch(
|
|
160
|
+
change.path,
|
|
161
|
+
oldText,
|
|
162
|
+
newText,
|
|
163
|
+
"previous",
|
|
164
|
+
"current"
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Skip the header lines and process the diff
|
|
168
|
+
const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
|
|
169
|
+
|
|
170
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
171
|
+
console.log(chalk.gray(" (content identical)"));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
if (line.startsWith("@@")) {
|
|
177
|
+
// Hunk header
|
|
178
|
+
console.log(chalk.cyan(line));
|
|
179
|
+
} else if (line.startsWith("+")) {
|
|
180
|
+
// Added line
|
|
181
|
+
console.log(chalk.green(line));
|
|
182
|
+
} else if (line.startsWith("-")) {
|
|
183
|
+
// Removed line
|
|
184
|
+
console.log(chalk.red(line));
|
|
185
|
+
} else if (line.startsWith(" ")) {
|
|
186
|
+
// Context line
|
|
187
|
+
console.log(chalk.gray(line));
|
|
188
|
+
} else if (line === "") {
|
|
189
|
+
// Empty line
|
|
190
|
+
console.log("");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.log(chalk.gray(` (diff error: ${error})`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Initialize sync in a directory
|
|
200
|
+
*/
|
|
201
|
+
export async function init(
|
|
202
|
+
targetPath: string,
|
|
203
|
+
syncServer?: string,
|
|
204
|
+
syncServerStorageId?: string
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const spinner = ora("Starting initialization...").start();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const resolvedPath = path.resolve(targetPath);
|
|
210
|
+
|
|
211
|
+
// Step 1: Directory setup
|
|
212
|
+
spinner.text = "Setting up directory structure...";
|
|
213
|
+
await ensureDirectoryExists(resolvedPath);
|
|
214
|
+
|
|
215
|
+
// Check if already initialized
|
|
216
|
+
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
217
|
+
if (await pathExists(syncToolDir)) {
|
|
218
|
+
spinner.fail("Directory already initialized for sync");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Step 2: Create sync directories
|
|
223
|
+
spinner.text = "Creating .pushwork directory...";
|
|
224
|
+
await ensureDirectoryExists(syncToolDir);
|
|
225
|
+
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
226
|
+
|
|
227
|
+
ProgressMessages.directoryStructureCreated();
|
|
228
|
+
|
|
229
|
+
// Step 3: Configuration setup
|
|
230
|
+
spinner.text = "Setting up configuration...";
|
|
231
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
232
|
+
const defaultSyncServer = syncServer || "wss://sync3.automerge.org";
|
|
233
|
+
const defaultStorageId =
|
|
234
|
+
syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
235
|
+
const config: DirectoryConfig = {
|
|
236
|
+
sync_server: defaultSyncServer,
|
|
237
|
+
sync_server_storage_id: defaultStorageId,
|
|
238
|
+
sync_enabled: true,
|
|
239
|
+
defaults: {
|
|
240
|
+
exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
|
|
241
|
+
large_file_threshold: "100MB",
|
|
242
|
+
},
|
|
243
|
+
diff: {
|
|
244
|
+
show_binary: false,
|
|
245
|
+
},
|
|
246
|
+
sync: {
|
|
247
|
+
move_detection_threshold: 0.8,
|
|
248
|
+
prompt_threshold: 0.5,
|
|
249
|
+
auto_sync: false,
|
|
250
|
+
parallel_operations: 4,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
await configManager.save(config);
|
|
254
|
+
|
|
255
|
+
ProgressMessages.configSaved();
|
|
256
|
+
ProgressMessages.syncServer(defaultSyncServer);
|
|
257
|
+
ProgressMessages.storageId(defaultStorageId);
|
|
258
|
+
|
|
259
|
+
// Step 4: Initialize Automerge repo and create root directory document
|
|
260
|
+
spinner.text = "Creating root directory document...";
|
|
261
|
+
const repo = await createRepo(resolvedPath, {
|
|
262
|
+
enableNetwork: true,
|
|
263
|
+
syncServer: syncServer,
|
|
264
|
+
syncServerStorageId: syncServerStorageId,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Create the root directory document
|
|
268
|
+
const rootDoc: DirectoryDocument = {
|
|
269
|
+
"@patchwork": { type: "folder" },
|
|
270
|
+
docs: [],
|
|
271
|
+
};
|
|
272
|
+
const rootHandle = repo.create(rootDoc);
|
|
273
|
+
|
|
274
|
+
ProgressMessages.repoCreated();
|
|
275
|
+
ProgressMessages.rootUrl(rootHandle.url);
|
|
276
|
+
|
|
277
|
+
// Step 5: Scan existing files
|
|
278
|
+
spinner.text = "Scanning existing files...";
|
|
279
|
+
const syncEngine = new SyncEngine(
|
|
280
|
+
repo,
|
|
281
|
+
resolvedPath,
|
|
282
|
+
config.defaults.exclude_patterns,
|
|
283
|
+
true, // Network sync enabled for init
|
|
284
|
+
config.sync_server_storage_id
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Get file count for progress
|
|
288
|
+
const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
289
|
+
const fileCount = dirEntries.filter((dirent: any) =>
|
|
290
|
+
dirent.isFile()
|
|
291
|
+
).length;
|
|
292
|
+
|
|
293
|
+
if (fileCount > 0) {
|
|
294
|
+
console.log(chalk.gray(` ā Found ${fileCount} existing files`));
|
|
295
|
+
spinner.text = `Creating initial snapshot with ${fileCount} files...`;
|
|
296
|
+
} else {
|
|
297
|
+
spinner.text = "Creating initial empty snapshot...";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Step 6: Set the root directory URL before creating initial snapshot
|
|
301
|
+
await syncEngine.setRootDirectoryUrl(rootHandle.url);
|
|
302
|
+
|
|
303
|
+
// Step 7: Create initial snapshot
|
|
304
|
+
spinner.text = "Creating initial snapshot...";
|
|
305
|
+
const startTime = Date.now();
|
|
306
|
+
await syncEngine.sync(false);
|
|
307
|
+
const duration = Date.now() - startTime;
|
|
308
|
+
|
|
309
|
+
ProgressMessages.syncCompleted(duration);
|
|
310
|
+
|
|
311
|
+
// Step 8: Ensure all Automerge operations are flushed to disk
|
|
312
|
+
spinner.text = "Flushing changes to disk...";
|
|
313
|
+
await safeRepoShutdown(repo, "init");
|
|
314
|
+
ProgressMessages.changesWritten();
|
|
315
|
+
|
|
316
|
+
spinner.succeed(`Initialized sync in ${chalk.green(resolvedPath)}`);
|
|
317
|
+
|
|
318
|
+
console.log(`\n${chalk.bold("š Sync Directory Created!")}`);
|
|
319
|
+
console.log(` š Directory: ${chalk.blue(resolvedPath)}`);
|
|
320
|
+
console.log(` š Sync server: ${chalk.blue(defaultSyncServer)}`);
|
|
321
|
+
console.log(
|
|
322
|
+
`\n${chalk.green("Initialization complete!")} Run ${chalk.cyan(
|
|
323
|
+
"pushwork sync"
|
|
324
|
+
)} to start syncing.`
|
|
325
|
+
);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
spinner.fail(`Failed to initialize: ${error}`);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Run bidirectional sync
|
|
334
|
+
*/
|
|
335
|
+
export async function sync(options: SyncOptions): Promise<void> {
|
|
336
|
+
const spinner = ora("Starting sync operation...").start();
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Step 1: Setup shared context
|
|
340
|
+
spinner.text = "Setting up sync context...";
|
|
341
|
+
const { repo, syncEngine, config, workingDir } =
|
|
342
|
+
await setupCommandContext();
|
|
343
|
+
|
|
344
|
+
ProgressMessages.directoryFound();
|
|
345
|
+
ProgressMessages.configLoaded();
|
|
346
|
+
ProgressMessages.syncServer(
|
|
347
|
+
config?.sync_server || "wss://sync3.automerge.org"
|
|
348
|
+
);
|
|
349
|
+
ProgressMessages.repoConnected();
|
|
350
|
+
|
|
351
|
+
// Show root directory URL for context
|
|
352
|
+
const syncStatus = await syncEngine.getStatus();
|
|
353
|
+
if (syncStatus.snapshot?.rootDirectoryUrl) {
|
|
354
|
+
ProgressMessages.rootUrl(syncStatus.snapshot.rootDirectoryUrl);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (options.dryRun) {
|
|
358
|
+
// Dry run mode - detailed preview
|
|
359
|
+
spinner.text = "Analyzing changes (dry run)...";
|
|
360
|
+
const startTime = Date.now();
|
|
361
|
+
const preview = await syncEngine.previewChanges();
|
|
362
|
+
const analysisTime = Date.now() - startTime;
|
|
363
|
+
|
|
364
|
+
spinner.succeed("Change analysis completed");
|
|
365
|
+
|
|
366
|
+
console.log(`\n${chalk.bold("š Change Analysis")} (${analysisTime}ms):`);
|
|
367
|
+
console.log(chalk.gray(` Directory: ${workingDir}`));
|
|
368
|
+
console.log(chalk.gray(` Analysis time: ${analysisTime}ms`));
|
|
369
|
+
|
|
370
|
+
if (preview.changes.length === 0 && preview.moves.length === 0) {
|
|
371
|
+
console.log(
|
|
372
|
+
`\n${chalk.green("⨠No changes detected")} - everything is in sync!`
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log(`\n${chalk.bold("š Summary:")}`);
|
|
378
|
+
console.log(` ${preview.summary}`);
|
|
379
|
+
|
|
380
|
+
if (preview.changes.length > 0) {
|
|
381
|
+
const localChanges = preview.changes.filter(
|
|
382
|
+
(c) =>
|
|
383
|
+
c.changeType === "local_only" || c.changeType === "both_changed"
|
|
384
|
+
).length;
|
|
385
|
+
const remoteChanges = preview.changes.filter(
|
|
386
|
+
(c) =>
|
|
387
|
+
c.changeType === "remote_only" || c.changeType === "both_changed"
|
|
388
|
+
).length;
|
|
389
|
+
const conflicts = preview.changes.filter(
|
|
390
|
+
(c) => c.changeType === "both_changed"
|
|
391
|
+
).length;
|
|
392
|
+
|
|
393
|
+
console.log(
|
|
394
|
+
`\n${chalk.bold("š File Changes:")} (${
|
|
395
|
+
preview.changes.length
|
|
396
|
+
} total)`
|
|
397
|
+
);
|
|
398
|
+
if (localChanges > 0) {
|
|
399
|
+
console.log(` ${chalk.green("š¤")} Local changes: ${localChanges}`);
|
|
400
|
+
}
|
|
401
|
+
if (remoteChanges > 0) {
|
|
402
|
+
console.log(` ${chalk.blue("š„")} Remote changes: ${remoteChanges}`);
|
|
403
|
+
}
|
|
404
|
+
if (conflicts > 0) {
|
|
405
|
+
console.log(` ${chalk.yellow("ā ļø")} Conflicts: ${conflicts}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.log(`\n${chalk.bold("š Changed Files:")}`);
|
|
409
|
+
for (const change of preview.changes.slice(0, 10)) {
|
|
410
|
+
// Show first 10
|
|
411
|
+
const typeIcon =
|
|
412
|
+
change.changeType === "local_only"
|
|
413
|
+
? chalk.green("š¤")
|
|
414
|
+
: change.changeType === "remote_only"
|
|
415
|
+
? chalk.blue("š„")
|
|
416
|
+
: change.changeType === "both_changed"
|
|
417
|
+
? chalk.yellow("ā ļø")
|
|
418
|
+
: chalk.gray("ā");
|
|
419
|
+
console.log(` ${typeIcon} ${change.path}`);
|
|
420
|
+
}
|
|
421
|
+
if (preview.changes.length > 10) {
|
|
422
|
+
console.log(
|
|
423
|
+
` ${chalk.gray(
|
|
424
|
+
`... and ${preview.changes.length - 10} more files`
|
|
425
|
+
)}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (preview.moves.length > 0) {
|
|
431
|
+
console.log(
|
|
432
|
+
`\n${chalk.bold("š Potential Moves:")} (${preview.moves.length})`
|
|
433
|
+
);
|
|
434
|
+
for (const move of preview.moves.slice(0, 5)) {
|
|
435
|
+
// Show first 5
|
|
436
|
+
const confidence =
|
|
437
|
+
move.confidence === "auto"
|
|
438
|
+
? chalk.green("Auto")
|
|
439
|
+
: move.confidence === "prompt"
|
|
440
|
+
? chalk.yellow("Prompt")
|
|
441
|
+
: chalk.red("Low");
|
|
442
|
+
console.log(` š ${move.fromPath} ā ${move.toPath} (${confidence})`);
|
|
443
|
+
}
|
|
444
|
+
if (preview.moves.length > 5) {
|
|
445
|
+
console.log(
|
|
446
|
+
` ${chalk.gray(`... and ${preview.moves.length - 5} more moves`)}`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log(
|
|
452
|
+
`\n${chalk.cyan("ā¹ļø Run without --dry-run to apply these changes")}`
|
|
453
|
+
);
|
|
454
|
+
} else {
|
|
455
|
+
// Actual sync operation
|
|
456
|
+
spinner.text = "Detecting changes...";
|
|
457
|
+
const startTime = Date.now();
|
|
458
|
+
|
|
459
|
+
const result = await syncEngine.sync(false);
|
|
460
|
+
const totalTime = Date.now() - startTime;
|
|
461
|
+
|
|
462
|
+
if (result.success) {
|
|
463
|
+
spinner.succeed(`Sync completed in ${totalTime}ms`);
|
|
464
|
+
|
|
465
|
+
console.log(`\n${chalk.bold("ā
Sync Results:")}`);
|
|
466
|
+
console.log(` š Files changed: ${chalk.yellow(result.filesChanged)}`);
|
|
467
|
+
console.log(
|
|
468
|
+
` š Directories changed: ${chalk.yellow(result.directoriesChanged)}`
|
|
469
|
+
);
|
|
470
|
+
console.log(` ā±ļø Total time: ${chalk.gray(totalTime + "ms")}`);
|
|
471
|
+
|
|
472
|
+
if (result.warnings.length > 0) {
|
|
473
|
+
console.log(
|
|
474
|
+
`\n${chalk.yellow("ā ļø Warnings:")} (${result.warnings.length})`
|
|
475
|
+
);
|
|
476
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
477
|
+
console.log(` ${chalk.yellow("ā ļø")} ${warning}`);
|
|
478
|
+
}
|
|
479
|
+
if (result.warnings.length > 5) {
|
|
480
|
+
console.log(
|
|
481
|
+
` ${chalk.gray(
|
|
482
|
+
`... and ${result.warnings.length - 5} more warnings`
|
|
483
|
+
)}`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (result.filesChanged === 0 && result.directoriesChanged === 0) {
|
|
489
|
+
console.log(`\n${chalk.green("⨠Everything already in sync!")}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Ensure all changes are flushed to disk
|
|
493
|
+
spinner.text = "Flushing changes to disk...";
|
|
494
|
+
await safeRepoShutdown(repo, "sync");
|
|
495
|
+
ProgressMessages.changesWritten();
|
|
496
|
+
} else {
|
|
497
|
+
spinner.fail("Sync completed with errors");
|
|
498
|
+
|
|
499
|
+
console.log(
|
|
500
|
+
`\n${chalk.red("ā Sync Errors:")} (${result.errors.length})`
|
|
501
|
+
);
|
|
502
|
+
for (const error of result.errors.slice(0, 5)) {
|
|
503
|
+
console.log(
|
|
504
|
+
` ${chalk.red("ā")} ${error.path}: ${error.error.message}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (result.errors.length > 5) {
|
|
508
|
+
console.log(
|
|
509
|
+
` ${chalk.gray(`... and ${result.errors.length - 5} more errors`)}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (result.filesChanged > 0 || result.directoriesChanged > 0) {
|
|
514
|
+
console.log(`\n${chalk.yellow("ā ļø Partial sync completed:")}`);
|
|
515
|
+
console.log(` š Files changed: ${result.filesChanged}`);
|
|
516
|
+
console.log(` š Directories changed: ${result.directoriesChanged}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Still try to flush any partial changes
|
|
520
|
+
await safeRepoShutdown(repo, "sync-error");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
spinner.fail(`Sync failed: ${error}`);
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Show differences between local and remote
|
|
531
|
+
*/
|
|
532
|
+
export async function diff(
|
|
533
|
+
targetPath = ".",
|
|
534
|
+
options: DiffOptions
|
|
535
|
+
): Promise<void> {
|
|
536
|
+
try {
|
|
537
|
+
// Setup shared context with network disabled for diff check
|
|
538
|
+
const { repo, syncEngine } = await setupCommandContext(
|
|
539
|
+
targetPath,
|
|
540
|
+
undefined,
|
|
541
|
+
undefined,
|
|
542
|
+
false
|
|
543
|
+
);
|
|
544
|
+
const preview = await syncEngine.previewChanges();
|
|
545
|
+
|
|
546
|
+
if (options.nameOnly) {
|
|
547
|
+
// Show only file names
|
|
548
|
+
for (const change of preview.changes) {
|
|
549
|
+
console.log(change.path);
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Show root directory URL for context
|
|
555
|
+
const diffStatus = await syncEngine.getStatus();
|
|
556
|
+
if (diffStatus.snapshot?.rootDirectoryUrl) {
|
|
557
|
+
console.log(
|
|
558
|
+
chalk.gray(`Root URL: ${diffStatus.snapshot.rootDirectoryUrl}`)
|
|
559
|
+
);
|
|
560
|
+
console.log("");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (preview.changes.length === 0) {
|
|
564
|
+
console.log(chalk.green("No changes detected"));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
console.log(chalk.bold("Differences:"));
|
|
569
|
+
|
|
570
|
+
for (const change of preview.changes) {
|
|
571
|
+
const typeLabel =
|
|
572
|
+
change.changeType === "local_only"
|
|
573
|
+
? chalk.green("[LOCAL]")
|
|
574
|
+
: change.changeType === "remote_only"
|
|
575
|
+
? chalk.blue("[REMOTE]")
|
|
576
|
+
: change.changeType === "both_changed"
|
|
577
|
+
? chalk.yellow("[CONFLICT]")
|
|
578
|
+
: chalk.gray("[NO CHANGE]");
|
|
579
|
+
|
|
580
|
+
console.log(`\n${typeLabel} ${change.path}`);
|
|
581
|
+
|
|
582
|
+
if (options.tool) {
|
|
583
|
+
console.log(` Use "${options.tool}" to view detailed diff`);
|
|
584
|
+
} else {
|
|
585
|
+
// Show actual diff content
|
|
586
|
+
await showContentDiff(change);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Cleanup repo resources
|
|
591
|
+
await safeRepoShutdown(repo, "diff");
|
|
592
|
+
} catch (error) {
|
|
593
|
+
console.error(chalk.red(`Diff failed: ${error}`));
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Show sync status
|
|
600
|
+
*/
|
|
601
|
+
export async function status(): Promise<void> {
|
|
602
|
+
try {
|
|
603
|
+
const spinner = ora("Loading sync status...").start();
|
|
604
|
+
|
|
605
|
+
// Setup shared context with network disabled for status check
|
|
606
|
+
const { repo, syncEngine, workingDir } = await setupCommandContext(
|
|
607
|
+
process.cwd(),
|
|
608
|
+
undefined,
|
|
609
|
+
undefined,
|
|
610
|
+
false
|
|
611
|
+
);
|
|
612
|
+
const syncStatus = await syncEngine.getStatus();
|
|
613
|
+
|
|
614
|
+
spinner.stop();
|
|
615
|
+
|
|
616
|
+
console.log(chalk.bold("š Sync Status Report"));
|
|
617
|
+
console.log(`${"=".repeat(50)}`);
|
|
618
|
+
|
|
619
|
+
// Directory information
|
|
620
|
+
console.log(`\n${chalk.bold("š Directory Information:")}`);
|
|
621
|
+
console.log(` š Path: ${chalk.blue(workingDir)}`);
|
|
622
|
+
console.log(` š§ Config: ${path.join(workingDir, ".pushwork")}`);
|
|
623
|
+
|
|
624
|
+
// Show root directory URL if available
|
|
625
|
+
if (syncStatus.snapshot?.rootDirectoryUrl) {
|
|
626
|
+
console.log(
|
|
627
|
+
` š Root URL: ${chalk.cyan(syncStatus.snapshot.rootDirectoryUrl)}`
|
|
628
|
+
);
|
|
629
|
+
} else {
|
|
630
|
+
console.log(` š Root URL: ${chalk.yellow("Not set")}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Sync timing
|
|
634
|
+
if (syncStatus.lastSync) {
|
|
635
|
+
const timeSince = Date.now() - syncStatus.lastSync.getTime();
|
|
636
|
+
const timeAgo =
|
|
637
|
+
timeSince < 60000
|
|
638
|
+
? `${Math.floor(timeSince / 1000)}s ago`
|
|
639
|
+
: timeSince < 3600000
|
|
640
|
+
? `${Math.floor(timeSince / 60000)}m ago`
|
|
641
|
+
: `${Math.floor(timeSince / 3600000)}h ago`;
|
|
642
|
+
|
|
643
|
+
console.log(`\n${chalk.bold("ā±ļø Sync Timing:")}`);
|
|
644
|
+
console.log(
|
|
645
|
+
` š Last sync: ${chalk.green(syncStatus.lastSync.toLocaleString())}`
|
|
646
|
+
);
|
|
647
|
+
console.log(` ā³ Time since: ${chalk.gray(timeAgo)}`);
|
|
648
|
+
} else {
|
|
649
|
+
console.log(`\n${chalk.bold("ā±ļø Sync Timing:")}`);
|
|
650
|
+
console.log(` š Last sync: ${chalk.yellow("Never synced")}`);
|
|
651
|
+
console.log(
|
|
652
|
+
` š” Run ${chalk.cyan("pushwork sync")} to perform initial sync`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Change status
|
|
657
|
+
console.log(`\n${chalk.bold("š Change Status:")}`);
|
|
658
|
+
if (syncStatus.hasChanges) {
|
|
659
|
+
console.log(
|
|
660
|
+
` š Pending changes: ${chalk.yellow(syncStatus.changeCount)}`
|
|
661
|
+
);
|
|
662
|
+
console.log(` š Status: ${chalk.yellow("Sync needed")}`);
|
|
663
|
+
console.log(` š” Run ${chalk.cyan("pushwork diff")} to see details`);
|
|
664
|
+
} else {
|
|
665
|
+
console.log(` š Pending changes: ${chalk.green("None")}`);
|
|
666
|
+
console.log(` ā
Status: ${chalk.green("Up to date")}`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Configuration
|
|
670
|
+
console.log(`\n${chalk.bold("āļø Configuration:")}`);
|
|
671
|
+
|
|
672
|
+
const statusConfigManager2 = new ConfigManager(workingDir);
|
|
673
|
+
const statusConfig2 = await statusConfigManager2.load();
|
|
674
|
+
|
|
675
|
+
if (statusConfig2?.sync_server) {
|
|
676
|
+
console.log(` š Sync server: ${chalk.blue(statusConfig2.sync_server)}`);
|
|
677
|
+
} else {
|
|
678
|
+
console.log(
|
|
679
|
+
` š Sync server: ${chalk.blue("wss://sync3.automerge.org")} (default)`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
console.log(
|
|
684
|
+
` ā” Auto sync: ${
|
|
685
|
+
statusConfig2?.sync?.auto_sync
|
|
686
|
+
? chalk.green("Enabled")
|
|
687
|
+
: chalk.gray("Disabled")
|
|
688
|
+
}`
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// Snapshot information
|
|
692
|
+
if (syncStatus.snapshot) {
|
|
693
|
+
const fileCount = syncStatus.snapshot.files.size;
|
|
694
|
+
const dirCount = syncStatus.snapshot.directories.size;
|
|
695
|
+
|
|
696
|
+
console.log(`\n${chalk.bold("š Repository Statistics:")}`);
|
|
697
|
+
console.log(` š Tracked files: ${chalk.yellow(fileCount)}`);
|
|
698
|
+
console.log(` š Tracked directories: ${chalk.yellow(dirCount)}`);
|
|
699
|
+
console.log(
|
|
700
|
+
` š·ļø Snapshot timestamp: ${chalk.gray(
|
|
701
|
+
new Date(syncStatus.snapshot.timestamp).toLocaleString()
|
|
702
|
+
)}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Quick actions
|
|
707
|
+
console.log(`\n${chalk.bold("š Quick Actions:")}`);
|
|
708
|
+
if (syncStatus.hasChanges) {
|
|
709
|
+
console.log(
|
|
710
|
+
` ${chalk.cyan("pushwork diff")} - View pending changes`
|
|
711
|
+
);
|
|
712
|
+
console.log(` ${chalk.cyan("pushwork sync")} - Apply changes`);
|
|
713
|
+
} else {
|
|
714
|
+
console.log(
|
|
715
|
+
` ${chalk.cyan("pushwork sync")} - Check for remote changes`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
console.log(` ${chalk.cyan("pushwork log")} - View sync history`);
|
|
719
|
+
|
|
720
|
+
// Cleanup repo resources
|
|
721
|
+
await safeRepoShutdown(repo, "status");
|
|
722
|
+
} catch (error) {
|
|
723
|
+
console.error(chalk.red(`ā Status check failed: ${error}`));
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Show sync history
|
|
730
|
+
*/
|
|
731
|
+
export async function log(
|
|
732
|
+
targetPath = ".",
|
|
733
|
+
options: LogOptions
|
|
734
|
+
): Promise<void> {
|
|
735
|
+
try {
|
|
736
|
+
// Setup shared context with network disabled for log check
|
|
737
|
+
const {
|
|
738
|
+
repo: logRepo,
|
|
739
|
+
syncEngine: logSyncEngine,
|
|
740
|
+
workingDir,
|
|
741
|
+
} = await setupCommandContext(targetPath, undefined, undefined, false);
|
|
742
|
+
const logStatus = await logSyncEngine.getStatus();
|
|
743
|
+
|
|
744
|
+
if (logStatus.snapshot?.rootDirectoryUrl) {
|
|
745
|
+
console.log(
|
|
746
|
+
chalk.gray(`Root URL: ${logStatus.snapshot.rootDirectoryUrl}`)
|
|
747
|
+
);
|
|
748
|
+
console.log("");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// TODO: Implement history tracking and display
|
|
752
|
+
// For now, show basic information
|
|
753
|
+
|
|
754
|
+
console.log(chalk.bold("Sync History:"));
|
|
755
|
+
|
|
756
|
+
// Check for snapshot files
|
|
757
|
+
const snapshotPath = path.join(workingDir, ".pushwork", "snapshot.json");
|
|
758
|
+
if (await pathExists(snapshotPath)) {
|
|
759
|
+
const stats = await fs.stat(snapshotPath);
|
|
760
|
+
|
|
761
|
+
if (options.oneline) {
|
|
762
|
+
console.log(`${stats.mtime.toISOString()} - Last sync`);
|
|
763
|
+
} else {
|
|
764
|
+
console.log(`Last sync: ${chalk.green(stats.mtime.toISOString())}`);
|
|
765
|
+
console.log(`Snapshot size: ${stats.size} bytes`);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
console.log(chalk.yellow("No sync history found"));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Cleanup repo resources
|
|
772
|
+
await safeRepoShutdown(logRepo, "log");
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.error(chalk.red(`Log failed: ${error}`));
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Checkout/restore from previous sync
|
|
781
|
+
*/
|
|
782
|
+
export async function checkout(
|
|
783
|
+
syncId: string,
|
|
784
|
+
targetPath = ".",
|
|
785
|
+
options: CheckoutOptions
|
|
786
|
+
): Promise<void> {
|
|
787
|
+
try {
|
|
788
|
+
// Setup shared context
|
|
789
|
+
const { workingDir } = await setupCommandContext(targetPath);
|
|
790
|
+
|
|
791
|
+
// TODO: Implement checkout functionality
|
|
792
|
+
// This would involve:
|
|
793
|
+
// 1. Finding the sync with the given ID
|
|
794
|
+
// 2. Restoring file states from that sync
|
|
795
|
+
// 3. Updating the snapshot
|
|
796
|
+
|
|
797
|
+
console.log(chalk.yellow(`Checkout functionality not yet implemented`));
|
|
798
|
+
console.log(`Would restore to sync: ${syncId}`);
|
|
799
|
+
console.log(`Target path: ${workingDir}`);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error(chalk.red(`Checkout failed: ${error}`));
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Clone an existing synced directory from an AutomergeUrl
|
|
808
|
+
*/
|
|
809
|
+
export async function clone(
|
|
810
|
+
rootUrl: string,
|
|
811
|
+
targetPath: string,
|
|
812
|
+
options: CloneOptions
|
|
813
|
+
): Promise<void> {
|
|
814
|
+
const spinner = ora("Starting clone operation...").start();
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const resolvedPath = path.resolve(targetPath);
|
|
818
|
+
|
|
819
|
+
// Step 1: Directory setup
|
|
820
|
+
spinner.text = "Setting up target directory...";
|
|
821
|
+
|
|
822
|
+
// Check if directory exists and handle --force
|
|
823
|
+
if (await pathExists(resolvedPath)) {
|
|
824
|
+
const files = await fs.readdir(resolvedPath);
|
|
825
|
+
if (files.length > 0 && !options.force) {
|
|
826
|
+
spinner.fail(
|
|
827
|
+
"Target directory is not empty. Use --force to overwrite."
|
|
828
|
+
);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
await ensureDirectoryExists(resolvedPath);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check if already initialized
|
|
836
|
+
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
837
|
+
if (await pathExists(syncToolDir)) {
|
|
838
|
+
if (!options.force) {
|
|
839
|
+
spinner.fail(
|
|
840
|
+
"Directory already initialized for sync. Use --force to overwrite."
|
|
841
|
+
);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// Clean up existing sync directory
|
|
845
|
+
await fs.rm(syncToolDir, { recursive: true, force: true });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
console.log(chalk.gray(" ā Target directory prepared"));
|
|
849
|
+
|
|
850
|
+
// Step 2: Create sync directories
|
|
851
|
+
spinner.text = "Creating .pushwork directory...";
|
|
852
|
+
await ensureDirectoryExists(syncToolDir);
|
|
853
|
+
await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
|
|
854
|
+
|
|
855
|
+
ProgressMessages.directoryStructureCreated();
|
|
856
|
+
|
|
857
|
+
// Step 3: Configuration setup
|
|
858
|
+
spinner.text = "Setting up configuration...";
|
|
859
|
+
const configManager = new ConfigManager(resolvedPath);
|
|
860
|
+
const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
|
|
861
|
+
const defaultStorageId =
|
|
862
|
+
options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
863
|
+
const config: DirectoryConfig = {
|
|
864
|
+
sync_server: defaultSyncServer,
|
|
865
|
+
sync_server_storage_id: defaultStorageId,
|
|
866
|
+
sync_enabled: true,
|
|
867
|
+
defaults: {
|
|
868
|
+
exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
|
|
869
|
+
large_file_threshold: "100MB",
|
|
870
|
+
},
|
|
871
|
+
diff: {
|
|
872
|
+
show_binary: false,
|
|
873
|
+
},
|
|
874
|
+
sync: {
|
|
875
|
+
move_detection_threshold: 0.8,
|
|
876
|
+
prompt_threshold: 0.5,
|
|
877
|
+
auto_sync: false,
|
|
878
|
+
parallel_operations: 4,
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
await configManager.save(config);
|
|
882
|
+
|
|
883
|
+
ProgressMessages.configSaved();
|
|
884
|
+
ProgressMessages.syncServer(defaultSyncServer);
|
|
885
|
+
ProgressMessages.storageId(defaultStorageId);
|
|
886
|
+
|
|
887
|
+
// Step 4: Initialize Automerge repo and connect to root directory
|
|
888
|
+
spinner.text = "Connecting to root directory document...";
|
|
889
|
+
const repo = await createRepo(resolvedPath, {
|
|
890
|
+
enableNetwork: true,
|
|
891
|
+
syncServer: options.syncServer,
|
|
892
|
+
syncServerStorageId: options.syncServerStorageId,
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
ProgressMessages.repoCreated();
|
|
896
|
+
ProgressMessages.rootUrl(rootUrl);
|
|
897
|
+
|
|
898
|
+
// Step 5: Initialize sync engine and pull existing structure
|
|
899
|
+
spinner.text = "Downloading directory structure...";
|
|
900
|
+
const syncEngine = new SyncEngine(
|
|
901
|
+
repo,
|
|
902
|
+
resolvedPath,
|
|
903
|
+
config.defaults.exclude_patterns,
|
|
904
|
+
true, // Network sync enabled for clone
|
|
905
|
+
defaultStorageId
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// Set the root directory URL to connect to the cloned repository
|
|
909
|
+
await syncEngine.setRootDirectoryUrl(rootUrl as AutomergeUrl);
|
|
910
|
+
|
|
911
|
+
// Sync to pull the existing directory structure and files
|
|
912
|
+
const startTime = Date.now();
|
|
913
|
+
await syncEngine.sync(false);
|
|
914
|
+
const duration = Date.now() - startTime;
|
|
915
|
+
|
|
916
|
+
console.log(chalk.gray(` ā Directory sync completed in ${duration}ms`));
|
|
917
|
+
|
|
918
|
+
// Ensure all changes are flushed to disk
|
|
919
|
+
spinner.text = "Flushing changes to disk...";
|
|
920
|
+
await safeRepoShutdown(repo, "clone");
|
|
921
|
+
ProgressMessages.changesWritten();
|
|
922
|
+
|
|
923
|
+
spinner.succeed(`Cloned sync directory to ${chalk.green(resolvedPath)}`);
|
|
924
|
+
|
|
925
|
+
console.log(`\n${chalk.bold("š Directory Cloned!")}`);
|
|
926
|
+
console.log(` š Directory: ${chalk.blue(resolvedPath)}`);
|
|
927
|
+
console.log(` š Root URL: ${chalk.cyan(rootUrl)}`);
|
|
928
|
+
console.log(` š Sync server: ${chalk.blue(defaultSyncServer)}`);
|
|
929
|
+
console.log(
|
|
930
|
+
`\n${chalk.green("Clone complete!")} Run ${chalk.cyan(
|
|
931
|
+
"pushwork sync"
|
|
932
|
+
)} to stay in sync.`
|
|
933
|
+
);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
spinner.fail(`Failed to clone: ${error}`);
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Get the root URL for the current pushwork repository
|
|
942
|
+
*/
|
|
943
|
+
export async function url(targetPath = "."): Promise<void> {
|
|
944
|
+
try {
|
|
945
|
+
const resolvedPath = path.resolve(targetPath);
|
|
946
|
+
|
|
947
|
+
// Check if initialized
|
|
948
|
+
const syncToolDir = path.join(resolvedPath, ".pushwork");
|
|
949
|
+
if (!(await pathExists(syncToolDir))) {
|
|
950
|
+
console.error(chalk.red("Directory not initialized for sync"));
|
|
951
|
+
console.error(`Run ${chalk.cyan("pushwork init .")} to get started`);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Load the snapshot directly to get the URL without all the verbose output
|
|
956
|
+
const snapshotPath = path.join(syncToolDir, "snapshot.json");
|
|
957
|
+
if (!(await pathExists(snapshotPath))) {
|
|
958
|
+
console.error(chalk.red("No snapshot found"));
|
|
959
|
+
console.error(
|
|
960
|
+
chalk.gray("The repository may not be properly initialized")
|
|
961
|
+
);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const snapshotData = await fs.readFile(snapshotPath, "utf-8");
|
|
966
|
+
const snapshot = JSON.parse(snapshotData);
|
|
967
|
+
|
|
968
|
+
if (snapshot.rootDirectoryUrl) {
|
|
969
|
+
// Output just the URL for easy use in scripts
|
|
970
|
+
console.log(snapshot.rootDirectoryUrl);
|
|
971
|
+
} else {
|
|
972
|
+
console.error(chalk.red("No root URL found in snapshot"));
|
|
973
|
+
console.error(
|
|
974
|
+
chalk.gray("The repository may not be properly initialized")
|
|
975
|
+
);
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
} catch (error) {
|
|
979
|
+
console.error(chalk.red(`Failed to get URL: ${error}`));
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
export async function commit(
|
|
985
|
+
targetPath: string,
|
|
986
|
+
dryRun: boolean = false
|
|
987
|
+
): Promise<void> {
|
|
988
|
+
const spinner = ora("Starting commit operation...").start();
|
|
989
|
+
let repo: Repo | undefined;
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
// Setup shared context with network disabled for local-only commit
|
|
993
|
+
spinner.text = "Setting up commit context...";
|
|
994
|
+
const context = await setupCommandContext(
|
|
995
|
+
targetPath,
|
|
996
|
+
undefined,
|
|
997
|
+
undefined,
|
|
998
|
+
false
|
|
999
|
+
);
|
|
1000
|
+
repo = context.repo;
|
|
1001
|
+
const syncEngine = context.syncEngine;
|
|
1002
|
+
spinner.succeed("Connected to repository");
|
|
1003
|
+
|
|
1004
|
+
// Run local commit only
|
|
1005
|
+
spinner.text = "Committing local changes...";
|
|
1006
|
+
const startTime = Date.now();
|
|
1007
|
+
const result = await syncEngine.commitLocal(dryRun);
|
|
1008
|
+
const duration = Date.now() - startTime;
|
|
1009
|
+
|
|
1010
|
+
if (repo) {
|
|
1011
|
+
await safeRepoShutdown(repo, "commit");
|
|
1012
|
+
}
|
|
1013
|
+
spinner.succeed(`Commit completed in ${duration}ms`);
|
|
1014
|
+
|
|
1015
|
+
// Display results
|
|
1016
|
+
console.log(chalk.green("\nā
Commit Results:"));
|
|
1017
|
+
console.log(` š Files committed: ${result.filesChanged}`);
|
|
1018
|
+
console.log(` š Directories committed: ${result.directoriesChanged}`);
|
|
1019
|
+
console.log(` ā±ļø Total time: ${duration}ms`);
|
|
1020
|
+
|
|
1021
|
+
if (result.warnings.length > 0) {
|
|
1022
|
+
console.log(chalk.yellow("\nā ļø Warnings:"));
|
|
1023
|
+
result.warnings.forEach((warning: string) =>
|
|
1024
|
+
console.log(chalk.yellow(` ⢠${warning}`))
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (result.errors.length > 0) {
|
|
1029
|
+
console.log(chalk.red("\nā Errors:"));
|
|
1030
|
+
result.errors.forEach((error) =>
|
|
1031
|
+
console.log(
|
|
1032
|
+
chalk.red(
|
|
1033
|
+
` ⢠${error.operation} at ${error.path}: ${error.error.message}`
|
|
1034
|
+
)
|
|
1035
|
+
)
|
|
1036
|
+
);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
console.log(
|
|
1041
|
+
chalk.gray("\nš” Run 'pushwork push' to upload to sync server")
|
|
1042
|
+
);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (repo) {
|
|
1045
|
+
await safeRepoShutdown(repo, "commit-error");
|
|
1046
|
+
}
|
|
1047
|
+
spinner.fail(`Commit failed: ${error}`);
|
|
1048
|
+
console.error(chalk.red(`Error: ${error}`));
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// TODO: Add push and pull commands later
|