harnery 0.41.0 → 0.42.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/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { listAccounts } from "./claude-desktop.js";
|
|
6
|
+
/** macOS st_flags bit for a file whose contents live only in the cloud. */
|
|
7
|
+
const SF_DATALESS = 0x40000000;
|
|
8
|
+
/**
|
|
9
|
+
* Paths whose bytes are not on this disk yet. On macOS this reads st_flags
|
|
10
|
+
* (Node's stat does not expose them) in one `stat` call; everywhere else only
|
|
11
|
+
* the `.<name>.icloud` stub convention applies.
|
|
12
|
+
*/
|
|
13
|
+
export function findPlaceholders(paths) {
|
|
14
|
+
const out = new Set();
|
|
15
|
+
const present = [];
|
|
16
|
+
for (const p of paths) {
|
|
17
|
+
if (existsSync(p))
|
|
18
|
+
present.push(p);
|
|
19
|
+
else if (existsSync(join(dirname(p), `.${p.slice(dirname(p).length + 1)}.icloud`)))
|
|
20
|
+
out.add(p);
|
|
21
|
+
}
|
|
22
|
+
if (process.platform !== "darwin" || present.length === 0)
|
|
23
|
+
return out;
|
|
24
|
+
const r = spawnSync("/usr/bin/stat", ["-f", "%Xf %N", ...present], { encoding: "utf8" });
|
|
25
|
+
for (const line of (r.stdout ?? "").split("\n")) {
|
|
26
|
+
const sp = line.indexOf(" ");
|
|
27
|
+
if (sp < 0)
|
|
28
|
+
continue;
|
|
29
|
+
const flags = Number.parseInt(line.slice(0, sp), 16);
|
|
30
|
+
if (Number.isFinite(flags) && (flags & SF_DATALESS) !== 0)
|
|
31
|
+
out.add(line.slice(sp + 1));
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** Best-effort, fire-and-forget: ask iCloud Drive to fetch these paths. */
|
|
36
|
+
export function requestDownload(paths) {
|
|
37
|
+
if (process.platform !== "darwin")
|
|
38
|
+
return;
|
|
39
|
+
for (const p of paths) {
|
|
40
|
+
try {
|
|
41
|
+
spawn("/usr/bin/brctl", ["download", p], { stdio: "ignore", detached: true }).unref();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Not an iCloud folder or brctl missing; the next pass retries.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function defaultShareDir(home = homedir()) {
|
|
49
|
+
if (process.platform !== "darwin")
|
|
50
|
+
return null;
|
|
51
|
+
const icloud = join(home, "Library", "Mobile Documents", "com~apple~CloudDocs");
|
|
52
|
+
return existsSync(icloud) ? join(icloud, "Harnery", "claude-sessions") : null;
|
|
53
|
+
}
|
|
54
|
+
/** Claude Code's per-project transcript folder name for a working directory. */
|
|
55
|
+
export function projectSlug(cwd) {
|
|
56
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
57
|
+
}
|
|
58
|
+
function transcriptPath(home, cwd, cliSessionId) {
|
|
59
|
+
return join(home, ".claude", "projects", projectSlug(cwd), `${cliSessionId}.jsonl`);
|
|
60
|
+
}
|
|
61
|
+
function safeStat(p) {
|
|
62
|
+
try {
|
|
63
|
+
return statSync(p);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function safeReaddir(dir) {
|
|
70
|
+
try {
|
|
71
|
+
return readdirSync(dir);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function readJson(p) {
|
|
78
|
+
try {
|
|
79
|
+
const v = JSON.parse(readFileSync(p, "utf8"));
|
|
80
|
+
return typeof v === "object" && v !== null ? v : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Rewrite a path from the remote home to the local one (same layout, different user). */
|
|
87
|
+
export function remapHome(p, remoteHome, localHome) {
|
|
88
|
+
if (!remoteHome || remoteHome === localHome)
|
|
89
|
+
return p;
|
|
90
|
+
if (p === remoteHome)
|
|
91
|
+
return localHome;
|
|
92
|
+
return p.startsWith(`${remoteHome}/`) ? localHome + p.slice(remoteHome.length) : p;
|
|
93
|
+
}
|
|
94
|
+
/** True when `prefixFile`'s bytes are exactly the start of `fullFile`. */
|
|
95
|
+
export function isBytePrefix(prefixFile, fullFile) {
|
|
96
|
+
const a = safeStat(prefixFile);
|
|
97
|
+
const b = safeStat(fullFile);
|
|
98
|
+
if (!a || !b || a.size > b.size)
|
|
99
|
+
return false;
|
|
100
|
+
const chunk = 1 << 20;
|
|
101
|
+
const fa = openSync(prefixFile, "r");
|
|
102
|
+
const fb = openSync(fullFile, "r");
|
|
103
|
+
try {
|
|
104
|
+
const ba = Buffer.alloc(chunk);
|
|
105
|
+
const bb = Buffer.alloc(chunk);
|
|
106
|
+
let pos = 0;
|
|
107
|
+
while (pos < a.size) {
|
|
108
|
+
const n = Math.min(chunk, a.size - pos);
|
|
109
|
+
readSync(fa, ba, 0, n, pos);
|
|
110
|
+
readSync(fb, bb, 0, n, pos);
|
|
111
|
+
if (ba.compare(bb, 0, n, 0, n) !== 0)
|
|
112
|
+
return false;
|
|
113
|
+
pos += n;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
closeSync(fa);
|
|
119
|
+
closeSync(fb);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Newest non-archived entry per cliSessionId across every local account. */
|
|
123
|
+
function localEntriesById(accounts) {
|
|
124
|
+
const out = new Map();
|
|
125
|
+
for (const a of accounts) {
|
|
126
|
+
for (const e of a.entries) {
|
|
127
|
+
if (!e.cliSessionId)
|
|
128
|
+
continue;
|
|
129
|
+
const prev = out.get(e.cliSessionId);
|
|
130
|
+
if (!prev || (e.lastActivityAt ?? 0) > (prev.lastActivityAt ?? 0))
|
|
131
|
+
out.set(e.cliSessionId, e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Env directory new entries go into: the one holding the account's newest
|
|
138
|
+
* entry, else any env directory the app already created for the account.
|
|
139
|
+
*/
|
|
140
|
+
function targetEnvDir(account) {
|
|
141
|
+
if (!account)
|
|
142
|
+
return null;
|
|
143
|
+
const newest = account.entries[0];
|
|
144
|
+
if (newest)
|
|
145
|
+
return join(account.path, newest.envId);
|
|
146
|
+
const env = safeReaddir(account.path).find((d) => safeStat(join(account.path, d))?.isDirectory());
|
|
147
|
+
return env ? join(account.path, env) : null;
|
|
148
|
+
}
|
|
149
|
+
export function planShare(opts) {
|
|
150
|
+
const home = opts.home ?? homedir();
|
|
151
|
+
const now = opts.now ?? Date.now();
|
|
152
|
+
const liveWindow = opts.liveWindowMs ?? 3 * 60_000;
|
|
153
|
+
const cutoff = now - opts.days * 86_400_000;
|
|
154
|
+
const accounts = listAccounts(opts.dataDir);
|
|
155
|
+
const local = localEntriesById(accounts);
|
|
156
|
+
// Export: this machine's recent sessions whose transcript exists here and
|
|
157
|
+
// differs from what was last published.
|
|
158
|
+
const exports = [];
|
|
159
|
+
const mine = join(opts.shareDir, opts.machine, "sessions");
|
|
160
|
+
for (const e of local.values()) {
|
|
161
|
+
if (e.isArchived || !e.cwd || !e.cliSessionId)
|
|
162
|
+
continue;
|
|
163
|
+
if ((e.lastActivityAt ?? 0) < cutoff)
|
|
164
|
+
continue;
|
|
165
|
+
const t = transcriptPath(home, e.cwd, e.cliSessionId);
|
|
166
|
+
const st = safeStat(t);
|
|
167
|
+
if (!st)
|
|
168
|
+
continue;
|
|
169
|
+
const published = safeStat(join(mine, e.cliSessionId, "transcript.jsonl"));
|
|
170
|
+
const publishedEntry = readJson(join(mine, e.cliSessionId, "entry.json"));
|
|
171
|
+
const entryCurrent = publishedEntry?.lastActivityAt === e.lastActivityAt && publishedEntry?.title === e.title;
|
|
172
|
+
if (published && published.size === st.size && entryCurrent)
|
|
173
|
+
continue;
|
|
174
|
+
exports.push({ cliSessionId: e.cliSessionId, title: e.title, transcript: t, bytes: st.size });
|
|
175
|
+
}
|
|
176
|
+
// Import: every other machine's published sessions.
|
|
177
|
+
const imports = [];
|
|
178
|
+
const skips = [];
|
|
179
|
+
const machines = [];
|
|
180
|
+
const target = accounts.find((a) => a.accountUuid === opts.targetAccountUuid);
|
|
181
|
+
const envDir = targetEnvDir(target) ??
|
|
182
|
+
join(opts.dataDir, "claude-code-sessions", opts.targetAccountUuid, "shared");
|
|
183
|
+
for (const machine of safeReaddir(opts.shareDir)) {
|
|
184
|
+
if (machine === opts.machine)
|
|
185
|
+
continue;
|
|
186
|
+
const mdir = join(opts.shareDir, machine);
|
|
187
|
+
const manifest = readJson(join(mdir, "manifest.json"));
|
|
188
|
+
if (!manifest)
|
|
189
|
+
continue;
|
|
190
|
+
machines.push(machine);
|
|
191
|
+
const remoteHome = typeof manifest.home === "string" ? manifest.home : null;
|
|
192
|
+
const ids = safeReaddir(join(mdir, "sessions")).filter((id) => !id.startsWith("."));
|
|
193
|
+
const files = ids.flatMap((id) => [
|
|
194
|
+
join(mdir, "sessions", id, "entry.json"),
|
|
195
|
+
join(mdir, "sessions", id, "transcript.jsonl"),
|
|
196
|
+
]);
|
|
197
|
+
const placeholders = (opts.findPlaceholders ?? findPlaceholders)(files);
|
|
198
|
+
const toDownload = [];
|
|
199
|
+
for (const id of ids) {
|
|
200
|
+
const sdir = join(mdir, "sessions", id);
|
|
201
|
+
const entryFrom = join(sdir, "entry.json");
|
|
202
|
+
if (placeholders.has(entryFrom) || placeholders.has(join(sdir, "transcript.jsonl"))) {
|
|
203
|
+
toDownload.push(sdir);
|
|
204
|
+
skips.push({ machine, cliSessionId: id, title: null, reason: "not-downloaded" });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const remote = readJson(entryFrom);
|
|
208
|
+
const remoteTranscript = join(sdir, "transcript.jsonl");
|
|
209
|
+
const title = typeof remote?.title === "string" ? remote.title : null;
|
|
210
|
+
if (!remote || typeof remote.cwd !== "string" || !safeStat(remoteTranscript)) {
|
|
211
|
+
skips.push({ machine, cliSessionId: id, title, reason: "unreadable" });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const remoteLast = typeof remote.lastActivityAt === "number" ? remote.lastActivityAt : null;
|
|
215
|
+
if (remote.isArchived === true || (remoteLast ?? 0) < cutoff)
|
|
216
|
+
continue;
|
|
217
|
+
const cwd = remapHome(remote.cwd, remoteHome, home);
|
|
218
|
+
if (!existsSync(cwd)) {
|
|
219
|
+
skips.push({ machine, cliSessionId: id, title, reason: "missing-cwd" });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const existing = local.get(id);
|
|
223
|
+
const transcriptTo = transcriptPath(home, cwd, id);
|
|
224
|
+
const localSt = safeStat(transcriptTo);
|
|
225
|
+
const remoteSt = safeStat(remoteTranscript);
|
|
226
|
+
let copyTranscript = false;
|
|
227
|
+
if (!localSt) {
|
|
228
|
+
copyTranscript = true;
|
|
229
|
+
}
|
|
230
|
+
else if (remoteSt && remoteSt.size > localSt.size) {
|
|
231
|
+
if (now - localSt.mtimeMs < liveWindow) {
|
|
232
|
+
skips.push({ machine, cliSessionId: id, title, reason: "live-here" });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!isBytePrefix(transcriptTo, remoteTranscript)) {
|
|
236
|
+
skips.push({ machine, cliSessionId: id, title, reason: "diverged" });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
copyTranscript = true;
|
|
240
|
+
}
|
|
241
|
+
else if (remoteSt && remoteSt.size < localSt.size && existing) {
|
|
242
|
+
// Continued here since the other machine last saw it; keep ours.
|
|
243
|
+
skips.push({ machine, cliSessionId: id, title, reason: "local-newer" });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const entryStale = !existing ||
|
|
247
|
+
(copyTranscript && (remoteLast ?? 0) > (existing.lastActivityAt ?? 0)) ||
|
|
248
|
+
(copyTranscript && title !== existing.title);
|
|
249
|
+
if (!copyTranscript && !entryStale) {
|
|
250
|
+
skips.push({ machine, cliSessionId: id, title, reason: "up-to-date" });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const sidecar = join(sdir, "sidecar");
|
|
254
|
+
imports.push({
|
|
255
|
+
kind: !existing ? (copyTranscript ? "new" : "entry-only") : "updated",
|
|
256
|
+
machine,
|
|
257
|
+
cliSessionId: id,
|
|
258
|
+
title,
|
|
259
|
+
cwd,
|
|
260
|
+
transcriptFrom: copyTranscript ? remoteTranscript : null,
|
|
261
|
+
transcriptTo,
|
|
262
|
+
sidecarFrom: copyTranscript && existsSync(sidecar) ? sidecar : null,
|
|
263
|
+
entryFrom,
|
|
264
|
+
entryTo: existing
|
|
265
|
+
? existing.file
|
|
266
|
+
: join(envDir, `${typeof remote.sessionId === "string" ? remote.sessionId : `local_${id}`}.json`),
|
|
267
|
+
entryExists: Boolean(existing),
|
|
268
|
+
remoteLastActivityAt: remoteLast,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (toDownload.length > 0)
|
|
272
|
+
(opts.requestDownload ?? requestDownload)(toDownload);
|
|
273
|
+
}
|
|
274
|
+
return { exports, imports, skips, machines };
|
|
275
|
+
}
|
|
276
|
+
/** Write via a temp file + rename so a syncing reader never sees half a file. */
|
|
277
|
+
function atomicCopy(from, to) {
|
|
278
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
279
|
+
const tmp = `${to}.harnery-tmp`;
|
|
280
|
+
try {
|
|
281
|
+
copyFileSync(from, tmp);
|
|
282
|
+
renameSync(tmp, to);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
rmSync(tmp, { force: true });
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function atomicWrite(to, content) {
|
|
290
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
291
|
+
const tmp = `${to}.harnery-tmp`;
|
|
292
|
+
writeFileSync(tmp, content);
|
|
293
|
+
renameSync(tmp, to);
|
|
294
|
+
}
|
|
295
|
+
export function applyShare(plan, opts) {
|
|
296
|
+
const home = opts.home ?? homedir();
|
|
297
|
+
const mdir = join(opts.shareDir, opts.machine);
|
|
298
|
+
const accounts = listAccounts(opts.dataDir);
|
|
299
|
+
const local = localEntriesById(accounts);
|
|
300
|
+
for (const x of plan.exports) {
|
|
301
|
+
const e = local.get(x.cliSessionId);
|
|
302
|
+
if (!e)
|
|
303
|
+
continue;
|
|
304
|
+
const sdir = join(mdir, "sessions", x.cliSessionId);
|
|
305
|
+
atomicCopy(x.transcript, join(sdir, "transcript.jsonl"));
|
|
306
|
+
const sidecar = join(dirname(x.transcript), x.cliSessionId);
|
|
307
|
+
if (existsSync(sidecar))
|
|
308
|
+
cpSync(sidecar, join(sdir, "sidecar"), { recursive: true, force: true });
|
|
309
|
+
atomicCopy(e.file, join(sdir, "entry.json"));
|
|
310
|
+
}
|
|
311
|
+
atomicWrite(join(mdir, "manifest.json"), `${JSON.stringify({ machine: opts.machine, home, exported_at: new Date(opts.now ?? Date.now()).toISOString() }, null, 2)}\n`);
|
|
312
|
+
let imported = 0;
|
|
313
|
+
const failed = [];
|
|
314
|
+
for (const x of plan.imports) {
|
|
315
|
+
try {
|
|
316
|
+
if (importOne(x, opts, home))
|
|
317
|
+
imported++;
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
failed.push({
|
|
321
|
+
machine: x.machine,
|
|
322
|
+
cliSessionId: x.cliSessionId,
|
|
323
|
+
title: x.title,
|
|
324
|
+
error: err instanceof Error ? err.message : String(err),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { exported: plan.exports.length, imported, failed };
|
|
329
|
+
}
|
|
330
|
+
function importOne(x, opts, home) {
|
|
331
|
+
const remote = readJson(x.entryFrom);
|
|
332
|
+
if (!remote)
|
|
333
|
+
return false;
|
|
334
|
+
const remoteManifest = readJson(join(opts.shareDir, x.machine, "manifest.json"));
|
|
335
|
+
const remoteHome = typeof remoteManifest?.home === "string" ? remoteManifest.home : null;
|
|
336
|
+
if (x.transcriptFrom)
|
|
337
|
+
atomicCopy(x.transcriptFrom, x.transcriptTo);
|
|
338
|
+
if (x.sidecarFrom) {
|
|
339
|
+
cpSync(x.sidecarFrom, join(dirname(x.transcriptTo), x.cliSessionId), {
|
|
340
|
+
recursive: true,
|
|
341
|
+
force: true,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
let entry;
|
|
345
|
+
if (x.entryExists) {
|
|
346
|
+
entry = readJson(x.entryTo) ?? {};
|
|
347
|
+
if (typeof remote.title === "string")
|
|
348
|
+
entry.title = remote.title;
|
|
349
|
+
if (typeof remote.lastActivityAt === "number")
|
|
350
|
+
entry.lastActivityAt = remote.lastActivityAt;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
entry = { ...remote, isArchived: false };
|
|
354
|
+
for (const k of ["cwd", "originCwd"]) {
|
|
355
|
+
if (typeof entry[k] === "string")
|
|
356
|
+
entry[k] = remapHome(entry[k], remoteHome, home);
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(entry.gitAnchors)) {
|
|
359
|
+
entry.gitAnchors = entry.gitAnchors.map((g) => ({
|
|
360
|
+
...g,
|
|
361
|
+
gitRoot: typeof g.gitRoot === "string" ? remapHome(g.gitRoot, remoteHome, home) : g.gitRoot,
|
|
362
|
+
commonDir: typeof g.commonDir === "string" ? remapHome(g.commonDir, remoteHome, home) : g.commonDir,
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
atomicWrite(x.entryTo, `${JSON.stringify(entry)}\n`);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
export function shareSummary(plan) {
|
|
370
|
+
return {
|
|
371
|
+
machines: plan.machines,
|
|
372
|
+
export: plan.exports.map((x) => ({
|
|
373
|
+
title: x.title,
|
|
374
|
+
cli_session_id: x.cliSessionId,
|
|
375
|
+
bytes: x.bytes,
|
|
376
|
+
})),
|
|
377
|
+
import: plan.imports.map((x) => ({
|
|
378
|
+
kind: x.kind,
|
|
379
|
+
from_machine: x.machine,
|
|
380
|
+
title: x.title,
|
|
381
|
+
cli_session_id: x.cliSessionId,
|
|
382
|
+
})),
|
|
383
|
+
skipped: plan.skips
|
|
384
|
+
.filter((s) => s.reason !== "up-to-date" && s.reason !== "not-downloaded")
|
|
385
|
+
.map((s) => ({
|
|
386
|
+
from_machine: s.machine,
|
|
387
|
+
title: s.title,
|
|
388
|
+
cli_session_id: s.cliSessionId,
|
|
389
|
+
reason: s.reason,
|
|
390
|
+
})),
|
|
391
|
+
not_downloaded: plan.skips.filter((s) => s.reason === "not-downloaded").length,
|
|
392
|
+
up_to_date: plan.skips.filter((s) => s.reason === "up-to-date").length,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const DISK_GIT_STATES: readonly ["tracked", "untracked", "ignored", "git-metadata", "non-repository", "unknown"];
|
|
2
|
+
export type DiskGitState = (typeof DISK_GIT_STATES)[number];
|
|
3
|
+
export type DiskGroup = "directory" | "repository" | "extension" | "git" | "files";
|
|
4
|
+
export interface DiskOptions {
|
|
5
|
+
git?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
exclude?: string[];
|
|
8
|
+
group?: DiskGroup;
|
|
9
|
+
depth?: number;
|
|
10
|
+
top?: number;
|
|
11
|
+
minSize?: number;
|
|
12
|
+
apparent?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface DiskTotals {
|
|
15
|
+
files: number;
|
|
16
|
+
apparent_bytes: number;
|
|
17
|
+
allocated_bytes: number | null;
|
|
18
|
+
}
|
|
19
|
+
export interface DiskRow extends DiskTotals {
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DiskReport {
|
|
23
|
+
schema: "harnery.disk-usage/v1";
|
|
24
|
+
root: string;
|
|
25
|
+
measurement: "allocated" | "apparent" | "apparent-fallback";
|
|
26
|
+
complete: boolean;
|
|
27
|
+
filters: DiskOptions;
|
|
28
|
+
totals: DiskTotals;
|
|
29
|
+
git: Record<DiskGitState, DiskTotals>;
|
|
30
|
+
groups: DiskRow[];
|
|
31
|
+
group_count: number;
|
|
32
|
+
largest_files: (DiskRow & {
|
|
33
|
+
git: DiskGitState;
|
|
34
|
+
repository: string | null;
|
|
35
|
+
})[];
|
|
36
|
+
repositories: string[];
|
|
37
|
+
skipped: {
|
|
38
|
+
symlinks: number;
|
|
39
|
+
special_files: number;
|
|
40
|
+
excluded: number;
|
|
41
|
+
};
|
|
42
|
+
issues: {
|
|
43
|
+
path: string;
|
|
44
|
+
message: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export declare function parseDiskSize(value: string): number;
|
|
48
|
+
/** Inspect regular files only. No bodies are read and no symlinks are followed. */
|
|
49
|
+
export declare function diskUsage(input?: string, options?: DiskOptions): DiskReport;
|
|
50
|
+
//# sourceMappingURL=disk-usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disk-usage.d.ts","sourceRoot":"","sources":["../../src/lib/disk-usage.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,2FAOlB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5D,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,KAAK,GAAG,OAAO,CAAC;AAEnF,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AACD,MAAM,WAAW,OAAQ,SAAQ,UAAU;IACzC,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,uBAAuB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,WAAW,GAAG,UAAU,GAAG,mBAAmB,CAAC;IAC5D,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IACtC,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,OAAO,GAAG;QAAE,GAAG,EAAE,YAAY,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,EAAE,CAAC;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC7C;AAoCD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBnD;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,KAAK,SAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,UAAU,CAiP5E"}
|