windows-development-sandbox 1.1.0 → 1.4.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 +3 -2
- package/config.example.json +38 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/agent-commands.js +1 -1
- package/dist/cli/commands/context.js +2 -1
- package/dist/cli/commands/data/clean.js +6 -1
- package/dist/cli/commands/data/collect.js +3 -3
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch.js +36 -3
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +35 -2
- package/dist/cli/commands/sessions.js +3 -3
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +5 -0
- package/dist/cli/menu/apps.d.ts +21 -0
- package/dist/cli/menu/apps.js +122 -0
- package/dist/cli/menu/command-form.d.ts +28 -0
- package/dist/cli/menu/command-form.js +219 -0
- package/dist/cli/menu/index.d.ts +26 -0
- package/dist/cli/menu/index.js +180 -0
- package/dist/cli/menu/settings/categories.d.ts +9 -0
- package/dist/cli/menu/settings/categories.js +28 -0
- package/dist/cli/menu/settings/config-file.d.ts +40 -0
- package/dist/cli/menu/settings/config-file.js +71 -0
- package/dist/cli/menu/settings/editors.d.ts +30 -0
- package/dist/cli/menu/settings/editors.js +305 -0
- package/dist/cli/menu/settings/index.d.ts +32 -0
- package/dist/cli/menu/settings/index.js +136 -0
- package/dist/cli/menu/settings/schema-node.d.ts +62 -0
- package/dist/cli/menu/settings/schema-node.js +104 -0
- package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
- package/dist/cli/menu/several.js +102 -0
- package/dist/cli/tui/app.d.ts +39 -0
- package/dist/cli/tui/app.js +52 -0
- package/dist/cli/tui/dialogs.d.ts +57 -0
- package/dist/cli/tui/dialogs.js +162 -0
- package/dist/cli/tui/frame.d.ts +18 -0
- package/dist/cli/tui/frame.js +27 -0
- package/dist/cli/tui/index.d.ts +7 -0
- package/dist/cli/tui/index.js +8 -0
- package/dist/cli/tui/list-screen.d.ts +101 -0
- package/dist/cli/tui/list-screen.js +299 -0
- package/dist/cli/tui/terminal.d.ts +37 -0
- package/dist/cli/tui/terminal.js +89 -0
- package/dist/cli/tui/text.d.ts +20 -0
- package/dist/cli/tui/text.js +105 -0
- package/dist/cli/tui/theme.d.ts +45 -0
- package/dist/cli/tui/theme.js +30 -0
- package/dist/core/config.js +9 -6
- package/dist/core/local-config.d.ts +15 -0
- package/dist/core/local-config.js +63 -4
- package/dist/core/schema/apps.d.ts +40 -0
- package/dist/core/schema/apps.js +27 -0
- package/dist/core/schema/config.d.ts +66 -1
- package/dist/core/schema/config.js +18 -6
- package/dist/core/schema/profile.d.ts +5 -0
- package/dist/core/schema/profile.js +4 -2
- package/dist/core/schema/sandbox.d.ts +28 -0
- package/dist/core/schema/sandbox.js +28 -0
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.js +22 -5
- package/dist/guest/apps/index.d.ts +10 -0
- package/dist/guest/apps/index.js +206 -0
- package/dist/guest/apps/resolvers.d.ts +8 -0
- package/dist/guest/apps/resolvers.js +90 -0
- package/dist/guest/audit-parse.d.ts +7 -0
- package/dist/guest/audit-parse.js +24 -0
- package/dist/guest/audit.d.ts +4 -0
- package/dist/guest/audit.js +47 -0
- package/dist/guest/daemon/relays.d.ts +1 -0
- package/dist/guest/daemon/relays.js +18 -0
- package/dist/guest/daemon.js +4 -0
- package/dist/guest/env.d.ts +1 -0
- package/dist/guest/env.js +3 -1
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +15 -2
- package/dist/guest/netproxy.d.ts +1 -0
- package/dist/guest/netproxy.js +100 -0
- package/dist/guest/ptyhost.js +14 -4
- package/dist/guest/setup/claude.d.ts +0 -4
- package/dist/guest/setup/claude.js +48 -2
- package/dist/guest/setup/network.d.ts +9 -0
- package/dist/guest/setup/network.js +97 -4
- package/dist/guest/setup/user.js +4 -0
- package/dist/guest/toolchains/native/mesa.js +13 -3
- package/dist/guest/toolchains/plan.d.ts +3 -1
- package/dist/guest/toolchains/plan.js +53 -9
- package/dist/guest/toolchains/python.js +26 -2
- package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
- package/dist/guest/toolchains/vscode-profile.js +69 -0
- package/dist/guest/toolchains/vscode.d.ts +17 -0
- package/dist/guest/toolchains/vscode.js +87 -1
- package/dist/host/attach.js +2 -1
- package/dist/host/audit/collect.d.ts +34 -0
- package/dist/host/audit/collect.js +126 -0
- package/dist/host/audit/log.d.ts +46 -0
- package/dist/host/audit/log.js +143 -0
- package/dist/host/generate.d.ts +6 -0
- package/dist/host/generate.js +10 -0
- package/dist/host/launch/mappings.js +5 -1
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/motw.d.ts +17 -0
- package/dist/host/motw.js +74 -0
- package/dist/host/platform.js +3 -0
- package/dist/host/port-forward.d.ts +8 -0
- package/dist/host/port-forward.js +37 -0
- package/dist/host/repo-guard.d.ts +10 -3
- package/dist/host/repo-guard.js +142 -14
- package/dist/host/stops.d.ts +67 -0
- package/dist/host/stops.js +126 -0
- package/dist/host/terminal.d.ts +8 -0
- package/dist/host/terminal.js +45 -1
- package/dist/host/workspace-view/index.d.ts +20 -0
- package/dist/host/workspace-view/index.js +53 -0
- package/dist/host/workspace-view/rules.d.ts +26 -0
- package/dist/host/workspace-view/rules.js +93 -0
- package/dist/host/workspace-view/view.d.ts +50 -0
- package/dist/host/workspace-view/view.js +281 -0
- package/dist/runtime/daemon.js +1319 -552
- package/dist/runtime/db.js +168 -13
- package/dist/runtime/main.js +2898 -608
- package/dist/runtime/netproxy.js +25936 -0
- package/dist/runtime/view.js +272 -23
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +7 -0
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +45 -0
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +5 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/local-config.test.js +16 -3
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +1 -1
- package/dist/selftest/repo-guard.test.js +59 -1
- package/dist/selftest/sync.test.js +50 -10
- package/dist/selftest/toolchains.test.js +30 -0
- package/dist/selftest/workspace-view.test.d.ts +2 -0
- package/dist/selftest/workspace-view.test.js +89 -0
- package/dist/shared/apps.d.ts +49 -0
- package/dist/shared/apps.js +114 -0
- package/dist/shared/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/pty-client.d.ts +9 -4
- package/dist/shared/pty-client.js +139 -17
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +10 -0
- package/dist/shared/schema.js +2 -0
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/claude-mem.js +3 -2
- package/dist/sync/history.d.ts +6 -0
- package/dist/sync/history.js +26 -11
- package/dist/sync/watcher.js +78 -6
- package/package.json +2 -1
- package/sandbox/cargoshim.cs +56 -0
- package/sandbox/cpucap.cs +33 -0
- package/sandbox/cpucap.ps1 +4 -35
- package/sandbox/logon.cmd +6 -0
- package/sandbox/memlog.ps1 +34 -0
- package/sandbox/pagefile.cs +31 -0
- package/sandbox/pagefile.ps1 +21 -0
- package/sandbox/ptyhost.cs +105 -7
- package/schemas/config.schema.json +303 -2
- package/schemas/sandbox-config.schema.json +126 -12
- package/dist/cli/menu-multi.js +0 -82
- package/dist/cli/menu.d.ts +0 -14
- package/dist/cli/menu.js +0 -290
- package/dist/cli/settings/edit.d.ts +0 -13
- package/dist/cli/settings/edit.js +0 -19
- package/dist/cli/settings/index.d.ts +0 -8
- package/dist/cli/settings/index.js +0 -131
- package/dist/cli/settings/prompts.d.ts +0 -7
- package/dist/cli/settings/prompts.js +0 -48
- package/dist/cli/settings/schema.d.ts +0 -33
- package/dist/cli/settings/schema.js +0 -29
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// A project as the sandbox sees it with workspaceFilter on: a copy of the project on this PC (in storage, outside the
|
|
2
|
+
// project) that holds only what git does not ignore, kept in step with the project both ways. The sandbox maps the view
|
|
3
|
+
// instead of the project, and the project's real .git separately (a "gitdir:" file in the view points at it), so git
|
|
4
|
+
// works as usual. What the sandbox creates that is ignored (node_modules, build output) stays in the view; what is
|
|
5
|
+
// ignored in your project (.env, your node_modules) never enters the sandbox.
|
|
6
|
+
//
|
|
7
|
+
// Each file is compared on both sides with what both looked like after the last sync (size + time): a change on one
|
|
8
|
+
// side is copied to the other, a deletion is repeated, and a file changed on both sides keeps yours in the project and
|
|
9
|
+
// the sandbox's next to it as <name>.sandbox-conflict-<time><ext>. Copies keep the file time, so an unchanged file is
|
|
10
|
+
// never copied again. Links are never followed or created on either side.
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { readValid, writeJsonAtomic } from '../../shared/rundir.js';
|
|
16
|
+
import { IgnoreRules } from './rules.js';
|
|
17
|
+
const Manifest = z.record(z.string().max(1024), z.tuple([z.number(), z.number(), z.number(), z.number()]));
|
|
18
|
+
/** Most entries one walk looks at: a sandbox can create files faster than they are worth syncing. */
|
|
19
|
+
const MAX_ENTRIES = 300000;
|
|
20
|
+
const stamp = (file) => {
|
|
21
|
+
try {
|
|
22
|
+
const s = fs.lstatSync(file);
|
|
23
|
+
return s.isFile() ? [s.size, Math.round(s.mtimeMs)] : [-1, -1];
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [-1, -1];
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const sameContent = (a, b) => {
|
|
30
|
+
const hash = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
|
|
31
|
+
try {
|
|
32
|
+
return fs.statSync(a).size === fs.statSync(b).size && hash(a) === hash(b);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/** A path from an event or the manifest: relative, forward slashes, no way out of the root. */
|
|
39
|
+
const safeRel = (rel) => {
|
|
40
|
+
const clean = rel.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
41
|
+
if (!clean || clean.length > 1000 || clean.split('/').some((p) => p === '..' || p === '' || p.includes(':')))
|
|
42
|
+
return null;
|
|
43
|
+
return clean;
|
|
44
|
+
};
|
|
45
|
+
export class WorkspaceView {
|
|
46
|
+
o;
|
|
47
|
+
rules;
|
|
48
|
+
seen;
|
|
49
|
+
constructor(o) {
|
|
50
|
+
this.o = o;
|
|
51
|
+
this.rules = new IgnoreRules(o.host, o.exclude, o.include);
|
|
52
|
+
this.seen = readValid(Manifest, o.state) ?? {};
|
|
53
|
+
}
|
|
54
|
+
/** Where a project's view and its sync state live: <storage>\views\<id> and <storage>\views\<id>.json. */
|
|
55
|
+
static pathsFor(storageRoot, host) {
|
|
56
|
+
const id = path.basename(host).replace(/[^A-Za-z0-9._-]/g, '_') + '-' + crypto.createHash('sha256').update(path.resolve(host).toLowerCase()).digest('hex').slice(0, 8);
|
|
57
|
+
const view = path.join(storageRoot, 'views', id);
|
|
58
|
+
return { host, view, state: view + '.json' };
|
|
59
|
+
}
|
|
60
|
+
abs(root, rel) { return path.join(root, ...rel.split('/')); }
|
|
61
|
+
/** Creates the view (with its .git file) and brings it fully in step with the project. */
|
|
62
|
+
prepare() {
|
|
63
|
+
fs.mkdirSync(this.o.view, { recursive: true });
|
|
64
|
+
const gitFile = path.join(this.o.view, '.git');
|
|
65
|
+
if (this.o.guestGitDir)
|
|
66
|
+
fs.writeFileSync(gitFile, 'gitdir: ' + this.o.guestGitDir.replace(/\\/g, '/') + '\n');
|
|
67
|
+
else
|
|
68
|
+
fs.rmSync(gitFile, { force: true });
|
|
69
|
+
return this.syncAll();
|
|
70
|
+
}
|
|
71
|
+
/** Every file not ignored, on one side (bounded, links skipped, ignored folders not entered). */
|
|
72
|
+
walk(root, under = '') {
|
|
73
|
+
const files = [];
|
|
74
|
+
// An ignored folder is only entered when one of your include patterns could match inside it; then only files those
|
|
75
|
+
// patterns let in are taken.
|
|
76
|
+
const queue = [{ rel: under, ignored: under !== '' && this.rules.isIgnored(under, true) }];
|
|
77
|
+
let count = 0;
|
|
78
|
+
while (queue.length) {
|
|
79
|
+
const { rel, ignored } = queue.shift();
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(this.abs(root, rel), { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
if (++count > MAX_ENTRIES) {
|
|
89
|
+
this.o.note?.('stopped listing ' + root + ' after ' + MAX_ENTRIES + ' entries');
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
const child = rel ? rel + '/' + e.name : e.name;
|
|
93
|
+
if (e.isSymbolicLink() || child === '.git')
|
|
94
|
+
continue;
|
|
95
|
+
if (e.isDirectory()) {
|
|
96
|
+
const childIgnored = ignored || this.rules.isIgnoredChild(child, true);
|
|
97
|
+
if (!childIgnored || this.rules.mayIncludeInside(child))
|
|
98
|
+
queue.push({ rel: child, ignored: childIgnored });
|
|
99
|
+
}
|
|
100
|
+
else if (e.isFile() && ((!ignored && !this.rules.isIgnoredChild(child, false)) || this.rules.isForced(child)))
|
|
101
|
+
files.push(child);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return files;
|
|
105
|
+
}
|
|
106
|
+
/** A full pass over both sides and everything synced before. */
|
|
107
|
+
syncAll() {
|
|
108
|
+
this.rules.reset();
|
|
109
|
+
const all = new Set([...this.walk(this.o.host), ...this.walk(this.o.view), ...Object.keys(this.seen)]);
|
|
110
|
+
return this.syncPaths(all);
|
|
111
|
+
}
|
|
112
|
+
/** Paths that changed (from file events): files, or folders whose contents are walked. */
|
|
113
|
+
syncChanged(rels) {
|
|
114
|
+
const files = new Set();
|
|
115
|
+
for (const raw of rels) {
|
|
116
|
+
const rel = safeRel(raw);
|
|
117
|
+
if (!rel)
|
|
118
|
+
continue;
|
|
119
|
+
if (rel.endsWith('.gitignore')) {
|
|
120
|
+
this.rules.reset();
|
|
121
|
+
return this.syncAll();
|
|
122
|
+
}
|
|
123
|
+
const isDir = [this.o.host, this.o.view].some((root) => { try {
|
|
124
|
+
return fs.lstatSync(this.abs(root, rel)).isDirectory();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
} });
|
|
129
|
+
if (isDir) {
|
|
130
|
+
if (this.rules.isIgnored(rel, true) && !this.rules.mayIncludeInside(rel))
|
|
131
|
+
continue;
|
|
132
|
+
for (const f of [...this.walk(this.o.host, rel), ...this.walk(this.o.view, rel)])
|
|
133
|
+
files.add(f);
|
|
134
|
+
for (const k of Object.keys(this.seen))
|
|
135
|
+
if (k.startsWith(rel + '/'))
|
|
136
|
+
files.add(k);
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
files.add(rel);
|
|
140
|
+
}
|
|
141
|
+
return this.syncPaths(files);
|
|
142
|
+
}
|
|
143
|
+
syncPaths(rels) {
|
|
144
|
+
const stats = { toView: 0, toHost: 0, deleted: 0, conflicts: 0, skipped: 0 };
|
|
145
|
+
for (const rel of rels) {
|
|
146
|
+
if (this.rules.isIgnored(rel)) {
|
|
147
|
+
delete this.seen[rel];
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
this.syncOne(rel, stats);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
stats.skipped++;
|
|
155
|
+
this.o.note?.('could not sync ' + rel + ': ' + (e instanceof Error ? e.message : String(e)));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
writeJsonAtomic(this.o.state, this.seen);
|
|
159
|
+
return stats;
|
|
160
|
+
}
|
|
161
|
+
syncOne(rel, stats) {
|
|
162
|
+
const hostFile = this.abs(this.o.host, rel);
|
|
163
|
+
const viewFile = this.abs(this.o.view, rel);
|
|
164
|
+
const [hs, hm] = stamp(hostFile);
|
|
165
|
+
const [vs, vm] = stamp(viewFile);
|
|
166
|
+
const before = this.seen[rel];
|
|
167
|
+
const record = () => { const h = stamp(hostFile); const v = stamp(viewFile); if (h[0] < 0 && v[0] < 0)
|
|
168
|
+
delete this.seen[rel];
|
|
169
|
+
else
|
|
170
|
+
this.seen[rel] = [h[0], h[1], v[0], v[1]]; };
|
|
171
|
+
if (hs < 0 && vs < 0) {
|
|
172
|
+
delete this.seen[rel];
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const hostChanged = !before || before[0] !== hs || before[1] !== hm;
|
|
176
|
+
const viewChanged = !before || before[2] !== vs || before[3] !== vm;
|
|
177
|
+
if (!hostChanged && !viewChanged)
|
|
178
|
+
return;
|
|
179
|
+
if (hostChanged && !viewChanged) {
|
|
180
|
+
this.carry(hostFile, viewFile, hs < 0, stats, 'toView');
|
|
181
|
+
record();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (viewChanged && !hostChanged) {
|
|
185
|
+
this.carry(viewFile, hostFile, vs < 0, stats, 'toHost');
|
|
186
|
+
record();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Changed on both sides (or never synced): the same content is no conflict.
|
|
190
|
+
if (hs >= 0 && vs >= 0 && sameContent(hostFile, viewFile)) {
|
|
191
|
+
record();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (hs < 0) {
|
|
195
|
+
this.carry(viewFile, hostFile, false, stats, 'toHost');
|
|
196
|
+
record();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (vs < 0) {
|
|
200
|
+
this.carry(hostFile, viewFile, false, stats, 'toView');
|
|
201
|
+
record();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Yours stays in the project; the sandbox's version is kept next to it, and the view gets yours.
|
|
205
|
+
const ext = path.extname(hostFile);
|
|
206
|
+
const conflict = hostFile.slice(0, hostFile.length - ext.length) + '.sandbox-conflict-' + new Date().toISOString().replace(/\D/g, '').slice(0, 14) + ext;
|
|
207
|
+
fs.copyFileSync(viewFile, conflict);
|
|
208
|
+
this.carry(hostFile, viewFile, false, stats, 'toView');
|
|
209
|
+
stats.conflicts++;
|
|
210
|
+
this.o.note?.('both sides changed ' + rel + ': kept yours, the sandbox\'s version is ' + path.basename(conflict));
|
|
211
|
+
record();
|
|
212
|
+
}
|
|
213
|
+
/** Copies (keeping the file time) or deletes on the other side. */
|
|
214
|
+
carry(from, to, deleted, stats, way) {
|
|
215
|
+
if (deleted) {
|
|
216
|
+
if (stamp(to)[0] < 0)
|
|
217
|
+
return;
|
|
218
|
+
fs.rmSync(to, { force: true });
|
|
219
|
+
this.pruneEmpty(path.dirname(to), way === 'toView' ? this.o.view : this.o.host);
|
|
220
|
+
stats.deleted++;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Never write through a link that is already there.
|
|
224
|
+
try {
|
|
225
|
+
if (fs.lstatSync(to).isSymbolicLink())
|
|
226
|
+
throw new Error('a link is in the way');
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
if (e.code !== 'ENOENT')
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
233
|
+
fs.copyFileSync(from, to);
|
|
234
|
+
const t = fs.statSync(from).mtime;
|
|
235
|
+
fs.utimesSync(to, t, t);
|
|
236
|
+
stats[way]++;
|
|
237
|
+
}
|
|
238
|
+
pruneEmpty(dir, root) {
|
|
239
|
+
for (let d = dir; d.length > root.length && d.startsWith(root); d = path.dirname(d)) {
|
|
240
|
+
try {
|
|
241
|
+
if (fs.readdirSync(d).length)
|
|
242
|
+
return;
|
|
243
|
+
fs.rmdirSync(d);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Keeps both sides in step while it runs: file events from both, batched every 300 ms, plus a full pass every
|
|
252
|
+
* `fullEveryMs` to catch anything an event missed. Returns a function that stops it.
|
|
253
|
+
*/
|
|
254
|
+
watch(fullEveryMs = 60000) {
|
|
255
|
+
const pending = new Set();
|
|
256
|
+
let timer;
|
|
257
|
+
const flush = () => {
|
|
258
|
+
timer = undefined;
|
|
259
|
+
const batch = [...pending];
|
|
260
|
+
pending.clear();
|
|
261
|
+
const s = this.syncChanged(batch);
|
|
262
|
+
if (s.toHost || s.toView || s.deleted || s.conflicts)
|
|
263
|
+
this.o.note?.('synced: ' + s.toView + ' in, ' + s.toHost + ' out, ' + s.deleted + ' deleted, ' + s.conflicts + ' conflicts');
|
|
264
|
+
};
|
|
265
|
+
const onEvent = (_event, file) => {
|
|
266
|
+
if (!file)
|
|
267
|
+
return;
|
|
268
|
+
pending.add(String(file));
|
|
269
|
+
timer ??= setTimeout(flush, 300);
|
|
270
|
+
};
|
|
271
|
+
const watchers = [this.o.host, this.o.view].map((dir) => fs.watch(dir, { recursive: true }, onEvent));
|
|
272
|
+
const full = setInterval(() => { try {
|
|
273
|
+
this.syncAll();
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
this.o.note?.('full sync failed: ' + (e instanceof Error ? e.message : String(e)));
|
|
277
|
+
} }, fullEveryMs);
|
|
278
|
+
return () => { watchers.forEach((w) => w.close()); clearInterval(full); clearTimeout(timer); if (pending.size)
|
|
279
|
+
flush(); };
|
|
280
|
+
}
|
|
281
|
+
}
|