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
package/src/cli/index.ts
ADDED
package/src/cli.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import {
|
|
6
|
+
init,
|
|
7
|
+
clone,
|
|
8
|
+
sync,
|
|
9
|
+
diff,
|
|
10
|
+
status,
|
|
11
|
+
log,
|
|
12
|
+
checkout,
|
|
13
|
+
commit,
|
|
14
|
+
url,
|
|
15
|
+
} from "./cli/commands";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wrapper for command actions with consistent error handling
|
|
19
|
+
*/
|
|
20
|
+
function withErrorHandling<T extends any[], R>(
|
|
21
|
+
fn: (...args: T) => Promise<R>
|
|
22
|
+
): (...args: T) => Promise<void> {
|
|
23
|
+
return async (...args: T): Promise<void> => {
|
|
24
|
+
try {
|
|
25
|
+
await fn(...args);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(chalk.red(`Error: ${error}`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const program = new Command();
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.name("pushwork")
|
|
37
|
+
.description("Bidirectional directory synchronization using Automerge CRDTs")
|
|
38
|
+
.version("1.0.0");
|
|
39
|
+
|
|
40
|
+
// Init command
|
|
41
|
+
program
|
|
42
|
+
.command("init")
|
|
43
|
+
.description("Initialize sync in directory")
|
|
44
|
+
.argument("<path>", "Directory path to initialize")
|
|
45
|
+
.option(
|
|
46
|
+
"--sync-server <url>",
|
|
47
|
+
"Custom sync server URL (must be used with --sync-server-storage-id)"
|
|
48
|
+
)
|
|
49
|
+
.option(
|
|
50
|
+
"--sync-server-storage-id <id>",
|
|
51
|
+
"Custom sync server storage ID (must be used with --sync-server)"
|
|
52
|
+
)
|
|
53
|
+
.addHelpText(
|
|
54
|
+
"after",
|
|
55
|
+
`
|
|
56
|
+
Examples:
|
|
57
|
+
pushwork init ./my-folder
|
|
58
|
+
pushwork init ./my-folder --sync-server ws://localhost:3030 --sync-server-storage-id 1d89eba7-f7a4-4e8e-80f2-5f4e2406f507
|
|
59
|
+
|
|
60
|
+
Note: Custom sync server options must always be used together.`
|
|
61
|
+
)
|
|
62
|
+
.action(
|
|
63
|
+
withErrorHandling(async (path: string, options) => {
|
|
64
|
+
// Validate that both sync server options are provided together
|
|
65
|
+
const hasSyncServer = !!options.syncServer;
|
|
66
|
+
const hasSyncServerStorageId = !!options.syncServerStorageId;
|
|
67
|
+
|
|
68
|
+
if (hasSyncServer && !hasSyncServerStorageId) {
|
|
69
|
+
console.error(
|
|
70
|
+
chalk.red("Error: --sync-server requires --sync-server-storage-id")
|
|
71
|
+
);
|
|
72
|
+
console.error(
|
|
73
|
+
chalk.yellow("Both arguments must be provided together.")
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (hasSyncServerStorageId && !hasSyncServer) {
|
|
79
|
+
console.error(
|
|
80
|
+
chalk.red("Error: --sync-server-storage-id requires --sync-server")
|
|
81
|
+
);
|
|
82
|
+
console.error(
|
|
83
|
+
chalk.yellow("Both arguments must be provided together.")
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await init(path, options.syncServer, options.syncServerStorageId);
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Clone command
|
|
93
|
+
program
|
|
94
|
+
.command("clone")
|
|
95
|
+
.description("Clone an existing synced directory")
|
|
96
|
+
.argument("<url>", "AutomergeUrl of root directory to clone")
|
|
97
|
+
.argument("<path>", "Target directory path")
|
|
98
|
+
.option("--force", "Overwrite existing directory")
|
|
99
|
+
.option(
|
|
100
|
+
"--sync-server <url>",
|
|
101
|
+
"Custom sync server URL (must be used with --sync-server-storage-id)"
|
|
102
|
+
)
|
|
103
|
+
.option(
|
|
104
|
+
"--sync-server-storage-id <id>",
|
|
105
|
+
"Custom sync server storage ID (must be used with --sync-server)"
|
|
106
|
+
)
|
|
107
|
+
.addHelpText(
|
|
108
|
+
"after",
|
|
109
|
+
`
|
|
110
|
+
Examples:
|
|
111
|
+
pushwork clone automerge:abc123 ./my-clone
|
|
112
|
+
pushwork clone automerge:abc123 ./my-clone --force
|
|
113
|
+
pushwork clone automerge:abc123 ./my-clone --sync-server ws://localhost:3030 --sync-server-storage-id 1d89eba7-f7a4-4e8e-80f2-5f4e2406f507
|
|
114
|
+
|
|
115
|
+
Note: Custom sync server options must always be used together.`
|
|
116
|
+
)
|
|
117
|
+
.action(
|
|
118
|
+
withErrorHandling(async (url: string, path: string, options) => {
|
|
119
|
+
// Validate that both sync server options are provided together
|
|
120
|
+
const hasSyncServer = !!options.syncServer;
|
|
121
|
+
const hasSyncServerStorageId = !!options.syncServerStorageId;
|
|
122
|
+
|
|
123
|
+
if (hasSyncServer && !hasSyncServerStorageId) {
|
|
124
|
+
console.error(
|
|
125
|
+
chalk.red("Error: --sync-server requires --sync-server-storage-id")
|
|
126
|
+
);
|
|
127
|
+
console.error(
|
|
128
|
+
chalk.yellow("Both arguments must be provided together.")
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (hasSyncServerStorageId && !hasSyncServer) {
|
|
134
|
+
console.error(
|
|
135
|
+
chalk.red("Error: --sync-server-storage-id requires --sync-server")
|
|
136
|
+
);
|
|
137
|
+
console.error(
|
|
138
|
+
chalk.yellow("Both arguments must be provided together.")
|
|
139
|
+
);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await clone(url, path, {
|
|
144
|
+
force: options.force || false,
|
|
145
|
+
dryRun: false,
|
|
146
|
+
verbose: false,
|
|
147
|
+
syncServer: options.syncServer,
|
|
148
|
+
syncServerStorageId: options.syncServerStorageId,
|
|
149
|
+
});
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Commit command
|
|
154
|
+
program
|
|
155
|
+
.command("commit")
|
|
156
|
+
.description("Commit local changes (no network sync)")
|
|
157
|
+
.argument("[path]", "Directory path to commit", ".")
|
|
158
|
+
.option("--dry-run", "Show what would be committed without applying changes")
|
|
159
|
+
.action(
|
|
160
|
+
withErrorHandling(async (path: string, options) => {
|
|
161
|
+
await commit(path, options.dryRun || false);
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Sync command
|
|
166
|
+
program
|
|
167
|
+
.command("sync")
|
|
168
|
+
.description("Run full bidirectional synchronization")
|
|
169
|
+
.option("--dry-run", "Show what would be done without applying changes")
|
|
170
|
+
.option("-v, --verbose", "Verbose output")
|
|
171
|
+
.action(
|
|
172
|
+
withErrorHandling(async (options) => {
|
|
173
|
+
await sync({
|
|
174
|
+
dryRun: options.dryRun || false,
|
|
175
|
+
verbose: options.verbose || false,
|
|
176
|
+
});
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Diff command
|
|
181
|
+
program
|
|
182
|
+
.command("diff")
|
|
183
|
+
.description("Show changes in working directory since last sync")
|
|
184
|
+
.argument("[path]", "Limit diff to specific path", ".")
|
|
185
|
+
.option("--tool <tool>", "Use external diff tool (meld, vimdiff, etc.)")
|
|
186
|
+
.option("--name-only", "Show only changed file names")
|
|
187
|
+
.action(
|
|
188
|
+
withErrorHandling(async (path: string, options) => {
|
|
189
|
+
await diff(path, {
|
|
190
|
+
tool: options.tool,
|
|
191
|
+
nameOnly: options.nameOnly || false,
|
|
192
|
+
dryRun: false,
|
|
193
|
+
verbose: false,
|
|
194
|
+
});
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Status command
|
|
199
|
+
program
|
|
200
|
+
.command("status")
|
|
201
|
+
.description("Show sync status summary")
|
|
202
|
+
.action(
|
|
203
|
+
withErrorHandling(async (options) => {
|
|
204
|
+
await status();
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Log command
|
|
209
|
+
program
|
|
210
|
+
.command("log")
|
|
211
|
+
.description("Show sync history")
|
|
212
|
+
.argument("[path]", "Show history for specific file or directory", ".")
|
|
213
|
+
.option("--oneline", "Compact one-line per sync format")
|
|
214
|
+
.option("--since <date>", "Show syncs since date")
|
|
215
|
+
.option("--limit <n>", "Limit number of syncs shown", "10")
|
|
216
|
+
.action(
|
|
217
|
+
withErrorHandling(async (path: string, options) => {
|
|
218
|
+
await log(path, {
|
|
219
|
+
oneline: options.oneline || false,
|
|
220
|
+
since: options.since,
|
|
221
|
+
limit: parseInt(options.limit),
|
|
222
|
+
dryRun: false,
|
|
223
|
+
verbose: false,
|
|
224
|
+
});
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Checkout command
|
|
229
|
+
program
|
|
230
|
+
.command("checkout")
|
|
231
|
+
.description("Restore directory to state from previous sync")
|
|
232
|
+
.argument("<sync-id>", "Sync ID to restore to")
|
|
233
|
+
.argument("[path]", "Specific path to restore", ".")
|
|
234
|
+
.option("-f, --force", "Force checkout even if there are uncommitted changes")
|
|
235
|
+
.action(
|
|
236
|
+
withErrorHandling(async (syncId: string, path: string, options) => {
|
|
237
|
+
await checkout(syncId, path, {
|
|
238
|
+
force: options.force || false,
|
|
239
|
+
dryRun: false,
|
|
240
|
+
verbose: false,
|
|
241
|
+
});
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// URL command
|
|
246
|
+
program
|
|
247
|
+
.command("url")
|
|
248
|
+
.description("Show the Automerge root URL for this repository")
|
|
249
|
+
.argument("[path]", "Directory path", ".")
|
|
250
|
+
.addHelpText(
|
|
251
|
+
"after",
|
|
252
|
+
`
|
|
253
|
+
Examples:
|
|
254
|
+
pushwork url # Show URL for current directory
|
|
255
|
+
pushwork url ./repo # Show URL for specific directory
|
|
256
|
+
|
|
257
|
+
Note: This command outputs only the URL, making it useful for scripts.`
|
|
258
|
+
)
|
|
259
|
+
.action(
|
|
260
|
+
withErrorHandling(async (path: string) => {
|
|
261
|
+
await url(path);
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Global error handler
|
|
266
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
267
|
+
console.error(
|
|
268
|
+
chalk.red("Unhandled Rejection at:"),
|
|
269
|
+
promise,
|
|
270
|
+
chalk.red("reason:"),
|
|
271
|
+
reason
|
|
272
|
+
);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
process.on("uncaughtException", (error) => {
|
|
277
|
+
console.error(chalk.red("Uncaught Exception:"), error);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Parse arguments
|
|
282
|
+
program.parse();
|
|
283
|
+
|
|
284
|
+
// Show help if no arguments provided
|
|
285
|
+
if (!process.argv.slice(2).length) {
|
|
286
|
+
program.outputHelp();
|
|
287
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { GlobalConfig, DirectoryConfig } from "../types";
|
|
5
|
+
import { pathExists, ensureDirectoryExists } from "../utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configuration manager for pushwork
|
|
9
|
+
*/
|
|
10
|
+
export class ConfigManager {
|
|
11
|
+
private static readonly GLOBAL_CONFIG_DIR = ".pushwork";
|
|
12
|
+
private static readonly CONFIG_FILENAME = "config.json";
|
|
13
|
+
private static readonly LOCAL_CONFIG_DIR = ".pushwork";
|
|
14
|
+
|
|
15
|
+
constructor(private workingDir?: string) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get global configuration path
|
|
19
|
+
*/
|
|
20
|
+
private getGlobalConfigPath(): string {
|
|
21
|
+
return path.join(
|
|
22
|
+
os.homedir(),
|
|
23
|
+
ConfigManager.GLOBAL_CONFIG_DIR,
|
|
24
|
+
ConfigManager.CONFIG_FILENAME
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get local configuration path
|
|
30
|
+
*/
|
|
31
|
+
private getLocalConfigPath(): string {
|
|
32
|
+
if (!this.workingDir) {
|
|
33
|
+
throw new Error("Working directory not set for local config");
|
|
34
|
+
}
|
|
35
|
+
return path.join(
|
|
36
|
+
this.workingDir,
|
|
37
|
+
ConfigManager.LOCAL_CONFIG_DIR,
|
|
38
|
+
ConfigManager.CONFIG_FILENAME
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load global configuration
|
|
44
|
+
*/
|
|
45
|
+
async loadGlobal(): Promise<GlobalConfig | null> {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = this.getGlobalConfigPath();
|
|
48
|
+
if (!(await pathExists(configPath))) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const content = await fs.readFile(configPath, "utf8");
|
|
53
|
+
return JSON.parse(content) as GlobalConfig;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`Failed to load global config: ${error}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save global configuration
|
|
62
|
+
*/
|
|
63
|
+
async saveGlobal(config: GlobalConfig): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
const configPath = this.getGlobalConfigPath();
|
|
66
|
+
await ensureDirectoryExists(path.dirname(configPath));
|
|
67
|
+
|
|
68
|
+
const content = JSON.stringify(config, null, 2);
|
|
69
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(`Failed to save global config: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load local/directory configuration
|
|
77
|
+
*/
|
|
78
|
+
async load(): Promise<DirectoryConfig | null> {
|
|
79
|
+
if (!this.workingDir) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const configPath = this.getLocalConfigPath();
|
|
85
|
+
if (!(await pathExists(configPath))) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const content = await fs.readFile(configPath, "utf8");
|
|
90
|
+
return JSON.parse(content) as DirectoryConfig;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`Failed to load local config: ${error}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Save local/directory configuration
|
|
99
|
+
*/
|
|
100
|
+
async save(config: DirectoryConfig): Promise<void> {
|
|
101
|
+
if (!this.workingDir) {
|
|
102
|
+
throw new Error("Working directory not set for local config");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const configPath = this.getLocalConfigPath();
|
|
107
|
+
await ensureDirectoryExists(path.dirname(configPath));
|
|
108
|
+
|
|
109
|
+
const content = JSON.stringify(config, null, 2);
|
|
110
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`Failed to save local config: ${error}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get merged configuration (global + local)
|
|
118
|
+
*/
|
|
119
|
+
async getMerged(): Promise<DirectoryConfig> {
|
|
120
|
+
const globalConfig = await this.loadGlobal();
|
|
121
|
+
const localConfig = await this.load();
|
|
122
|
+
|
|
123
|
+
// Create default configuration
|
|
124
|
+
const defaultConfig: DirectoryConfig = {
|
|
125
|
+
sync_enabled: true,
|
|
126
|
+
sync_server_storage_id: "3760df37-a4c6-4f66-9ecd-732039a9385d",
|
|
127
|
+
defaults: {
|
|
128
|
+
exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
|
|
129
|
+
large_file_threshold: "100MB",
|
|
130
|
+
},
|
|
131
|
+
diff: {
|
|
132
|
+
show_binary: false,
|
|
133
|
+
},
|
|
134
|
+
sync: {
|
|
135
|
+
move_detection_threshold: 0.8,
|
|
136
|
+
prompt_threshold: 0.5,
|
|
137
|
+
auto_sync: false,
|
|
138
|
+
parallel_operations: 4,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Merge configurations: default < global < local
|
|
143
|
+
let merged = { ...defaultConfig };
|
|
144
|
+
|
|
145
|
+
if (globalConfig) {
|
|
146
|
+
merged = this.mergeConfigs(merged, globalConfig);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (localConfig) {
|
|
150
|
+
merged = this.mergeConfigs(merged, localConfig);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Merge two configuration objects
|
|
158
|
+
*/
|
|
159
|
+
private mergeConfigs(
|
|
160
|
+
base: DirectoryConfig,
|
|
161
|
+
override: Partial<DirectoryConfig> | GlobalConfig
|
|
162
|
+
): DirectoryConfig {
|
|
163
|
+
const merged = { ...base };
|
|
164
|
+
|
|
165
|
+
if ("sync_server" in override && override.sync_server !== undefined) {
|
|
166
|
+
merged.sync_server = override.sync_server;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
"sync_server_storage_id" in override &&
|
|
171
|
+
override.sync_server_storage_id !== undefined
|
|
172
|
+
) {
|
|
173
|
+
merged.sync_server_storage_id = override.sync_server_storage_id;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ("sync_enabled" in override && override.sync_enabled !== undefined) {
|
|
177
|
+
merged.sync_enabled = override.sync_enabled;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle GlobalConfig structure
|
|
181
|
+
if ("exclude_patterns" in override && override.exclude_patterns) {
|
|
182
|
+
merged.defaults.exclude_patterns = override.exclude_patterns;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ("large_file_threshold" in override && override.large_file_threshold) {
|
|
186
|
+
merged.defaults.large_file_threshold = override.large_file_threshold;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle DirectoryConfig structure
|
|
190
|
+
if ("defaults" in override && override.defaults) {
|
|
191
|
+
merged.defaults = { ...merged.defaults, ...override.defaults };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if ("diff" in override && override.diff) {
|
|
195
|
+
// Merge diff settings, ensuring show_binary has a default
|
|
196
|
+
merged.diff = {
|
|
197
|
+
...merged.diff,
|
|
198
|
+
...override.diff,
|
|
199
|
+
show_binary: override.diff.show_binary ?? merged.diff.show_binary,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if ("sync" in override && override.sync) {
|
|
204
|
+
merged.sync = { ...merged.sync, ...override.sync };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return merged;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create default global configuration
|
|
212
|
+
*/
|
|
213
|
+
async createDefaultGlobal(): Promise<void> {
|
|
214
|
+
const defaultGlobal: GlobalConfig = {
|
|
215
|
+
exclude_patterns: [
|
|
216
|
+
".git",
|
|
217
|
+
"node_modules",
|
|
218
|
+
"*.tmp",
|
|
219
|
+
".DS_Store",
|
|
220
|
+
".pushwork",
|
|
221
|
+
],
|
|
222
|
+
large_file_threshold: "100MB",
|
|
223
|
+
sync_server: "wss://sync3.automerge.org",
|
|
224
|
+
sync_server_storage_id: "3760df37-a4c6-4f66-9ecd-732039a9385d",
|
|
225
|
+
diff: {
|
|
226
|
+
show_binary: false,
|
|
227
|
+
},
|
|
228
|
+
sync: {
|
|
229
|
+
move_detection_threshold: 0.8,
|
|
230
|
+
prompt_threshold: 0.5,
|
|
231
|
+
auto_sync: false,
|
|
232
|
+
parallel_operations: 4,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
await this.saveGlobal(defaultGlobal);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if global configuration exists
|
|
241
|
+
*/
|
|
242
|
+
async globalConfigExists(): Promise<boolean> {
|
|
243
|
+
return await pathExists(this.getGlobalConfigPath());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if local configuration exists
|
|
248
|
+
*/
|
|
249
|
+
async localConfigExists(): Promise<boolean> {
|
|
250
|
+
if (!this.workingDir) return false;
|
|
251
|
+
return await pathExists(this.getLocalConfigPath());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get configuration value by path (e.g., 'sync.auto_sync')
|
|
256
|
+
*/
|
|
257
|
+
async getValue(keyPath: string): Promise<any> {
|
|
258
|
+
const config = await this.getMerged();
|
|
259
|
+
|
|
260
|
+
const keys = keyPath.split(".");
|
|
261
|
+
let value: any = config;
|
|
262
|
+
|
|
263
|
+
for (const key of keys) {
|
|
264
|
+
if (value && typeof value === "object" && key in value) {
|
|
265
|
+
value = value[key];
|
|
266
|
+
} else {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set configuration value by path
|
|
276
|
+
*/
|
|
277
|
+
async setValue(keyPath: string, value: any): Promise<void> {
|
|
278
|
+
const config = (await this.load()) || ({} as DirectoryConfig);
|
|
279
|
+
|
|
280
|
+
const keys = keyPath.split(".");
|
|
281
|
+
let current: any = config;
|
|
282
|
+
|
|
283
|
+
// Navigate to the parent of the target key
|
|
284
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
285
|
+
const key = keys[i];
|
|
286
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
287
|
+
current[key] = {};
|
|
288
|
+
}
|
|
289
|
+
current = current[key];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Set the value
|
|
293
|
+
const finalKey = keys[keys.length - 1];
|
|
294
|
+
current[finalKey] = value;
|
|
295
|
+
|
|
296
|
+
await this.save(config);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Validate configuration
|
|
301
|
+
*/
|
|
302
|
+
validate(config: DirectoryConfig): { valid: boolean; errors: string[] } {
|
|
303
|
+
const errors: string[] = [];
|
|
304
|
+
|
|
305
|
+
if (config.sync?.move_detection_threshold !== undefined) {
|
|
306
|
+
if (
|
|
307
|
+
config.sync.move_detection_threshold < 0 ||
|
|
308
|
+
config.sync.move_detection_threshold > 1
|
|
309
|
+
) {
|
|
310
|
+
errors.push("move_detection_threshold must be between 0 and 1");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (config.sync?.prompt_threshold !== undefined) {
|
|
315
|
+
if (
|
|
316
|
+
config.sync.prompt_threshold < 0 ||
|
|
317
|
+
config.sync.prompt_threshold > 1
|
|
318
|
+
) {
|
|
319
|
+
errors.push("prompt_threshold must be between 0 and 1");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (config.sync?.parallel_operations !== undefined) {
|
|
324
|
+
if (config.sync.parallel_operations < 1) {
|
|
325
|
+
errors.push("parallel_operations must be at least 1");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
valid: errors.length === 0,
|
|
331
|
+
errors,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|