@phnx-labs/agents-cli 1.15.0 → 1.17.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/CHANGELOG.md +143 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +793 -83
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +70 -1
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +224 -17
- package/dist/commands/prune.js +29 -1
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +154 -20
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +78 -20
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +48 -36
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +32 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +41 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +22 -6
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +158 -23
- package/dist/lib/browser/profiles.d.ts +10 -2
- package/dist/lib/browser/profiles.js +122 -37
- package/dist/lib/browser/service.d.ts +91 -13
- package/dist/lib/browser/service.js +767 -132
- package/dist/lib/browser/types.d.ts +91 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +69 -14
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -15
- package/dist/lib/commands.js +11 -7
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +138 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1237 -22
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -66
- package/dist/lib/permissions.js +18 -18
- package/dist/lib/plugins.d.ts +94 -24
- package/dist/lib/plugins.js +702 -123
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +109 -5
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +55 -29
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +4 -52
- package/dist/lib/shims.js +23 -15
- package/dist/lib/skills.js +6 -2
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +101 -16
- package/dist/lib/state.js +179 -31
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +75 -17
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +158 -47
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +60 -59
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
package/dist/lib/migrate.js
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as os from 'os';
|
|
10
|
-
|
|
10
|
+
import * as yaml from 'yaml';
|
|
11
|
+
const HOME = process.env.HOME ?? os.homedir();
|
|
11
12
|
const SYSTEM_DIR = path.join(HOME, '.agents-system');
|
|
12
13
|
const USER_DIR = path.join(HOME, '.agents');
|
|
14
|
+
const HISTORY_DIR = path.join(USER_DIR, '.history');
|
|
15
|
+
const CACHE_DIR = path.join(USER_DIR, '.cache');
|
|
13
16
|
/**
|
|
14
17
|
* Move ~/.agents-system/agents.yaml -> ~/.agents/agents.yaml.
|
|
15
18
|
* No-op if user file already exists or system file absent.
|
|
@@ -19,12 +22,22 @@ const USER_DIR = path.join(HOME, '.agents');
|
|
|
19
22
|
function migrateAgentsYaml() {
|
|
20
23
|
const src = path.join(SYSTEM_DIR, 'agents.yaml');
|
|
21
24
|
const dest = path.join(USER_DIR, 'agents.yaml');
|
|
22
|
-
if (
|
|
25
|
+
if (!fs.existsSync(src))
|
|
26
|
+
return;
|
|
27
|
+
if (fs.existsSync(dest)) {
|
|
28
|
+
// User copy is authoritative — drop the stale system leftover. The system
|
|
29
|
+
// repo (npm-shipped) does not track agents.yaml; any copy here is residue
|
|
30
|
+
// from the pre-split layout.
|
|
31
|
+
try {
|
|
32
|
+
fs.unlinkSync(src);
|
|
33
|
+
}
|
|
34
|
+
catch { /* best-effort */ }
|
|
23
35
|
return;
|
|
36
|
+
}
|
|
24
37
|
try {
|
|
25
38
|
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
26
39
|
fs.renameSync(src, dest);
|
|
27
|
-
console.
|
|
40
|
+
console.error('Migrated agents.yaml to ~/.agents/');
|
|
28
41
|
}
|
|
29
42
|
catch { /* best-effort */ }
|
|
30
43
|
}
|
|
@@ -77,28 +90,32 @@ function migratePromptcutsIntoHooks() {
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
/**
|
|
80
|
-
* Move installed agent versions from the legacy
|
|
81
|
-
* (~/.agents/versions/<agent>/<ver>/) into the
|
|
82
|
-
* (~/.agents
|
|
93
|
+
* Move installed agent versions from the legacy system-root layout
|
|
94
|
+
* (~/.agents-system/versions/<agent>/<ver>/) into the user root
|
|
95
|
+
* (~/.agents/versions/<agent>/<ver>/).
|
|
83
96
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
97
|
+
* Earlier installs (and an inverted prior version of this migrator) put
|
|
98
|
+
* binaries and home dirs under ~/.agents-system/. The current architecture
|
|
99
|
+
* keeps all operational state (versions, sessions, shims, trash) under
|
|
100
|
+
* ~/.agents/, and getVersionsDir() in state.ts resolves there. Without this
|
|
101
|
+
* migration the legacy versions become invisible to listInstalledVersions
|
|
102
|
+
* and every command that depends on it (view, prune, run, sync) writes a
|
|
103
|
+
* second copy to ~/.agents/versions/ while the agent CLIs keep reading the
|
|
104
|
+
* stale ~/.agents-system/versions/ copy via the existing ~/.<agent> symlink.
|
|
88
105
|
*
|
|
89
106
|
* Idempotent and non-destructive: if a same-named dest already exists we
|
|
90
107
|
* leave the legacy copy in place so the user can reconcile manually.
|
|
91
108
|
*/
|
|
92
|
-
function
|
|
93
|
-
const userVersions = path.join(USER_DIR, 'versions');
|
|
109
|
+
function migrateSystemVersionsToUser() {
|
|
94
110
|
const sysVersions = path.join(SYSTEM_DIR, 'versions');
|
|
95
|
-
|
|
111
|
+
const userVersions = path.join(USER_DIR, 'versions');
|
|
112
|
+
if (!fs.existsSync(sysVersions))
|
|
96
113
|
return;
|
|
97
114
|
let movedCount = 0;
|
|
98
115
|
let skippedCount = 0;
|
|
99
116
|
let agentEntries;
|
|
100
117
|
try {
|
|
101
|
-
agentEntries = fs.readdirSync(
|
|
118
|
+
agentEntries = fs.readdirSync(sysVersions, { withFileTypes: true });
|
|
102
119
|
}
|
|
103
120
|
catch {
|
|
104
121
|
return;
|
|
@@ -106,8 +123,8 @@ function migrateUserVersionsToSystem() {
|
|
|
106
123
|
for (const agent of agentEntries) {
|
|
107
124
|
if (!agent.isDirectory())
|
|
108
125
|
continue;
|
|
109
|
-
const srcAgentDir = path.join(
|
|
110
|
-
const dstAgentDir = path.join(
|
|
126
|
+
const srcAgentDir = path.join(sysVersions, agent.name);
|
|
127
|
+
const dstAgentDir = path.join(userVersions, agent.name);
|
|
111
128
|
try {
|
|
112
129
|
fs.mkdirSync(dstAgentDir, { recursive: true, mode: 0o700 });
|
|
113
130
|
}
|
|
@@ -141,15 +158,15 @@ function migrateUserVersionsToSystem() {
|
|
|
141
158
|
catch { /* best-effort */ }
|
|
142
159
|
}
|
|
143
160
|
try {
|
|
144
|
-
if (fs.readdirSync(
|
|
145
|
-
fs.rmdirSync(
|
|
161
|
+
if (fs.readdirSync(sysVersions).length === 0)
|
|
162
|
+
fs.rmdirSync(sysVersions);
|
|
146
163
|
}
|
|
147
164
|
catch { /* best-effort */ }
|
|
148
165
|
if (movedCount > 0) {
|
|
149
|
-
console.
|
|
166
|
+
console.error(`Migrated ${movedCount} version dir${movedCount === 1 ? '' : 's'} from ~/.agents-system/versions/ to ~/.agents/versions/`);
|
|
150
167
|
}
|
|
151
168
|
if (skippedCount > 0) {
|
|
152
|
-
console.
|
|
169
|
+
console.error(`Skipped ${skippedCount} version dir${skippedCount === 1 ? '' : 's'} already present in ~/.agents/versions/ (kept legacy copy at ~/.agents-system/versions/)`);
|
|
153
170
|
}
|
|
154
171
|
}
|
|
155
172
|
/**
|
|
@@ -195,14 +212,1212 @@ function migrateBackupsToHidden() {
|
|
|
195
212
|
}
|
|
196
213
|
catch { /* best-effort */ }
|
|
197
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Fold ~/.agents/hooks.yaml into ~/.agents/agents.yaml under a `hooks:` key,
|
|
217
|
+
* then delete the standalone hooks.yaml. Single user file to sync.
|
|
218
|
+
*
|
|
219
|
+
* On collision (a hook name already exists in agents.yaml hooks:), the
|
|
220
|
+
* existing agents.yaml entry wins and the standalone copy is dropped — this
|
|
221
|
+
* matches the behavior a user would get if they had already migrated
|
|
222
|
+
* manually and edited agents.yaml.
|
|
223
|
+
*
|
|
224
|
+
* Idempotent: skips if hooks.yaml is absent or unparseable.
|
|
225
|
+
*/
|
|
226
|
+
function foldUserHooksYamlIntoAgentsYaml() {
|
|
227
|
+
const hooksFile = path.join(USER_DIR, 'hooks.yaml');
|
|
228
|
+
if (!fs.existsSync(hooksFile))
|
|
229
|
+
return;
|
|
230
|
+
let hooks;
|
|
231
|
+
try {
|
|
232
|
+
const raw = fs.readFileSync(hooksFile, 'utf-8');
|
|
233
|
+
const parsed = yaml.parse(raw);
|
|
234
|
+
hooks = parsed && typeof parsed === 'object' ? parsed : {};
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const metaFile = path.join(USER_DIR, 'agents.yaml');
|
|
240
|
+
let meta = {};
|
|
241
|
+
if (fs.existsSync(metaFile)) {
|
|
242
|
+
try {
|
|
243
|
+
const raw = fs.readFileSync(metaFile, 'utf-8');
|
|
244
|
+
const parsed = yaml.parse(raw);
|
|
245
|
+
if (parsed && typeof parsed === 'object')
|
|
246
|
+
meta = parsed;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const existingHooks = meta.hooks ?? {};
|
|
253
|
+
const merged = { ...hooks, ...existingHooks };
|
|
254
|
+
meta.hooks = merged;
|
|
255
|
+
const header = `# agents-cli metadata
|
|
256
|
+
# Auto-generated - do not edit manually
|
|
257
|
+
# https://github.com/phnx-labs/agents-cli
|
|
258
|
+
|
|
259
|
+
`;
|
|
260
|
+
try {
|
|
261
|
+
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
262
|
+
fs.writeFileSync(metaFile, header + yaml.stringify(meta), 'utf-8');
|
|
263
|
+
fs.unlinkSync(hooksFile);
|
|
264
|
+
console.error('Folded ~/.agents/hooks.yaml into ~/.agents/agents.yaml (hooks: section)');
|
|
265
|
+
}
|
|
266
|
+
catch { /* best-effort */ }
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Fold ~/.agents/browser/profiles/*.yaml into ~/.agents/agents.yaml under a
|
|
270
|
+
* `browser:` key, then delete the profiles directory. Single user file to sync.
|
|
271
|
+
*
|
|
272
|
+
* On collision (a profile name already exists in agents.yaml browser:), the
|
|
273
|
+
* existing agents.yaml entry wins and the standalone copy is dropped.
|
|
274
|
+
*
|
|
275
|
+
* Idempotent: skips if profiles dir is absent or empty.
|
|
276
|
+
*/
|
|
277
|
+
function foldBrowserProfilesIntoAgentsYaml() {
|
|
278
|
+
const profilesDir = path.join(USER_DIR, 'browser', 'profiles');
|
|
279
|
+
if (!fs.existsSync(profilesDir))
|
|
280
|
+
return;
|
|
281
|
+
let files;
|
|
282
|
+
try {
|
|
283
|
+
files = fs.readdirSync(profilesDir).filter((f) => f.endsWith('.yaml'));
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (files.length === 0)
|
|
289
|
+
return;
|
|
290
|
+
const profiles = {};
|
|
291
|
+
for (const file of files) {
|
|
292
|
+
try {
|
|
293
|
+
const raw = fs.readFileSync(path.join(profilesDir, file), 'utf-8');
|
|
294
|
+
const parsed = yaml.parse(raw);
|
|
295
|
+
if (!parsed || typeof parsed !== 'object')
|
|
296
|
+
continue;
|
|
297
|
+
const name = parsed.name || file.replace(/\.yaml$/, '');
|
|
298
|
+
const { name: _, ...config } = parsed;
|
|
299
|
+
profiles[name] = config;
|
|
300
|
+
}
|
|
301
|
+
catch { /* skip unreadable file */ }
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(profiles).length === 0)
|
|
304
|
+
return;
|
|
305
|
+
const metaFile = path.join(USER_DIR, 'agents.yaml');
|
|
306
|
+
let meta = {};
|
|
307
|
+
if (fs.existsSync(metaFile)) {
|
|
308
|
+
try {
|
|
309
|
+
const raw = fs.readFileSync(metaFile, 'utf-8');
|
|
310
|
+
const parsed = yaml.parse(raw);
|
|
311
|
+
if (parsed && typeof parsed === 'object')
|
|
312
|
+
meta = parsed;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const existingBrowser = meta.browser ?? {};
|
|
319
|
+
const merged = { ...profiles, ...existingBrowser };
|
|
320
|
+
meta.browser = merged;
|
|
321
|
+
const header = `# agents-cli metadata
|
|
322
|
+
# Auto-generated - do not edit manually
|
|
323
|
+
# https://github.com/phnx-labs/agents-cli
|
|
324
|
+
|
|
325
|
+
`;
|
|
326
|
+
try {
|
|
327
|
+
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
328
|
+
fs.writeFileSync(metaFile, header + yaml.stringify(meta), 'utf-8');
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
try {
|
|
331
|
+
fs.unlinkSync(path.join(profilesDir, file));
|
|
332
|
+
}
|
|
333
|
+
catch { /* best-effort */ }
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
fs.rmdirSync(profilesDir);
|
|
337
|
+
}
|
|
338
|
+
catch { /* may not be empty */ }
|
|
339
|
+
try {
|
|
340
|
+
const browserDir = path.join(USER_DIR, 'browser');
|
|
341
|
+
if (fs.existsSync(browserDir) && fs.readdirSync(browserDir).length === 0) {
|
|
342
|
+
fs.rmdirSync(browserDir);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch { /* best-effort */ }
|
|
346
|
+
console.error('Folded ~/.agents/browser/profiles/ into ~/.agents/agents.yaml (browser: section)');
|
|
347
|
+
}
|
|
348
|
+
catch { /* best-effort */ }
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Delete ~/.agents/linear.json. The linear-cli now manages its own
|
|
352
|
+
* credentials in the OS keychain; this file was a legacy plaintext store.
|
|
353
|
+
*/
|
|
354
|
+
function deleteUserLinearJson() {
|
|
355
|
+
const f = path.join(USER_DIR, 'linear.json');
|
|
356
|
+
if (!fs.existsSync(f))
|
|
357
|
+
return;
|
|
358
|
+
try {
|
|
359
|
+
fs.unlinkSync(f);
|
|
360
|
+
}
|
|
361
|
+
catch { /* best-effort */ }
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Delete ~/.agents/prompts.json. Dead file with zero refs in src/ (the
|
|
365
|
+
* system-repo copy was cleared by deleteSystemPromptsJson; this is the
|
|
366
|
+
* matching cleanup at the user layer).
|
|
367
|
+
*/
|
|
368
|
+
function deleteUserPromptsJson() {
|
|
369
|
+
const f = path.join(USER_DIR, 'prompts.json');
|
|
370
|
+
if (!fs.existsSync(f))
|
|
371
|
+
return;
|
|
372
|
+
try {
|
|
373
|
+
fs.unlinkSync(f);
|
|
374
|
+
}
|
|
375
|
+
catch { /* best-effort */ }
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Delete ~/.agents/config.json. The canonical teams config is at
|
|
379
|
+
* ~/.agents/teams/config.json (teams/persistence.ts). If the canonical
|
|
380
|
+
* file exists we just unlink the legacy copy; otherwise migrate first.
|
|
381
|
+
*/
|
|
382
|
+
function cleanupUserConfigJson() {
|
|
383
|
+
const legacy = path.join(USER_DIR, 'config.json');
|
|
384
|
+
if (!fs.existsSync(legacy))
|
|
385
|
+
return;
|
|
386
|
+
const canonical = path.join(USER_DIR, 'teams', 'config.json');
|
|
387
|
+
try {
|
|
388
|
+
if (!fs.existsSync(canonical)) {
|
|
389
|
+
fs.mkdirSync(path.dirname(canonical), { recursive: true, mode: 0o700 });
|
|
390
|
+
fs.copyFileSync(legacy, canonical);
|
|
391
|
+
}
|
|
392
|
+
fs.unlinkSync(legacy);
|
|
393
|
+
}
|
|
394
|
+
catch { /* best-effort */ }
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Remove an empty ~/.agents/runs/ directory left over after the
|
|
398
|
+
* migrateRunsIntoRoutines() rename. Some older code paths re-created the
|
|
399
|
+
* empty parent; this trims it once it has no contents.
|
|
400
|
+
*/
|
|
401
|
+
function cleanupEmptyTopLevelRuns() {
|
|
402
|
+
const dir = path.join(USER_DIR, 'runs');
|
|
403
|
+
if (!fs.existsSync(dir))
|
|
404
|
+
return;
|
|
405
|
+
try {
|
|
406
|
+
if (fs.readdirSync(dir).length === 0)
|
|
407
|
+
fs.rmdirSync(dir);
|
|
408
|
+
}
|
|
409
|
+
catch { /* best-effort */ }
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Move ~/.agents-system/aliases.json -> ~/.agents/aliases.json.
|
|
413
|
+
* Aliases are per-user state and were previously written to the system root
|
|
414
|
+
* by mistake. Idempotent: skips if dest already exists or src absent.
|
|
415
|
+
*/
|
|
416
|
+
function migrateAliasesToUser() {
|
|
417
|
+
const src = path.join(SYSTEM_DIR, 'aliases.json');
|
|
418
|
+
const dest = path.join(USER_DIR, 'aliases.json');
|
|
419
|
+
if (fs.existsSync(dest) || !fs.existsSync(src))
|
|
420
|
+
return;
|
|
421
|
+
try {
|
|
422
|
+
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
423
|
+
fs.renameSync(src, dest);
|
|
424
|
+
}
|
|
425
|
+
catch { /* best-effort */ }
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* For overlapping versions that exist in BOTH ~/.agents-system/versions/ and
|
|
429
|
+
* ~/.agents/versions/, merge legacy operational state (history, sessions,
|
|
430
|
+
* settings) into the user copy without overwriting any files synced by
|
|
431
|
+
* agents-cli (skills, commands, hooks, memory). Then drop the legacy copy.
|
|
432
|
+
*
|
|
433
|
+
* Why: when both paths exist, the inverted-prior migrator left them split.
|
|
434
|
+
* Agent CLIs read from ~/.<agent> (a symlink that historically targeted
|
|
435
|
+
* the system path), while sync writes to the user path. Same skill ends up
|
|
436
|
+
* in both → duplicate entries in the skills picker, stale state, broken
|
|
437
|
+
* resource resolution.
|
|
438
|
+
*
|
|
439
|
+
* Strategy: copy legacy → user with "skip if exists". Sync-managed dirs
|
|
440
|
+
* (which live in user already, freshly written) stay untouched. Anything
|
|
441
|
+
* the agent CLI created on its own (history.jsonl, sessions/, settings.json,
|
|
442
|
+
* file-history/, paste-cache/, …) lands in user where it belongs. The
|
|
443
|
+
* legacy version-home is then renamed into ~/.agents/.trash/versions/ so
|
|
444
|
+
* it's recoverable.
|
|
445
|
+
*/
|
|
446
|
+
function mergeOverlappingVersionHomes() {
|
|
447
|
+
const sysVersions = path.join(SYSTEM_DIR, 'versions');
|
|
448
|
+
const userVersions = path.join(USER_DIR, 'versions');
|
|
449
|
+
if (!fs.existsSync(sysVersions))
|
|
450
|
+
return;
|
|
451
|
+
let mergedCount = 0;
|
|
452
|
+
let agentEntries;
|
|
453
|
+
try {
|
|
454
|
+
agentEntries = fs.readdirSync(sysVersions, { withFileTypes: true });
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
for (const agent of agentEntries) {
|
|
460
|
+
if (!agent.isDirectory())
|
|
461
|
+
continue;
|
|
462
|
+
const sysAgentDir = path.join(sysVersions, agent.name);
|
|
463
|
+
const userAgentDir = path.join(userVersions, agent.name);
|
|
464
|
+
let verEntries;
|
|
465
|
+
try {
|
|
466
|
+
verEntries = fs.readdirSync(sysAgentDir, { withFileTypes: true });
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
for (const ver of verEntries) {
|
|
472
|
+
if (!ver.isDirectory())
|
|
473
|
+
continue;
|
|
474
|
+
const sysHome = path.join(sysAgentDir, ver.name, 'home');
|
|
475
|
+
const userHome = path.join(userAgentDir, ver.name, 'home');
|
|
476
|
+
if (!fs.existsSync(sysHome) || !fs.existsSync(userHome))
|
|
477
|
+
continue;
|
|
478
|
+
try {
|
|
479
|
+
copyDirSkipExisting(sysHome, userHome);
|
|
480
|
+
const trashRoot = path.join(USER_DIR, '.trash', 'versions', agent.name, ver.name);
|
|
481
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
482
|
+
fs.mkdirSync(trashRoot, { recursive: true, mode: 0o700 });
|
|
483
|
+
fs.renameSync(path.join(sysAgentDir, ver.name), path.join(trashRoot, `legacy-${stamp}`));
|
|
484
|
+
mergedCount++;
|
|
485
|
+
}
|
|
486
|
+
catch { /* best-effort */ }
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
if (fs.readdirSync(sysAgentDir).length === 0)
|
|
490
|
+
fs.rmdirSync(sysAgentDir);
|
|
491
|
+
}
|
|
492
|
+
catch { /* best-effort */ }
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
if (fs.readdirSync(sysVersions).length === 0)
|
|
496
|
+
fs.rmdirSync(sysVersions);
|
|
497
|
+
}
|
|
498
|
+
catch { /* best-effort */ }
|
|
499
|
+
if (mergedCount > 0) {
|
|
500
|
+
console.error(`Merged ${mergedCount} overlapping version home${mergedCount === 1 ? '' : 's'} from legacy ~/.agents-system/versions/ into ~/.agents/versions/ (legacy moved to ~/.agents/.trash/versions/)`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function copyDirSkipExisting(src, dest) {
|
|
504
|
+
let entries;
|
|
505
|
+
try {
|
|
506
|
+
entries = fs.readdirSync(src, { withFileTypes: true });
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
512
|
+
for (const entry of entries) {
|
|
513
|
+
const s = path.join(src, entry.name);
|
|
514
|
+
const d = path.join(dest, entry.name);
|
|
515
|
+
if (fs.existsSync(d)) {
|
|
516
|
+
if (entry.isDirectory()) {
|
|
517
|
+
const dStat = fs.lstatSync(d);
|
|
518
|
+
if (dStat.isDirectory())
|
|
519
|
+
copyDirSkipExisting(s, d);
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
fs.renameSync(s, d);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
try {
|
|
528
|
+
if (entry.isDirectory()) {
|
|
529
|
+
copyDirSkipExisting(s, d);
|
|
530
|
+
}
|
|
531
|
+
else if (entry.isSymbolicLink()) {
|
|
532
|
+
fs.symlinkSync(fs.readlinkSync(s), d);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
fs.copyFileSync(s, d);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch { /* best-effort */ }
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Rename ~/.agents/permissions/sets/ -> ~/.agents/permissions/presets/.
|
|
544
|
+
* Also handles ~/.agents-system/permissions/sets/ for system repo.
|
|
545
|
+
* Idempotent: skips if dest already exists or src absent.
|
|
546
|
+
*/
|
|
547
|
+
function migratePermissionSetsToPresets() {
|
|
548
|
+
for (const root of [USER_DIR, SYSTEM_DIR]) {
|
|
549
|
+
const src = path.join(root, 'permissions', 'sets');
|
|
550
|
+
const dest = path.join(root, 'permissions', 'presets');
|
|
551
|
+
if (!fs.existsSync(src) || fs.existsSync(dest))
|
|
552
|
+
continue;
|
|
553
|
+
try {
|
|
554
|
+
fs.renameSync(src, dest);
|
|
555
|
+
const label = root === USER_DIR ? '~/.agents' : '~/.agents-system';
|
|
556
|
+
console.error(`Migrated ${label}/permissions/sets/ to ${label}/permissions/presets/`);
|
|
557
|
+
}
|
|
558
|
+
catch { /* best-effort */ }
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* After versions are migrated to ~/.agents/versions/, rewrite the per-agent
|
|
563
|
+
* config symlinks (~/.claude, ~/.codex, …) to point at the user-side
|
|
564
|
+
* version-home so the agent CLIs read fresh resources.
|
|
565
|
+
*
|
|
566
|
+
* Idempotent: if the symlink already points at the right user-path target,
|
|
567
|
+
* leave it. If it points at the legacy system path, re-create it. If a real
|
|
568
|
+
* directory exists there (no symlink yet), leave it alone — version-config
|
|
569
|
+
* switching is owned by `agents use`, not the migrator.
|
|
570
|
+
*/
|
|
571
|
+
function repairAgentConfigSymlinks() {
|
|
572
|
+
let yaml;
|
|
573
|
+
try {
|
|
574
|
+
yaml = fs.readFileSync(path.join(USER_DIR, 'agents.yaml'), 'utf-8');
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const agentsBlock = yaml.match(/^agents:\s*\n((?: [^\n]*\n)+)/m);
|
|
580
|
+
if (!agentsBlock)
|
|
581
|
+
return;
|
|
582
|
+
const defaults = [];
|
|
583
|
+
for (const line of agentsBlock[1].split('\n')) {
|
|
584
|
+
const m = line.match(/^\s+([a-z][a-z0-9_-]*):\s*([^\s#]+)/);
|
|
585
|
+
if (m)
|
|
586
|
+
defaults.push({ agent: m[1], version: m[2] });
|
|
587
|
+
}
|
|
588
|
+
let repaired = 0;
|
|
589
|
+
for (const { agent, version } of defaults) {
|
|
590
|
+
const userTarget = fs.existsSync(path.join(HISTORY_DIR, 'versions', agent, version, 'home', `.${agent}`))
|
|
591
|
+
? path.join(HISTORY_DIR, 'versions', agent, version, 'home', `.${agent}`)
|
|
592
|
+
: path.join(USER_DIR, 'versions', agent, version, 'home', `.${agent}`);
|
|
593
|
+
if (!fs.existsSync(userTarget))
|
|
594
|
+
continue;
|
|
595
|
+
const symlinkPath = path.join(HOME, `.${agent}`);
|
|
596
|
+
let stat = null;
|
|
597
|
+
try {
|
|
598
|
+
stat = fs.lstatSync(symlinkPath);
|
|
599
|
+
}
|
|
600
|
+
catch { /* missing */ }
|
|
601
|
+
if (stat && stat.isSymbolicLink()) {
|
|
602
|
+
let current;
|
|
603
|
+
try {
|
|
604
|
+
current = fs.readlinkSync(symlinkPath);
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const resolved = path.resolve(path.dirname(symlinkPath), current);
|
|
610
|
+
if (resolved === path.resolve(userTarget))
|
|
611
|
+
continue; // already correct
|
|
612
|
+
try {
|
|
613
|
+
fs.unlinkSync(symlinkPath);
|
|
614
|
+
fs.symlinkSync(userTarget, symlinkPath);
|
|
615
|
+
repaired++;
|
|
616
|
+
}
|
|
617
|
+
catch { /* best-effort */ }
|
|
618
|
+
}
|
|
619
|
+
else if (!stat) {
|
|
620
|
+
try {
|
|
621
|
+
fs.symlinkSync(userTarget, symlinkPath);
|
|
622
|
+
repaired++;
|
|
623
|
+
}
|
|
624
|
+
catch { /* best-effort */ }
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (repaired > 0) {
|
|
628
|
+
console.error(`Repaired ${repaired} agent config symlink${repaired === 1 ? '' : 's'} to point at ~/.agents/versions/`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Move a directory from `src` to `dest`. No-op when src is absent. When dest
|
|
633
|
+
* already exists, merge by copying everything that isn't already there, then
|
|
634
|
+
* remove the source. Idempotent: re-running converges without duplicating.
|
|
635
|
+
*
|
|
636
|
+
* The merge-on-collision behavior matters because `ensureAgentsDir()` may have
|
|
637
|
+
* pre-created an empty `dest` during startup before the migrator gets to run.
|
|
638
|
+
*/
|
|
639
|
+
function moveDirOnce(src, dest) {
|
|
640
|
+
if (!fs.existsSync(src))
|
|
641
|
+
return;
|
|
642
|
+
if (!fs.existsSync(dest)) {
|
|
643
|
+
try {
|
|
644
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
645
|
+
fs.renameSync(src, dest);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
/* fall through to copy + remove */
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
copyDirSkipExisting(src, dest);
|
|
654
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
catch { /* best-effort */ }
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Move a single file from `src` to `dest`. No-op when src is absent. When
|
|
660
|
+
* dest exists, the source is simply deleted (the in-place version is treated
|
|
661
|
+
* as the canonical state). Idempotent.
|
|
662
|
+
*/
|
|
663
|
+
function moveFileOnce(src, dest) {
|
|
664
|
+
if (!fs.existsSync(src))
|
|
665
|
+
return;
|
|
666
|
+
if (fs.existsSync(dest)) {
|
|
667
|
+
try {
|
|
668
|
+
fs.unlinkSync(src);
|
|
669
|
+
}
|
|
670
|
+
catch { /* best-effort */ }
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
675
|
+
fs.renameSync(src, dest);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
try {
|
|
679
|
+
fs.copyFileSync(src, dest);
|
|
680
|
+
fs.unlinkSync(src);
|
|
681
|
+
}
|
|
682
|
+
catch { /* best-effort */ }
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/** Remove a directory tree if it exists and contains no files (best-effort). */
|
|
686
|
+
function rmEmptyDirTree(dir) {
|
|
687
|
+
if (!fs.existsSync(dir))
|
|
688
|
+
return;
|
|
689
|
+
try {
|
|
690
|
+
const entries = fs.readdirSync(dir);
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
const child = path.join(dir, entry);
|
|
693
|
+
try {
|
|
694
|
+
const stat = fs.statSync(child);
|
|
695
|
+
if (stat.isDirectory())
|
|
696
|
+
rmEmptyDirTree(child);
|
|
697
|
+
}
|
|
698
|
+
catch { /* skip */ }
|
|
699
|
+
}
|
|
700
|
+
if (fs.readdirSync(dir).length === 0)
|
|
701
|
+
fs.rmdirSync(dir);
|
|
702
|
+
}
|
|
703
|
+
catch { /* best-effort */ }
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Move durable runtime data into ~/.agents/.history/.
|
|
707
|
+
*
|
|
708
|
+
* Sources cleared:
|
|
709
|
+
* ~/.agents/sessions/ -> ~/.agents/.history/sessions/
|
|
710
|
+
* ~/.agents/versions/ -> ~/.agents/.history/versions/
|
|
711
|
+
* ~/.agents/.trash/ -> ~/.agents/.history/trash/
|
|
712
|
+
* ~/.agents/.backups/ -> ~/.agents/.history/backups/
|
|
713
|
+
* ~/.agents/routines/runs/ -> ~/.agents/.history/runs/
|
|
714
|
+
* ~/.agents/teams/agents/ -> ~/.agents/.history/teams/agents/
|
|
715
|
+
*
|
|
716
|
+
* Idempotent — skips entries whose destination already exists.
|
|
717
|
+
*/
|
|
718
|
+
function migrateRuntimeToHistory() {
|
|
719
|
+
moveDirOnce(path.join(USER_DIR, 'sessions'), path.join(HISTORY_DIR, 'sessions'));
|
|
720
|
+
moveDirOnce(path.join(USER_DIR, 'versions'), path.join(HISTORY_DIR, 'versions'));
|
|
721
|
+
// Some installs left both `.trash/` (current) and `trash/` (legacy lowercase).
|
|
722
|
+
// Move whichever exist into the history bucket.
|
|
723
|
+
moveDirOnce(path.join(USER_DIR, '.trash'), path.join(HISTORY_DIR, 'trash'));
|
|
724
|
+
moveDirOnce(path.join(USER_DIR, 'trash'), path.join(HISTORY_DIR, 'trash'));
|
|
725
|
+
moveDirOnce(path.join(USER_DIR, '.backups'), path.join(HISTORY_DIR, 'backups'));
|
|
726
|
+
moveDirOnce(path.join(USER_DIR, 'routines', 'runs'), path.join(HISTORY_DIR, 'runs'));
|
|
727
|
+
moveDirOnce(path.join(USER_DIR, 'teams', 'agents'), path.join(HISTORY_DIR, 'teams', 'agents'));
|
|
728
|
+
// Drop any empty leftover skeletons created mid-rename (e.g. `versions/<agent>/<v>/home/`
|
|
729
|
+
// recreated by a concurrent process). The real data is already under .history/.
|
|
730
|
+
rmEmptyDirTree(path.join(USER_DIR, 'versions'));
|
|
731
|
+
rmEmptyDirTree(path.join(USER_DIR, 'sessions'));
|
|
732
|
+
// Old empty zero-byte sessions.db at user root is obsolete once the new db lives at
|
|
733
|
+
// .history/sessions/sessions.db.
|
|
734
|
+
const oldSessionsDb = path.join(USER_DIR, 'sessions.db');
|
|
735
|
+
if (fs.existsSync(oldSessionsDb)) {
|
|
736
|
+
try {
|
|
737
|
+
if (fs.statSync(oldSessionsDb).size === 0)
|
|
738
|
+
fs.unlinkSync(oldSessionsDb);
|
|
739
|
+
}
|
|
740
|
+
catch { /* best-effort */ }
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Move regenerable runtime data into ~/.agents/.cache/.
|
|
745
|
+
*
|
|
746
|
+
* Sources cleared:
|
|
747
|
+
* ~/.agents/shims/ -> ~/.agents/.cache/shims/
|
|
748
|
+
* ~/.agents/bin/ -> ~/.agents/.cache/bin/
|
|
749
|
+
* ~/.agents/packages/ -> ~/.agents/.cache/packages/
|
|
750
|
+
* ~/.agents/plugins/ -> ~/.agents/.cache/plugins/
|
|
751
|
+
* ~/.agents/cloud/ -> ~/.agents/.cache/cloud/
|
|
752
|
+
* ~/.agents/drive/ -> ~/.agents/.cache/drive/
|
|
753
|
+
* ~/.agents/terminals/ -> ~/.agents/.cache/terminals/
|
|
754
|
+
* ~/.agents/logs/ -> ~/.agents/.cache/logs/
|
|
755
|
+
* ~/.agents/swarmify/ -> ~/.agents/.cache/swarmify/
|
|
756
|
+
* ~/.agents/runtime/ -> ~/.agents/.cache/state/
|
|
757
|
+
* ~/.agents/cache/ -> ~/.agents/.cache/ (flatten — already a cache subdir)
|
|
758
|
+
* ~/.agents/helpers/{daemon,pty,...} -> ~/.agents/.cache/helpers/...
|
|
759
|
+
* ~/.agents-system/helpers/{daemon,pty,...} -> ~/.agents/.cache/helpers/...
|
|
760
|
+
* ~/.agents/browser/<profile>/ -> ~/.agents/.cache/browser/<profile>/ (profiles/ dir stays)
|
|
761
|
+
* ~/.agents-system/.fetch/ -> ~/.agents/.cache/.fetch/
|
|
762
|
+
*
|
|
763
|
+
* Loose dot-files at user root that are runtime caches:
|
|
764
|
+
* ~/.agents/.cli-version-cache.json -> ~/.agents/.cache/.cli-version-cache.json
|
|
765
|
+
* ~/.agents/.update-check -> ~/.agents/.cache/.update-check
|
|
766
|
+
* ~/.agents/.migrated -> ~/.agents/.cache/.migrated
|
|
767
|
+
* ~/.agents/watchdog.log -> ~/.agents/.cache/logs/watchdog.log
|
|
768
|
+
*
|
|
769
|
+
* Idempotent.
|
|
770
|
+
*/
|
|
771
|
+
function migrateRuntimeToCache() {
|
|
772
|
+
moveDirOnce(path.join(USER_DIR, 'shims'), path.join(CACHE_DIR, 'shims'));
|
|
773
|
+
moveDirOnce(path.join(USER_DIR, 'bin'), path.join(CACHE_DIR, 'bin'));
|
|
774
|
+
moveDirOnce(path.join(USER_DIR, 'packages'), path.join(CACHE_DIR, 'packages'));
|
|
775
|
+
moveDirOnce(path.join(USER_DIR, 'plugins'), path.join(CACHE_DIR, 'plugins'));
|
|
776
|
+
moveDirOnce(path.join(USER_DIR, 'cloud'), path.join(CACHE_DIR, 'cloud'));
|
|
777
|
+
moveDirOnce(path.join(USER_DIR, 'drive'), path.join(CACHE_DIR, 'drive'));
|
|
778
|
+
// terminals/ stays at the top level: the agents-cli IDE extension publishes
|
|
779
|
+
// ~/.agents/terminals/live-terminals.json and would race with the move on
|
|
780
|
+
// VS Code restart. Leave the path where the extension expects it.
|
|
781
|
+
moveDirOnce(path.join(USER_DIR, 'logs'), path.join(CACHE_DIR, 'logs'));
|
|
782
|
+
moveDirOnce(path.join(USER_DIR, 'swarmify'), path.join(CACHE_DIR, 'swarmify'));
|
|
783
|
+
moveDirOnce(path.join(USER_DIR, 'runtime'), path.join(CACHE_DIR, 'state'));
|
|
784
|
+
// Pre-existing user `cache/` dir (claude usage cache, cloud-runs, etc.) — flatten
|
|
785
|
+
// so it's not confused with the new bucket. Its contents merge into .cache/.
|
|
786
|
+
const oldCache = path.join(USER_DIR, 'cache');
|
|
787
|
+
if (fs.existsSync(oldCache) && fs.statSync(oldCache).isDirectory()) {
|
|
788
|
+
try {
|
|
789
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
|
|
790
|
+
copyDirSkipExisting(oldCache, CACHE_DIR);
|
|
791
|
+
fs.rmSync(oldCache, { recursive: true, force: true });
|
|
792
|
+
}
|
|
793
|
+
catch { /* best-effort */ }
|
|
794
|
+
}
|
|
795
|
+
// helpers/ at user-root and system-root → .cache/helpers/
|
|
796
|
+
for (const root of [USER_DIR, SYSTEM_DIR]) {
|
|
797
|
+
const src = path.join(root, 'helpers');
|
|
798
|
+
if (!fs.existsSync(src))
|
|
799
|
+
continue;
|
|
800
|
+
const destBase = path.join(CACHE_DIR, 'helpers');
|
|
801
|
+
try {
|
|
802
|
+
fs.mkdirSync(destBase, { recursive: true, mode: 0o700 });
|
|
803
|
+
copyDirSkipExisting(src, destBase);
|
|
804
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
805
|
+
}
|
|
806
|
+
catch { /* best-effort */ }
|
|
807
|
+
}
|
|
808
|
+
// browser runtime — keep browser/profiles/ in place, move everything else under
|
|
809
|
+
// browser/ into .cache/browser/.
|
|
810
|
+
const browserSrc = path.join(USER_DIR, 'browser');
|
|
811
|
+
if (fs.existsSync(browserSrc) && fs.statSync(browserSrc).isDirectory()) {
|
|
812
|
+
let entries = [];
|
|
813
|
+
try {
|
|
814
|
+
entries = fs.readdirSync(browserSrc);
|
|
815
|
+
}
|
|
816
|
+
catch { /* skip */ }
|
|
817
|
+
for (const entry of entries) {
|
|
818
|
+
if (entry === 'profiles')
|
|
819
|
+
continue;
|
|
820
|
+
const src = path.join(browserSrc, entry);
|
|
821
|
+
const dest = path.join(CACHE_DIR, 'browser', entry);
|
|
822
|
+
moveDirOnce(src, dest);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// System-root operational state that should not live in the npm-shipped repo.
|
|
826
|
+
moveDirOnce(path.join(SYSTEM_DIR, '.fetch'), path.join(CACHE_DIR, '.fetch'));
|
|
827
|
+
moveDirOnce(path.join(SYSTEM_DIR, 'browser'), path.join(CACHE_DIR, 'browser'));
|
|
828
|
+
moveDirOnce(path.join(SYSTEM_DIR, 'state'), path.join(CACHE_DIR, 'state'));
|
|
829
|
+
moveDirOnce(path.join(SYSTEM_DIR, 'swarmify'), path.join(CACHE_DIR, 'swarmify'));
|
|
830
|
+
moveFileOnce(path.join(SYSTEM_DIR, '.cli-version-cache.json'), path.join(CACHE_DIR, '.cli-version-cache.json'));
|
|
831
|
+
moveFileOnce(path.join(SYSTEM_DIR, '.update-check'), path.join(CACHE_DIR, '.update-check'));
|
|
832
|
+
moveFileOnce(path.join(SYSTEM_DIR, '.migrated'), path.join(CACHE_DIR, '.migrated'));
|
|
833
|
+
moveFileOnce(path.join(SYSTEM_DIR, '.models-cache.json'), path.join(CACHE_DIR, '.models-cache.json'));
|
|
834
|
+
// Loose dot-files at user root that belong in the cache bucket.
|
|
835
|
+
moveFileOnce(path.join(USER_DIR, '.cli-version-cache.json'), path.join(CACHE_DIR, '.cli-version-cache.json'));
|
|
836
|
+
moveFileOnce(path.join(USER_DIR, '.update-check'), path.join(CACHE_DIR, '.update-check'));
|
|
837
|
+
moveFileOnce(path.join(USER_DIR, '.migrated'), path.join(CACHE_DIR, '.migrated'));
|
|
838
|
+
moveFileOnce(path.join(USER_DIR, '.models-cache.json'), path.join(CACHE_DIR, '.models-cache.json'));
|
|
839
|
+
moveFileOnce(path.join(USER_DIR, 'watchdog.log'), path.join(CACHE_DIR, 'logs', 'watchdog.log'));
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Merge a SQLite database file at `src` into the one at `dest`, then delete the
|
|
843
|
+
* source (including its WAL/SHM sidecars). User-side rows win on collision via
|
|
844
|
+
* INSERT OR IGNORE.
|
|
845
|
+
*
|
|
846
|
+
* If `dest` is missing, the source is simply moved into place (no merge). If
|
|
847
|
+
* the SQLite open fails (corrupt / zero-byte / locked), the source is dropped
|
|
848
|
+
* so the system repo returns to npm-shipped state — the data is stale-anyway
|
|
849
|
+
* runtime state, not user resources.
|
|
850
|
+
*/
|
|
851
|
+
async function mergeSqliteDb(src, dest) {
|
|
852
|
+
if (!fs.existsSync(src))
|
|
853
|
+
return;
|
|
854
|
+
try {
|
|
855
|
+
if (fs.statSync(src).size === 0) {
|
|
856
|
+
// Zero-byte legacy DB — drop sidecars too and leave dest alone.
|
|
857
|
+
try {
|
|
858
|
+
fs.unlinkSync(src);
|
|
859
|
+
}
|
|
860
|
+
catch { /* best-effort */ }
|
|
861
|
+
for (const ext of ['-shm', '-wal']) {
|
|
862
|
+
try {
|
|
863
|
+
fs.unlinkSync(src + ext);
|
|
864
|
+
}
|
|
865
|
+
catch { /* best-effort */ }
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch { /* best-effort */ }
|
|
871
|
+
if (!fs.existsSync(dest)) {
|
|
872
|
+
try {
|
|
873
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
874
|
+
fs.renameSync(src, dest);
|
|
875
|
+
for (const ext of ['-shm', '-wal']) {
|
|
876
|
+
if (fs.existsSync(src + ext)) {
|
|
877
|
+
try {
|
|
878
|
+
fs.renameSync(src + ext, dest + ext);
|
|
879
|
+
}
|
|
880
|
+
catch { /* best-effort */ }
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
catch { /* fall through to merge */ }
|
|
886
|
+
}
|
|
887
|
+
// Both files exist — open the dest DB and ATTACH src, then INSERT OR IGNORE
|
|
888
|
+
// every user table. Dynamic import keeps the sqlite shim off the hot path
|
|
889
|
+
// for CLI starts that don't actually need a merge.
|
|
890
|
+
try {
|
|
891
|
+
const sqliteMod = (await import('./sqlite.js'));
|
|
892
|
+
const Database = sqliteMod.default;
|
|
893
|
+
const db = new Database(dest);
|
|
894
|
+
try {
|
|
895
|
+
db.exec(`ATTACH DATABASE '${src.replace(/'/g, "''")}' AS src`);
|
|
896
|
+
const tables = db.prepare(`SELECT name FROM src.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`).all();
|
|
897
|
+
// FTS5 virtual tables maintain shadow tables (<name>_data, _idx, _content,
|
|
898
|
+
// _docsize, _config) with internal segids/pgnos that MUST stay consistent.
|
|
899
|
+
// Row-merging shadow tables across two DBs corrupts the index. Skip them
|
|
900
|
+
// here — the indexer reconstructs FTS content on the next scan.
|
|
901
|
+
const ftsVirtuals = new Set(db.prepare(`SELECT name FROM src.sqlite_master WHERE type='table' AND sql LIKE '%fts5%'`).all().map((r) => r.name));
|
|
902
|
+
const ftsShadowSuffixes = ['_data', '_idx', '_content', '_docsize', '_config'];
|
|
903
|
+
const isFtsShadow = (name) => {
|
|
904
|
+
for (const v of ftsVirtuals) {
|
|
905
|
+
for (const suf of ftsShadowSuffixes) {
|
|
906
|
+
if (name === `${v}${suf}`)
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return false;
|
|
911
|
+
};
|
|
912
|
+
for (const { name } of tables) {
|
|
913
|
+
if (ftsVirtuals.has(name) || isFtsShadow(name))
|
|
914
|
+
continue;
|
|
915
|
+
try {
|
|
916
|
+
const row = db.prepare(`SELECT sql FROM src.sqlite_master WHERE type='table' AND name = ?`).get(name);
|
|
917
|
+
if (row?.sql) {
|
|
918
|
+
const ddl = row.sql.replace(/^CREATE TABLE\s+/i, 'CREATE TABLE IF NOT EXISTS ');
|
|
919
|
+
db.exec(ddl);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
catch { /* table likely exists already */ }
|
|
923
|
+
const quoted = '"' + name.replace(/"/g, '""') + '"';
|
|
924
|
+
try {
|
|
925
|
+
db.exec(`INSERT OR IGNORE INTO main.${quoted} SELECT * FROM src.${quoted}`);
|
|
926
|
+
}
|
|
927
|
+
catch { /* schema drift — skip table */ }
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
db.exec('DETACH DATABASE src');
|
|
931
|
+
}
|
|
932
|
+
catch { /* best-effort */ }
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
try {
|
|
936
|
+
db.close();
|
|
937
|
+
}
|
|
938
|
+
catch { /* best-effort */ }
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
fs.unlinkSync(src);
|
|
942
|
+
}
|
|
943
|
+
catch { /* best-effort */ }
|
|
944
|
+
for (const ext of ['-shm', '-wal']) {
|
|
945
|
+
try {
|
|
946
|
+
fs.unlinkSync(src + ext);
|
|
947
|
+
}
|
|
948
|
+
catch { /* best-effort */ }
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// Merge failed — drop the source so the system repo returns to clean state.
|
|
953
|
+
// The user-side DB is authoritative; system-side rows were duplicate state.
|
|
954
|
+
try {
|
|
955
|
+
fs.unlinkSync(src);
|
|
956
|
+
}
|
|
957
|
+
catch { /* best-effort */ }
|
|
958
|
+
for (const ext of ['-shm', '-wal']) {
|
|
959
|
+
try {
|
|
960
|
+
fs.unlinkSync(src + ext);
|
|
961
|
+
}
|
|
962
|
+
catch { /* best-effort */ }
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Move ~/.agents-system/sessions/ into ~/.agents/.history/sessions/.
|
|
968
|
+
*
|
|
969
|
+
* Filesystem entries (claude/, index.jsonl, content_index.jsonl, etc.) merge
|
|
970
|
+
* directory-by-directory with user-side winning on collision. The bundled
|
|
971
|
+
* sessions.db (plus WAL/SHM) goes through mergeSqliteDb so historical rows
|
|
972
|
+
* land in the user DB.
|
|
973
|
+
*/
|
|
974
|
+
async function migrateSystemSessionsToHistory() {
|
|
975
|
+
const src = path.join(SYSTEM_DIR, 'sessions');
|
|
976
|
+
if (!fs.existsSync(src))
|
|
977
|
+
return;
|
|
978
|
+
const dest = path.join(HISTORY_DIR, 'sessions');
|
|
979
|
+
try {
|
|
980
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
981
|
+
}
|
|
982
|
+
catch { /* best-effort */ }
|
|
983
|
+
let entries;
|
|
984
|
+
try {
|
|
985
|
+
entries = fs.readdirSync(src, { withFileTypes: true });
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
for (const entry of entries) {
|
|
991
|
+
const s = path.join(src, entry.name);
|
|
992
|
+
if (entry.name === 'sessions.db' || entry.name === 'sessions.db-shm' || entry.name === 'sessions.db-wal') {
|
|
993
|
+
// Handled by mergeSqliteDb on the canonical .db name below.
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
const d = path.join(dest, entry.name);
|
|
997
|
+
if (entry.isDirectory()) {
|
|
998
|
+
moveDirOnce(s, d);
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
moveFileOnce(s, d);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
await mergeSqliteDb(path.join(src, 'sessions.db'), path.join(dest, 'sessions.db'));
|
|
1005
|
+
try {
|
|
1006
|
+
if (fs.readdirSync(src).length === 0)
|
|
1007
|
+
fs.rmdirSync(src);
|
|
1008
|
+
}
|
|
1009
|
+
catch { /* best-effort */ }
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Move ~/.agents-system/teams/ contents to ~/.agents/teams/ (registry/config)
|
|
1013
|
+
* and ~/.agents/.history/teams/ (per-run dirs).
|
|
1014
|
+
*
|
|
1015
|
+
* Strategy:
|
|
1016
|
+
* config.json, registry.json -> ~/.agents/teams/ (live state)
|
|
1017
|
+
* agents/ -> ~/.agents/.history/teams/agents/
|
|
1018
|
+
* <anything else> -> ~/.agents/.history/teams/<name>/ (per-run dirs)
|
|
1019
|
+
*/
|
|
1020
|
+
function migrateSystemTeamsToUser() {
|
|
1021
|
+
const src = path.join(SYSTEM_DIR, 'teams');
|
|
1022
|
+
if (!fs.existsSync(src))
|
|
1023
|
+
return;
|
|
1024
|
+
const liveDest = path.join(USER_DIR, 'teams');
|
|
1025
|
+
const historyDest = path.join(HISTORY_DIR, 'teams');
|
|
1026
|
+
let entries;
|
|
1027
|
+
try {
|
|
1028
|
+
entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
for (const entry of entries) {
|
|
1034
|
+
const s = path.join(src, entry.name);
|
|
1035
|
+
if (entry.name === 'config.json' || entry.name === 'registry.json') {
|
|
1036
|
+
moveFileOnce(s, path.join(liveDest, entry.name));
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (entry.name === 'agents' && entry.isDirectory()) {
|
|
1040
|
+
moveDirOnce(s, path.join(historyDest, 'agents'));
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (entry.isDirectory()) {
|
|
1044
|
+
moveDirOnce(s, path.join(historyDest, entry.name));
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
moveFileOnce(s, path.join(historyDest, entry.name));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
if (fs.readdirSync(src).length === 0)
|
|
1052
|
+
fs.rmdirSync(src);
|
|
1053
|
+
}
|
|
1054
|
+
catch { /* best-effort */ }
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Move ~/.agents-system/trash/ -> ~/.agents/.history/trash/.
|
|
1058
|
+
*
|
|
1059
|
+
* The system trash was where the legacy `mergeOverlappingVersionHomes()` parked
|
|
1060
|
+
* orphan version homes. Folding it into the history bucket keeps "everything
|
|
1061
|
+
* recoverable" in one place.
|
|
1062
|
+
*/
|
|
1063
|
+
function migrateSystemTrashToHistory() {
|
|
1064
|
+
const src = path.join(SYSTEM_DIR, 'trash');
|
|
1065
|
+
if (!fs.existsSync(src))
|
|
1066
|
+
return;
|
|
1067
|
+
moveDirOnce(src, path.join(HISTORY_DIR, 'trash'));
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Move ~/.agents-system/cache/ contents into ~/.agents/.cache/.
|
|
1071
|
+
*
|
|
1072
|
+
* Special cases:
|
|
1073
|
+
* sessions.db -> mergeSqliteDb into HISTORY_DIR/sessions/sessions.db
|
|
1074
|
+
* cloud-runs/ -> .cache/cloud-runs/
|
|
1075
|
+
* claude-usage.json -> drop (regenerable per-version cache)
|
|
1076
|
+
* <anything else> -> .cache/<name> (merge-on-collision)
|
|
1077
|
+
*/
|
|
1078
|
+
async function migrateSystemCacheToUserCache() {
|
|
1079
|
+
const src = path.join(SYSTEM_DIR, 'cache');
|
|
1080
|
+
if (!fs.existsSync(src))
|
|
1081
|
+
return;
|
|
1082
|
+
let entries;
|
|
1083
|
+
try {
|
|
1084
|
+
entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
for (const entry of entries) {
|
|
1090
|
+
const s = path.join(src, entry.name);
|
|
1091
|
+
if (entry.name === 'sessions.db' || entry.name === 'sessions.db-shm' || entry.name === 'sessions.db-wal') {
|
|
1092
|
+
// Sessions DB belongs in HISTORY, not CACHE — merge into the durable one.
|
|
1093
|
+
if (entry.name === 'sessions.db') {
|
|
1094
|
+
await mergeSqliteDb(s, path.join(HISTORY_DIR, 'sessions', 'sessions.db'));
|
|
1095
|
+
}
|
|
1096
|
+
// Sidecars get dropped if they outlived the merge or were orphaned.
|
|
1097
|
+
try {
|
|
1098
|
+
if (fs.existsSync(s))
|
|
1099
|
+
fs.unlinkSync(s);
|
|
1100
|
+
}
|
|
1101
|
+
catch { /* best-effort */ }
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (entry.name === 'claude-usage.json') {
|
|
1105
|
+
try {
|
|
1106
|
+
fs.unlinkSync(s);
|
|
1107
|
+
}
|
|
1108
|
+
catch { /* best-effort */ }
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const d = path.join(CACHE_DIR, entry.name);
|
|
1112
|
+
if (entry.isDirectory()) {
|
|
1113
|
+
moveDirOnce(s, d);
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
moveFileOnce(s, d);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
if (fs.readdirSync(src).length === 0)
|
|
1121
|
+
fs.rmdirSync(src);
|
|
1122
|
+
}
|
|
1123
|
+
catch { /* best-effort */ }
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Merge ~/.agents-system/cloud/tasks.db into ~/.agents/.cache/cloud/tasks.db.
|
|
1127
|
+
*/
|
|
1128
|
+
async function migrateSystemCloudToCache() {
|
|
1129
|
+
const srcDir = path.join(SYSTEM_DIR, 'cloud');
|
|
1130
|
+
if (!fs.existsSync(srcDir))
|
|
1131
|
+
return;
|
|
1132
|
+
await mergeSqliteDb(path.join(srcDir, 'tasks.db'), path.join(CACHE_DIR, 'cloud', 'tasks.db'));
|
|
1133
|
+
// Any other files in cloud/ get moved into the user cache bucket.
|
|
1134
|
+
let entries;
|
|
1135
|
+
try {
|
|
1136
|
+
entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
for (const entry of entries) {
|
|
1142
|
+
const s = path.join(srcDir, entry.name);
|
|
1143
|
+
const d = path.join(CACHE_DIR, 'cloud', entry.name);
|
|
1144
|
+
if (entry.isDirectory()) {
|
|
1145
|
+
moveDirOnce(s, d);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
moveFileOnce(s, d);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
if (fs.readdirSync(srcDir).length === 0)
|
|
1153
|
+
fs.rmdirSync(srcDir);
|
|
1154
|
+
}
|
|
1155
|
+
catch { /* best-effort */ }
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Legacy ~/.agents-system/swarm/ predates the rename to teams/. Fold any
|
|
1159
|
+
* per-agent dirs into ~/.agents/.history/teams/agents/ and drop the bookkeeping
|
|
1160
|
+
* JSONs (cache.json, config.json, teams.json) — those are regenerable.
|
|
1161
|
+
*/
|
|
1162
|
+
function migrateLegacySwarmToTeams() {
|
|
1163
|
+
const src = path.join(SYSTEM_DIR, 'swarm');
|
|
1164
|
+
if (!fs.existsSync(src))
|
|
1165
|
+
return;
|
|
1166
|
+
const agentsSrc = path.join(src, 'agents');
|
|
1167
|
+
if (fs.existsSync(agentsSrc)) {
|
|
1168
|
+
moveDirOnce(agentsSrc, path.join(HISTORY_DIR, 'teams', 'agents'));
|
|
1169
|
+
}
|
|
1170
|
+
for (const dead of ['cache.json', 'config.json', 'teams.json']) {
|
|
1171
|
+
const f = path.join(src, dead);
|
|
1172
|
+
if (fs.existsSync(f)) {
|
|
1173
|
+
try {
|
|
1174
|
+
fs.unlinkSync(f);
|
|
1175
|
+
}
|
|
1176
|
+
catch { /* best-effort */ }
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
if (fs.readdirSync(src).length === 0)
|
|
1181
|
+
fs.rmdirSync(src);
|
|
1182
|
+
}
|
|
1183
|
+
catch { /* best-effort */ }
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Move ~/.agents-system/repos/<alias>/ to ~/.agents-<alias>/ peer dirs.
|
|
1187
|
+
*
|
|
1188
|
+
* Extra DotAgents repos are user-defined config and belong as peer dirs to
|
|
1189
|
+
* ~/.agents/, not nested under the npm-shipped system repo. The dir name in
|
|
1190
|
+
* `repos/` becomes the alias.
|
|
1191
|
+
*/
|
|
1192
|
+
function migrateSystemReposToPeerDirs() {
|
|
1193
|
+
const src = path.join(SYSTEM_DIR, 'repos');
|
|
1194
|
+
if (!fs.existsSync(src))
|
|
1195
|
+
return;
|
|
1196
|
+
let entries;
|
|
1197
|
+
try {
|
|
1198
|
+
entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1199
|
+
}
|
|
1200
|
+
catch {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
let moved = 0;
|
|
1204
|
+
for (const entry of entries) {
|
|
1205
|
+
if (!entry.isDirectory())
|
|
1206
|
+
continue;
|
|
1207
|
+
const alias = entry.name;
|
|
1208
|
+
const s = path.join(src, alias);
|
|
1209
|
+
const d = path.join(HOME, `.agents-${alias}`);
|
|
1210
|
+
if (fs.existsSync(d)) {
|
|
1211
|
+
// Peer dir already exists — drop the system-side copy to avoid drift.
|
|
1212
|
+
try {
|
|
1213
|
+
fs.rmSync(s, { recursive: true, force: true });
|
|
1214
|
+
}
|
|
1215
|
+
catch { /* best-effort */ }
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
fs.renameSync(s, d);
|
|
1220
|
+
moved++;
|
|
1221
|
+
}
|
|
1222
|
+
catch { /* best-effort, leave in place */ }
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
if (fs.readdirSync(src).length === 0)
|
|
1226
|
+
fs.rmdirSync(src);
|
|
1227
|
+
}
|
|
1228
|
+
catch { /* best-effort */ }
|
|
1229
|
+
if (moved > 0) {
|
|
1230
|
+
console.error(`Moved ${moved} extra repo${moved === 1 ? '' : 's'} from ~/.agents-system/repos/ to ~/.agents-<alias>/ peer dirs`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Drop known-dead artifacts from ~/.agents-system/ that are pure regenerable
|
|
1235
|
+
* runtime state and don't belong anywhere:
|
|
1236
|
+
* bin/agents-keychain-* — per-version keychain helper, rebuilt on demand
|
|
1237
|
+
* shims/ — moved long ago, only empty leftover remains
|
|
1238
|
+
*/
|
|
1239
|
+
function dropDeadSystemArtifacts() {
|
|
1240
|
+
const binDir = path.join(SYSTEM_DIR, 'bin');
|
|
1241
|
+
if (fs.existsSync(binDir)) {
|
|
1242
|
+
try {
|
|
1243
|
+
for (const name of fs.readdirSync(binDir)) {
|
|
1244
|
+
if (name.startsWith('agents-keychain-')) {
|
|
1245
|
+
try {
|
|
1246
|
+
fs.unlinkSync(path.join(binDir, name));
|
|
1247
|
+
}
|
|
1248
|
+
catch { /* best-effort */ }
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (fs.readdirSync(binDir).length === 0)
|
|
1252
|
+
fs.rmdirSync(binDir);
|
|
1253
|
+
}
|
|
1254
|
+
catch { /* best-effort */ }
|
|
1255
|
+
}
|
|
1256
|
+
const shimsDir = path.join(SYSTEM_DIR, 'shims');
|
|
1257
|
+
if (fs.existsSync(shimsDir)) {
|
|
1258
|
+
try {
|
|
1259
|
+
if (fs.readdirSync(shimsDir).length === 0)
|
|
1260
|
+
fs.rmdirSync(shimsDir);
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* best-effort */ }
|
|
1263
|
+
}
|
|
1264
|
+
// After migrateSystemVersionsToUser() moves real version dirs out, the system
|
|
1265
|
+
// may still hold an empty `versions/<agent>/` skeleton with a stray .DS_Store.
|
|
1266
|
+
// Sweep it: if every leaf file is .DS_Store, drop the tree entirely.
|
|
1267
|
+
const versionsDir = path.join(SYSTEM_DIR, 'versions');
|
|
1268
|
+
if (fs.existsSync(versionsDir)) {
|
|
1269
|
+
try {
|
|
1270
|
+
if (containsOnlyDsStore(versionsDir)) {
|
|
1271
|
+
fs.rmSync(versionsDir, { recursive: true, force: true });
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
catch { /* best-effort */ }
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function containsOnlyDsStore(dir) {
|
|
1278
|
+
let entries;
|
|
1279
|
+
try {
|
|
1280
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
for (const entry of entries) {
|
|
1286
|
+
if (entry.isDirectory()) {
|
|
1287
|
+
if (!containsOnlyDsStore(path.join(dir, entry.name)))
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
else if (entry.name !== '.DS_Store') {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* After the sweep runs, warn (once per invocation) about any unrecognized
|
|
1298
|
+
* subdirectory left in ~/.agents-system/. The system repo is the npm-shipped
|
|
1299
|
+
* defaults — anything outside the allowlist is drift that future maintainers
|
|
1300
|
+
* need to handle explicitly.
|
|
1301
|
+
*/
|
|
1302
|
+
function warnSystemOrphans() {
|
|
1303
|
+
const SHIPPED_ALLOWLIST = new Set([
|
|
1304
|
+
// resource directories shipped by the npm package
|
|
1305
|
+
'commands', 'hooks', 'skills', 'rules', 'mcp', 'permissions', 'subagents', 'profiles', 'agents',
|
|
1306
|
+
// top-level metadata files
|
|
1307
|
+
'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
|
|
1308
|
+
// git + repo metadata
|
|
1309
|
+
'.git', '.githooks', '.gitignore', '.assets', '.environment', '.plans',
|
|
1310
|
+
// benign noise that's safe to ignore
|
|
1311
|
+
'.DS_Store', '.claude',
|
|
1312
|
+
]);
|
|
1313
|
+
let entries;
|
|
1314
|
+
try {
|
|
1315
|
+
entries = fs.readdirSync(SYSTEM_DIR);
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
// Transient runtime sockets (.sock) are bound by long-running helpers like the
|
|
1321
|
+
// VS Code extension; they're live state, not stale data, and a future fix in
|
|
1322
|
+
// those helpers will move them into ~/.agents/.cache/ — until then, suppress.
|
|
1323
|
+
const orphans = entries.filter((name) => !SHIPPED_ALLOWLIST.has(name) && !name.endsWith('.sock'));
|
|
1324
|
+
if (orphans.length === 0)
|
|
1325
|
+
return;
|
|
1326
|
+
console.error(`~/.agents-system/ has unexpected entries (not part of the npm-shipped defaults): ${orphans.join(', ')}`);
|
|
1327
|
+
}
|
|
1328
|
+
const VERSION_RESOURCE_FLAT_KEYS = ['commands', 'skills', 'hooks', 'memory', 'subagents', 'plugins', 'workflows', 'permissions', 'mcp'];
|
|
1329
|
+
/**
|
|
1330
|
+
* Convert agents.yaml versions: entries from the old flat name-list format to
|
|
1331
|
+
* the new pattern format. Flat entries are detected by checking whether all
|
|
1332
|
+
* items in the array lack a ':' separator (plain names have no source prefix).
|
|
1333
|
+
*
|
|
1334
|
+
* The rulesPreset field is preserved. Flat resource lists are dropped — the
|
|
1335
|
+
* next `agents sync` will write default patterns (system:* user:* project:*).
|
|
1336
|
+
*
|
|
1337
|
+
* Idempotent: entries already in pattern format are left untouched.
|
|
1338
|
+
*/
|
|
1339
|
+
function migrateVersionResourcesToPatterns() {
|
|
1340
|
+
const metaFile = path.join(USER_DIR, 'agents.yaml');
|
|
1341
|
+
if (!fs.existsSync(metaFile))
|
|
1342
|
+
return;
|
|
1343
|
+
let meta;
|
|
1344
|
+
try {
|
|
1345
|
+
const raw = fs.readFileSync(metaFile, 'utf-8');
|
|
1346
|
+
meta = yaml.parse(raw) || {};
|
|
1347
|
+
}
|
|
1348
|
+
catch {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const versions = meta.versions;
|
|
1352
|
+
if (!versions || typeof versions !== 'object')
|
|
1353
|
+
return;
|
|
1354
|
+
let changed = false;
|
|
1355
|
+
for (const agentVersions of Object.values(versions)) {
|
|
1356
|
+
if (!agentVersions || typeof agentVersions !== 'object')
|
|
1357
|
+
continue;
|
|
1358
|
+
for (const vr of Object.values(agentVersions)) {
|
|
1359
|
+
if (!vr || typeof vr !== 'object')
|
|
1360
|
+
continue;
|
|
1361
|
+
for (const key of VERSION_RESOURCE_FLAT_KEYS) {
|
|
1362
|
+
const val = vr[key];
|
|
1363
|
+
if (!Array.isArray(val) || val.length === 0)
|
|
1364
|
+
continue;
|
|
1365
|
+
// Detect legacy: all items are plain names (no ':' separator)
|
|
1366
|
+
if (val.every(item => typeof item === 'string' && !item.includes(':'))) {
|
|
1367
|
+
if (key === 'memory') {
|
|
1368
|
+
// memory was a single-element array holding the preset name — move to rulesPreset
|
|
1369
|
+
if (val.length === 1 && !vr['rulesPreset']) {
|
|
1370
|
+
vr['rulesPreset'] = val[0];
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
delete vr[key];
|
|
1374
|
+
changed = true;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (changed) {
|
|
1380
|
+
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n\n';
|
|
1381
|
+
fs.writeFileSync(metaFile, META_HEADER + yaml.stringify(meta), 'utf-8');
|
|
1382
|
+
console.error('Migrated agents.yaml versions: entries to pattern format');
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
198
1385
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
199
|
-
export function runMigration() {
|
|
1386
|
+
export async function runMigration() {
|
|
200
1387
|
migrateAgentsYaml();
|
|
201
1388
|
deleteSystemPromptsJson();
|
|
202
1389
|
migrateSystemConfigJson();
|
|
203
1390
|
migratePromptcutsIntoHooks();
|
|
204
|
-
|
|
1391
|
+
migrateSystemVersionsToUser();
|
|
1392
|
+
mergeOverlappingVersionHomes();
|
|
205
1393
|
migrateRunsIntoRoutines();
|
|
206
1394
|
migrateTrashToHidden();
|
|
207
1395
|
migrateBackupsToHidden();
|
|
1396
|
+
migrateAliasesToUser();
|
|
1397
|
+
migratePermissionSetsToPresets();
|
|
1398
|
+
deleteUserLinearJson();
|
|
1399
|
+
deleteUserPromptsJson();
|
|
1400
|
+
cleanupUserConfigJson();
|
|
1401
|
+
cleanupEmptyTopLevelRuns();
|
|
1402
|
+
foldUserHooksYamlIntoAgentsYaml();
|
|
1403
|
+
foldBrowserProfilesIntoAgentsYaml();
|
|
1404
|
+
migrateVersionResourcesToPatterns();
|
|
1405
|
+
// Bucket moves: collapse runtime state into ~/.agents/.history and ~/.agents/.cache.
|
|
1406
|
+
migrateRuntimeToHistory();
|
|
1407
|
+
migrateRuntimeToCache();
|
|
1408
|
+
// System-repo sweep: move every remaining operational dir into its canonical
|
|
1409
|
+
// user-bucket location, then drop known-dead artifacts and warn about
|
|
1410
|
+
// anything we don't recognize. Order: durable (sessions/teams/trash/repos/
|
|
1411
|
+
// legacy-swarm) -> caches (cache/, cloud/) -> drops -> orphan check.
|
|
1412
|
+
await migrateSystemSessionsToHistory();
|
|
1413
|
+
migrateSystemTeamsToUser();
|
|
1414
|
+
migrateSystemTrashToHistory();
|
|
1415
|
+
migrateLegacySwarmToTeams();
|
|
1416
|
+
migrateSystemReposToPeerDirs();
|
|
1417
|
+
await migrateSystemCacheToUserCache();
|
|
1418
|
+
await migrateSystemCloudToCache();
|
|
1419
|
+
dropDeadSystemArtifacts();
|
|
1420
|
+
warnSystemOrphans();
|
|
1421
|
+
// Symlink repair runs LAST so it can find the post-move version homes.
|
|
1422
|
+
repairAgentConfigSymlinks();
|
|
208
1423
|
}
|