abelworkflow 1.1.3 → 1.2.1

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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -29
  3. package/extensions/gpt-responses-compat.ts +213 -0
  4. package/extensions/k2think-compat.ts +40 -0
  5. package/lib/cli/args.mjs +5 -19
  6. package/lib/cli/main.mjs +105 -72
  7. package/lib/cli/prompts.mjs +9 -2
  8. package/lib/config/dotenv.mjs +1 -1
  9. package/lib/config/jsonc.mjs +1 -1
  10. package/lib/config/store.mjs +197 -45
  11. package/lib/config/toml.mjs +147 -453
  12. package/lib/installer/assets.mjs +67 -45
  13. package/lib/installer/install.mjs +202 -45
  14. package/lib/installer/links.mjs +124 -101
  15. package/lib/installer/lock.mjs +82 -0
  16. package/lib/installer/state.mjs +14 -21
  17. package/lib/paths.mjs +9 -9
  18. package/lib/providers/claude.mjs +58 -114
  19. package/lib/providers/codex.mjs +101 -198
  20. package/lib/providers/pi.mjs +331 -496
  21. package/lib/providers/skills.mjs +88 -68
  22. package/lib/providers/url.mjs +35 -0
  23. package/lib/templates/workflow/commands/abel-design.md +23 -161
  24. package/lib/templates/workflow/commands/abel-diagnose.md +30 -56
  25. package/lib/templates/workflow/commands/abel-implement.md +30 -150
  26. package/lib/templates/workflow/commands/abel-init.md +1 -1
  27. package/lib/tools/cli-installer.mjs +29 -190
  28. package/package.json +7 -9
  29. package/skills/context7-auto-research/context7-api.cjs +75 -17
  30. package/skills/dev-browser/SKILL.md +15 -1
  31. package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
  32. package/skills/dev-browser/dist/scripts/start.js +8 -36
  33. package/skills/dev-browser/dist/src/client.d.ts +3 -4
  34. package/skills/dev-browser/dist/src/client.js +117 -117
  35. package/skills/dev-browser/dist/src/entrypoint.d.ts +1 -1
  36. package/skills/dev-browser/dist/src/entrypoint.js +38 -31
  37. package/skills/dev-browser/dist/src/index.d.ts +0 -1
  38. package/skills/dev-browser/dist/src/index.js +0 -1
  39. package/skills/dev-browser/dist/src/page-api.d.ts +0 -1
  40. package/skills/dev-browser/dist/src/page-api.js +0 -1
  41. package/skills/dev-browser/dist/src/relay.d.ts +0 -1
  42. package/skills/dev-browser/dist/src/relay.js +53 -7
  43. package/skills/dev-browser/dist/src/runtime.d.ts +3 -14
  44. package/skills/dev-browser/dist/src/runtime.js +5 -15
  45. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -1
  46. package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -1
  47. package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -1
  48. package/skills/dev-browser/dist/src/snapshot/index.js +0 -1
  49. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -1
  50. package/skills/dev-browser/dist/src/snapshot/inject.js +0 -1
  51. package/skills/dev-browser/dist/src/standalone.d.ts +0 -1
  52. package/skills/dev-browser/dist/src/standalone.js +2 -3
  53. package/skills/dev-browser/dist/src/startup.d.ts +3 -12
  54. package/skills/dev-browser/dist/src/startup.js +6 -10
  55. package/skills/dev-browser/dist/src/target-registry.d.ts +0 -1
  56. package/skills/dev-browser/dist/src/target-registry.js +0 -1
  57. package/skills/dev-browser/dist/src/types.d.ts +0 -1
  58. package/skills/dev-browser/dist/src/types.js +0 -1
  59. package/skills/dev-browser/package-lock.json +1 -1
  60. package/skills/dev-browser/package.json +2 -2
  61. package/skills/grok-search/.env.example +1 -1
  62. package/skills/grok-search/SKILL.md +13 -16
  63. package/skills/grok-search/scripts/_dotenv.py +1 -1
  64. package/skills/grok-search/scripts/groksearch_cli.py +271 -195
  65. package/skills/time/SKILL.md +5 -2
  66. package/skills/time/scripts/time_cli.py +61 -22
  67. package/extensions/pi-gpt-responses-compat/index.ts +0 -25
  68. package/lib/templates/codex/config-base.toml +0 -103
  69. package/skills/dev-browser/dist/scripts/start.d.ts.map +0 -1
  70. package/skills/dev-browser/dist/scripts/start.js.map +0 -1
  71. package/skills/dev-browser/dist/src/client.d.ts.map +0 -1
  72. package/skills/dev-browser/dist/src/client.js.map +0 -1
  73. package/skills/dev-browser/dist/src/entrypoint.d.ts.map +0 -1
  74. package/skills/dev-browser/dist/src/entrypoint.js.map +0 -1
  75. package/skills/dev-browser/dist/src/index.d.ts.map +0 -1
  76. package/skills/dev-browser/dist/src/index.js.map +0 -1
  77. package/skills/dev-browser/dist/src/page-api.d.ts.map +0 -1
  78. package/skills/dev-browser/dist/src/page-api.js.map +0 -1
  79. package/skills/dev-browser/dist/src/relay.d.ts.map +0 -1
  80. package/skills/dev-browser/dist/src/relay.js.map +0 -1
  81. package/skills/dev-browser/dist/src/runtime.d.ts.map +0 -1
  82. package/skills/dev-browser/dist/src/runtime.js.map +0 -1
  83. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +0 -1
  84. package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +0 -1
  85. package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +0 -1
  86. package/skills/dev-browser/dist/src/snapshot/index.js.map +0 -1
  87. package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +0 -1
  88. package/skills/dev-browser/dist/src/snapshot/inject.js.map +0 -1
  89. package/skills/dev-browser/dist/src/standalone.d.ts.map +0 -1
  90. package/skills/dev-browser/dist/src/standalone.js.map +0 -1
  91. package/skills/dev-browser/dist/src/startup.d.ts.map +0 -1
  92. package/skills/dev-browser/dist/src/startup.js.map +0 -1
  93. package/skills/dev-browser/dist/src/target-registry.d.ts.map +0 -1
  94. package/skills/dev-browser/dist/src/target-registry.js.map +0 -1
  95. package/skills/dev-browser/dist/src/types.d.ts.map +0 -1
  96. package/skills/dev-browser/dist/src/types.js.map +0 -1
  97. package/skills/grok-search/gitignore.template +0 -4
  98. package/skills/grok-search/scripts/groksearch_entry.py +0 -131
@@ -3,7 +3,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
3
3
  import { backupIfNeeded, pathExists, pathTargetExists } from "../config/store.mjs";
4
4
  import { containsPath, isWindows } from "../paths.mjs";
5
5
  import { hashBytes } from "../utils.mjs";
6
- import { ensureManagedContainerDirectory } from "./assets.mjs";
6
+ import { ensureManagedContainerDirectories } from "./assets.mjs";
7
7
 
8
8
  async function createManagedTargetState(targetPath, sourcePath, kind, mode, status) {
9
9
  let targetHash;
@@ -17,6 +17,11 @@ async function createSymlink(targetPath, sourcePath, linkType) {
17
17
  await fs.symlink(sourcePath, targetPath, linkType);
18
18
  }
19
19
 
20
+ function recordLinkResult(results, options, result) {
21
+ results.push(result);
22
+ options.onResult?.(result);
23
+ }
24
+
20
25
  async function symlinkPointsTo(targetPath, sourcePath) {
21
26
  try {
22
27
  if (!(await fs.lstat(targetPath)).isSymbolicLink()) return false;
@@ -34,14 +39,6 @@ function previousStateOwnsSource(previousState, sourcePath, kind) {
34
39
  && previousState.kind === kind;
35
40
  }
36
41
 
37
- function isUnverifiedWindowsCopy(previousState, kind) {
38
- return isWindows()
39
- && kind === "file"
40
- && previousState?.kind === "file"
41
- && previousState.mode === "copy"
42
- && !previousState.targetHash;
43
- }
44
-
45
42
  async function targetMatchesPreviousState(targetPath, previousState) {
46
43
  if (!previousState || !(await pathExists(targetPath))) return false;
47
44
  const targetStat = await fs.lstat(targetPath);
@@ -84,24 +81,40 @@ async function ordinaryFileMatchesSource(targetPath, sourcePath, kind) {
84
81
  }
85
82
  }
86
83
 
87
- async function moveReplaceableTarget(targetPath) {
84
+ async function moveReplaceableTarget(targetPath, options) {
88
85
  const targetStat = await fs.lstat(targetPath);
89
86
  if (targetStat.isDirectory() && !targetStat.isSymbolicLink()) return false;
87
+ await options.beforeMutation?.();
90
88
  await backupIfNeeded(targetPath);
91
89
  return true;
92
90
  }
93
91
 
92
+ async function removeRecordedTarget(targetPath, previousState, options = {}) {
93
+ if (!(await pathExists(targetPath))) return { targetPath, status: "removed" };
94
+ const matchesPrevious = await targetMatchesPreviousState(targetPath, previousState);
95
+ if (matchesPrevious) {
96
+ await options.beforeMutation?.();
97
+ await fs.rm(targetPath, { recursive: true, force: true });
98
+ return { targetPath, status: "removed" };
99
+ }
100
+ if (options.force && await moveReplaceableTarget(targetPath, options)) {
101
+ return { targetPath, status: "removed" };
102
+ }
103
+ return { targetPath, status: "conflict" };
104
+ }
105
+
94
106
  async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTargets = {}, options = {}) {
95
107
  const force = options.force ?? false;
108
+ if (!(await pathExists(dirname(targetPath)))) await options.beforeMutation?.();
96
109
  await fs.mkdir(dirname(targetPath), { recursive: true });
97
110
  const sourceExists = await pathTargetExists(sourcePath);
98
111
  const targetExists = await pathExists(targetPath);
99
112
  const previousState = previousLinkedTargets[targetPath];
100
113
  const wasPreviouslyManaged = previousStateOwnsSource(previousState, sourcePath, kind);
101
- const isUnverifiedCopy = wasPreviouslyManaged && isUnverifiedWindowsCopy(previousState, kind);
102
114
 
103
115
  if (targetExists && await symlinkPointsTo(targetPath, sourcePath)) {
104
116
  if (!sourceExists) {
117
+ await options.beforeMutation?.();
105
118
  await fs.rm(targetPath, { recursive: true, force: true });
106
119
  return { targetPath, status: "removed" };
107
120
  }
@@ -111,12 +124,12 @@ async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTar
111
124
  if (!sourceExists) {
112
125
  if (!targetExists) return { targetPath, status: "removed" };
113
126
  if (!wasPreviouslyManaged) return { targetPath, status: "skipped" };
114
- if (isUnverifiedCopy) return { targetPath, status: "conflict" };
115
127
  if (await targetMatchesPreviousState(targetPath, previousState)) {
128
+ await options.beforeMutation?.();
116
129
  await fs.rm(targetPath, { recursive: true, force: true });
117
130
  return { targetPath, status: "removed" };
118
131
  }
119
- if (force && await moveReplaceableTarget(targetPath)) return { targetPath, status: "removed" };
132
+ if (force && await moveReplaceableTarget(targetPath, options)) return { targetPath, status: "removed" };
120
133
  return { targetPath, status: "conflict" };
121
134
  }
122
135
 
@@ -125,23 +138,18 @@ async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTar
125
138
  }
126
139
 
127
140
  if (targetExists) {
128
- if (isUnverifiedCopy) {
129
- if (await ordinaryFileMatchesSource(targetPath, sourcePath, kind)) {
130
- return createManagedTargetState(targetPath, sourcePath, kind, "copy", "unchanged");
131
- }
132
- return { targetPath, status: "conflict" };
133
- }
134
141
  const matchesPrevious = wasPreviouslyManaged && await targetMatchesPreviousState(targetPath, previousState);
135
142
  if (matchesPrevious && await ordinaryFileMatchesSource(targetPath, sourcePath, kind)) {
136
143
  return createManagedTargetState(targetPath, sourcePath, kind, previousState.mode, "unchanged");
137
144
  }
138
- if ((!matchesPrevious && !force) || !(await moveReplaceableTarget(targetPath))) {
145
+ if ((!matchesPrevious && !force) || !(await moveReplaceableTarget(targetPath, options))) {
139
146
  return { targetPath, status: "conflict" };
140
147
  }
141
148
  }
142
149
 
143
150
  const linkType = isWindows() ? (kind === "dir" ? "junction" : "file") : kind;
144
151
  try {
152
+ await options.beforeMutation?.();
145
153
  await createSymlink(targetPath, sourcePath, linkType);
146
154
  return createManagedTargetState(targetPath, sourcePath, kind, "symlink", "linked");
147
155
  } catch (error) {
@@ -185,7 +193,8 @@ async function pathIsFile(path) {
185
193
  }
186
194
  }
187
195
 
188
- async function ensureProviderRootDirectory(homeDir, ...segments) {
196
+ async function ensureProviderRootDirectory(homeDir, segments, options = {}) {
197
+ if (!(await pathExists(homeDir))) await options.beforeMutation?.();
189
198
  await fs.mkdir(homeDir, { recursive: true });
190
199
  let directory = homeDir;
191
200
  for (const segment of segments) {
@@ -196,13 +205,20 @@ async function ensureProviderRootDirectory(homeDir, ...segments) {
196
205
  } catch (error) {
197
206
  if (error?.code !== "ENOENT") throw error;
198
207
  try {
208
+ await options.beforeMutation?.();
199
209
  await fs.mkdir(directory);
200
210
  } catch (mkdirError) {
201
211
  if (mkdirError?.code === "EEXIST") {
212
+ const concurrentTarget = await fs.lstat(directory);
213
+ if (!concurrentTarget.isSymbolicLink() && concurrentTarget.isDirectory()) continue;
202
214
  throw new Error(`Provider root conflict at ${directory}`, { cause: mkdirError });
203
215
  }
204
216
  throw mkdirError;
205
217
  }
218
+ const createdTarget = await fs.lstat(directory);
219
+ if (createdTarget.isSymbolicLink() || !createdTarget.isDirectory()) {
220
+ throw new Error(`Provider root conflict at ${directory}`);
221
+ }
206
222
  continue;
207
223
  }
208
224
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
@@ -257,37 +273,18 @@ function isWithinManagedRoot(targetPath, managedSourceRoot, pathOps = { isAbsolu
257
273
  async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames, previousLinkedTargets, options = {}) {
258
274
  const resolvedTargetDir = resolve(targetDir);
259
275
  const expectedNameSet = new Set(expectedNames);
260
- const results = [];
276
+ const candidates = [];
261
277
  for (const [targetPath, previousState] of Object.entries(previousLinkedTargets)) {
262
278
  const resolvedTargetPath = resolve(targetPath);
263
279
  if (targetPath !== resolvedTargetPath || dirname(resolvedTargetPath) !== resolvedTargetDir) continue;
264
280
  if (!isWithinManagedRoot(resolve(previousState.sourcePath), resolve(managedSourceRoot))) continue;
265
281
  if (expectedNameSet.has(basename(resolvedTargetPath)) && await pathTargetExists(previousState.sourcePath)) continue;
266
- if (!(await pathExists(resolvedTargetPath))) {
267
- results.push({ targetPath, status: "removed" });
268
- continue;
269
- }
270
- const isUnverifiedCopy = isUnverifiedWindowsCopy(previousState, previousState.kind);
271
- const matchesPrevious = await targetMatchesPreviousState(resolvedTargetPath, previousState)
272
- || (isUnverifiedCopy && await ordinaryFileMatchesSource(
273
- resolvedTargetPath,
274
- previousState.sourcePath,
275
- previousState.kind
276
- ));
277
- if (matchesPrevious) {
278
- await fs.rm(resolvedTargetPath, { recursive: true, force: true });
279
- results.push({ targetPath, status: "removed" });
280
- continue;
281
- }
282
- if (isUnverifiedCopy) {
283
- results.push({ targetPath, status: "conflict" });
284
- continue;
285
- }
286
- if (options.force && await moveReplaceableTarget(resolvedTargetPath)) {
287
- results.push({ targetPath, status: "removed" });
288
- continue;
289
- }
290
- results.push({ targetPath, status: "conflict" });
282
+ candidates.push([resolvedTargetPath, previousState]);
283
+ }
284
+ if (candidates.length > 0) await options.validateTargetDir?.(targetDir);
285
+ const results = [];
286
+ for (const [targetPath, previousState] of candidates) {
287
+ recordLinkResult(results, options, await removeRecordedTarget(targetPath, previousState, options));
291
288
  }
292
289
  return results;
293
290
  }
@@ -304,7 +301,7 @@ async function linkSkillDirectories(baseDir, paths, previousLinkedTargets, optio
304
301
  options
305
302
  ));
306
303
  for (const skillName of skillNames) {
307
- results.push(await ensureManagedLink(
304
+ recordLinkResult(results, options, await ensureManagedLink(
308
305
  join(baseDir, "skills", skillName),
309
306
  join(skillsRoot, skillName),
310
307
  "dir",
@@ -320,7 +317,7 @@ async function linkCommands(targetDir, paths, previousLinkedTargets, options) {
320
317
  const commandFiles = await getCommandNames(commandsRoot);
321
318
  const results = await pruneManagedTargets(targetDir, commandsRoot, commandFiles, previousLinkedTargets, options);
322
319
  for (const fileName of commandFiles) {
323
- results.push(await ensureManagedLink(
320
+ recordLinkResult(results, options, await ensureManagedLink(
324
321
  join(targetDir, fileName),
325
322
  join(commandsRoot, fileName),
326
323
  "file",
@@ -332,68 +329,56 @@ async function linkCommands(targetDir, paths, previousLinkedTargets, options) {
332
329
  }
333
330
 
334
331
  async function linkClaude(paths, previousLinkedTargets = {}, options = {}) {
335
- const claudeDir = await ensureProviderRootDirectory(paths.homeDir, ".claude");
336
- await ensureManagedContainerDirectory(join(claudeDir, "commands"), join(paths.agentsDir, "commands"));
337
- await ensureManagedContainerDirectory(join(claudeDir, "skills"), join(paths.agentsDir, "skills"));
338
- return [
339
- await ensureManagedLink(
340
- join(claudeDir, "CLAUDE.md"),
341
- join(paths.agentsDir, "AGENTS.md"),
342
- "file",
343
- previousLinkedTargets,
344
- options
345
- ),
346
- ...await linkCommands(join(claudeDir, "commands"), paths, previousLinkedTargets, options),
347
- ...await linkSkillDirectories(claudeDir, paths, previousLinkedTargets, options)
348
- ];
332
+ const claudeDir = await ensureProviderRootDirectory(paths.homeDir, [".claude"], options);
333
+ await ensureManagedContainerDirectories([
334
+ join(claudeDir, "commands"),
335
+ join(claudeDir, "skills")
336
+ ], options);
337
+ const results = [];
338
+ recordLinkResult(results, options, await ensureManagedLink(
339
+ join(claudeDir, "CLAUDE.md"),
340
+ join(paths.agentsDir, "AGENTS.md"),
341
+ "file",
342
+ previousLinkedTargets,
343
+ options
344
+ ));
345
+ results.push(...await linkCommands(join(claudeDir, "commands"), paths, previousLinkedTargets, options));
346
+ results.push(...await linkSkillDirectories(claudeDir, paths, previousLinkedTargets, options));
347
+ return results;
349
348
  }
350
349
 
351
350
  async function linkCodex(paths, previousLinkedTargets = {}, options = {}) {
352
- const codexDir = await ensureProviderRootDirectory(paths.homeDir, ".codex");
353
- await ensureManagedContainerDirectory(join(codexDir, "skills"), join(paths.agentsDir, "skills"));
354
- await ensureManagedContainerDirectory(join(codexDir, "prompts"), join(paths.agentsDir, "commands"));
355
- return [
356
- await ensureManagedLink(
357
- join(codexDir, "AGENTS.md"),
358
- join(paths.agentsDir, "AGENTS.md"),
359
- "file",
360
- previousLinkedTargets,
361
- options
362
- ),
363
- ...await linkSkillDirectories(codexDir, paths, previousLinkedTargets, options),
364
- ...await linkCommands(join(codexDir, "prompts"), paths, previousLinkedTargets, options)
365
- ];
351
+ const codexDir = await ensureProviderRootDirectory(paths.homeDir, [".codex"], options);
352
+ await ensureManagedContainerDirectories([
353
+ join(codexDir, "skills"),
354
+ join(codexDir, "prompts")
355
+ ], options);
356
+ const results = [];
357
+ recordLinkResult(results, options, await ensureManagedLink(
358
+ join(codexDir, "AGENTS.md"),
359
+ join(paths.agentsDir, "AGENTS.md"),
360
+ "file",
361
+ previousLinkedTargets,
362
+ options
363
+ ));
364
+ results.push(...await linkSkillDirectories(codexDir, paths, previousLinkedTargets, options));
365
+ results.push(...await linkCommands(join(codexDir, "prompts"), paths, previousLinkedTargets, options));
366
+ return results;
366
367
  }
367
368
 
368
- async function linkPi(paths, previousLinkedTargets = {}, options = {}) {
369
- const piAgentDir = await ensureProviderRootDirectory(paths.homeDir, ".pi", "agent");
370
- await ensureManagedContainerDirectory(join(piAgentDir, "skills"), join(paths.agentsDir, "skills"));
371
- await ensureManagedContainerDirectory(join(piAgentDir, "prompts"), join(paths.agentsDir, "commands"));
372
- await ensureManagedContainerDirectory(join(piAgentDir, "extensions"), join(paths.agentsDir, "extensions"));
373
- const results = [
374
- await ensureManagedLink(
375
- join(piAgentDir, "AGENTS.md"),
376
- join(paths.agentsDir, "AGENTS.md"),
377
- "file",
378
- previousLinkedTargets,
379
- options
380
- ),
381
- ...await linkSkillDirectories(piAgentDir, paths, previousLinkedTargets, options),
382
- ...await linkCommands(join(piAgentDir, "prompts"), paths, previousLinkedTargets, options)
383
- ];
384
- const extensionsRoot = join(paths.agentsDir, "extensions");
369
+ async function linkPiExtensionsInDirectory(targetDir, extensionsRoot, previousLinkedTargets, options) {
385
370
  const extensionNames = await getPiExtensionNames(extensionsRoot);
386
- results.push(...await pruneManagedTargets(
387
- join(piAgentDir, "extensions"),
371
+ const results = await pruneManagedTargets(
372
+ targetDir,
388
373
  extensionsRoot,
389
374
  extensionNames,
390
375
  previousLinkedTargets,
391
376
  options
392
- ));
377
+ );
393
378
  for (const entryName of extensionNames) {
394
379
  const sourcePath = join(extensionsRoot, entryName);
395
- results.push(await ensureManagedLink(
396
- join(piAgentDir, "extensions", entryName),
380
+ recordLinkResult(results, options, await ensureManagedLink(
381
+ join(targetDir, entryName),
397
382
  sourcePath,
398
383
  await pathIsDirectory(sourcePath) ? "dir" : "file",
399
384
  previousLinkedTargets,
@@ -403,14 +388,52 @@ async function linkPi(paths, previousLinkedTargets = {}, options = {}) {
403
388
  return results;
404
389
  }
405
390
 
391
+ async function linkPiExtensions(paths, previousLinkedTargets = {}, options = {}) {
392
+ const piAgentDir = await ensureProviderRootDirectory(paths.homeDir, [".pi", "agent"], options);
393
+ const targetDir = join(piAgentDir, "extensions");
394
+ await ensureManagedContainerDirectories([targetDir], options);
395
+ return linkPiExtensionsInDirectory(
396
+ targetDir,
397
+ join(paths.agentsDir, "extensions"),
398
+ previousLinkedTargets,
399
+ options
400
+ );
401
+ }
402
+
403
+ async function linkPi(paths, previousLinkedTargets = {}, options = {}) {
404
+ const piAgentDir = await ensureProviderRootDirectory(paths.homeDir, [".pi", "agent"], options);
405
+ await ensureManagedContainerDirectories([
406
+ join(piAgentDir, "extensions"),
407
+ join(piAgentDir, "skills"),
408
+ join(piAgentDir, "prompts")
409
+ ], options);
410
+ const results = [];
411
+ recordLinkResult(results, options, await ensureManagedLink(
412
+ join(piAgentDir, "AGENTS.md"),
413
+ join(paths.agentsDir, "AGENTS.md"),
414
+ "file",
415
+ previousLinkedTargets,
416
+ options
417
+ ));
418
+ results.push(...await linkSkillDirectories(piAgentDir, paths, previousLinkedTargets, options));
419
+ results.push(...await linkCommands(join(piAgentDir, "prompts"), paths, previousLinkedTargets, options));
420
+ results.push(...await linkPiExtensionsInDirectory(
421
+ join(piAgentDir, "extensions"),
422
+ join(paths.agentsDir, "extensions"),
423
+ previousLinkedTargets,
424
+ options
425
+ ));
426
+ return results;
427
+ }
428
+
406
429
  export {
407
430
  ensureManagedLink,
431
+ ensureProviderRootDirectory,
408
432
  getCommandNames,
409
433
  isWithinManagedRoot,
410
434
  linkClaude,
411
435
  linkCodex,
412
436
  linkPi,
413
- pathIsDirectory,
414
- pathIsFile,
437
+ linkPiExtensions,
415
438
  pruneManagedTargets
416
439
  };
@@ -0,0 +1,82 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+
5
+ async function readDeploymentRootIdentity(agentsDir) {
6
+ try {
7
+ const target = await fs.lstat(agentsDir);
8
+ if (target.isSymbolicLink() || !target.isDirectory()) {
9
+ throw new Error(`Deployment root conflict at ${agentsDir}`);
10
+ }
11
+ return {
12
+ canonicalPath: await fs.realpath(agentsDir),
13
+ dev: target.dev,
14
+ ino: target.ino
15
+ };
16
+ } catch (error) {
17
+ if (error?.code === "ENOENT") return null;
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ function sameIdentity(left, right) {
23
+ return Boolean(left && right)
24
+ && left.canonicalPath === right.canonicalPath
25
+ && left.dev === right.dev
26
+ && left.ino === right.ino;
27
+ }
28
+
29
+ async function assertDeploymentRootIdentity(agentsDir, expected) {
30
+ let current;
31
+ try {
32
+ current = await readDeploymentRootIdentity(agentsDir);
33
+ } catch (error) {
34
+ throw new Error(`Deployment root changed at ${agentsDir}`, { cause: error });
35
+ }
36
+ if (!sameIdentity(current, expected)) {
37
+ throw new Error(`Deployment root changed at ${agentsDir}`);
38
+ }
39
+ }
40
+
41
+ async function withDeploymentLock(paths, operation, options = {}) {
42
+ await fs.mkdir(dirname(paths.agentsDir), { recursive: true });
43
+ const beforeWait = await readDeploymentRootIdentity(paths.agentsDir);
44
+ const release = await lockfile.lock(paths.agentsDir, {
45
+ realpath: false,
46
+ lockfilePath: `${paths.agentsDir}.abelworkflow-operation.lock`,
47
+ retries: options.retries ?? {
48
+ retries: 100,
49
+ factor: 1,
50
+ minTimeout: 50,
51
+ maxTimeout: 50,
52
+ randomize: false
53
+ }
54
+ });
55
+ try {
56
+ let identity;
57
+ try {
58
+ identity = await readDeploymentRootIdentity(paths.agentsDir);
59
+ } catch (error) {
60
+ throw new Error(`Deployment root changed at ${paths.agentsDir}`, { cause: error });
61
+ }
62
+ if (beforeWait && !sameIdentity(identity, beforeWait)) {
63
+ throw new Error(`Deployment root changed at ${paths.agentsDir}`);
64
+ }
65
+ if (!identity) {
66
+ try {
67
+ await fs.mkdir(paths.agentsDir);
68
+ } catch (error) {
69
+ if (error?.code !== "EEXIST") throw error;
70
+ }
71
+ identity = await readDeploymentRootIdentity(paths.agentsDir);
72
+ }
73
+ if (!identity) throw new Error(`Deployment root changed at ${paths.agentsDir}`);
74
+ const guard = () => assertDeploymentRootIdentity(paths.agentsDir, identity);
75
+ await guard();
76
+ return await operation(guard);
77
+ } finally {
78
+ await release();
79
+ }
80
+ }
81
+
82
+ export { withDeploymentLock };
@@ -18,16 +18,6 @@ function objectField(value) {
18
18
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
19
19
  }
20
20
 
21
- function stringArray(value) {
22
- return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
23
- }
24
-
25
- function getPreviousManagedCodexAuthKeys(previousMetadata = {}) {
26
- return isMetadataV2(previousMetadata)
27
- ? stringArray(previousMetadata.managedCodexAuthKeys)
28
- : [];
29
- }
30
-
31
21
  function normalizeManagedCodexAgentFiles(value = {}) {
32
22
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
33
23
  return sortObject(Object.fromEntries(Object.entries(value).filter(([name, hash]) => (
@@ -41,30 +31,38 @@ function getPreviousManagedCodexAgentFiles(previousMetadata = {}) {
41
31
  }
42
32
 
43
33
  async function readInstallMetadata(paths) {
44
- const metadata = await readJsonFileSafe(metadataPathFor(paths), {});
34
+ const metadata = await readJsonFileSafe(metadataPathFor(paths), {}, { safeRoot: paths.agentsDir });
45
35
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
46
36
  throw new TypeError(`Install metadata in ${metadataPathFor(paths)} must contain a JSON object`);
47
37
  }
38
+ if (Object.keys(metadata).length > 0 && !isMetadataV2(metadata)) {
39
+ throw new Error(`Unsupported install metadata schema in ${metadataPathFor(paths)}`);
40
+ }
48
41
  return metadata;
49
42
  }
50
43
 
51
- async function writeInstallMetadata(paths, metadata) {
52
- return writeJson(metadataPathFor(paths), metadata, { backupLimit: 0 });
44
+ async function writeInstallMetadata(paths, metadata, options = {}) {
45
+ await options.guard?.();
46
+ return writeJson(metadataPathFor(paths), metadata, {
47
+ backupLimit: 0,
48
+ safeRoot: paths.agentsDir
49
+ });
53
50
  }
54
51
 
55
52
  function buildInstallMetadata({
56
53
  previousMetadata = {},
57
54
  packageVersion,
55
+ workflowInstalled = isMetadataV2(previousMetadata)
56
+ && previousMetadata.workflowInstalled === true,
58
57
  managedFiles = {},
59
- managedCodexAuthKeys = getPreviousManagedCodexAuthKeys(previousMetadata),
60
58
  managedCodexAgentFiles = getPreviousManagedCodexAgentFiles(previousMetadata),
61
59
  linkedTargets = {}
62
60
  }) {
63
61
  return {
64
62
  schemaVersion: 2,
65
63
  packageVersion,
64
+ ...(workflowInstalled ? { workflowInstalled: true } : {}),
66
65
  managedFiles: sortObject(managedFiles),
67
- managedCodexAuthKeys: [...new Set(managedCodexAuthKeys)].sort(),
68
66
  managedCodexAgentFiles: normalizeManagedCodexAgentFiles(managedCodexAgentFiles),
69
67
  linkedTargets: sortObject(linkedTargets)
70
68
  };
@@ -84,12 +82,9 @@ function finalizeProviderInstallMetadata({
84
82
  ? previousMetadata.packageVersion
85
83
  : packageVersion,
86
84
  managedFiles: isV2 ? objectField(previousMetadata.managedFiles) : {},
87
- managedCodexAuthKeys: stringArray(
88
- overrides.managedCodexAuthKeys ?? getPreviousManagedCodexAuthKeys(previousMetadata)
89
- ),
90
85
  managedCodexAgentFiles: overrides.managedCodexAgentFiles
91
86
  ?? getPreviousManagedCodexAgentFiles(previousMetadata),
92
- linkedTargets: objectField(previousMetadata.linkedTargets)
87
+ linkedTargets: isV2 ? objectField(previousMetadata.linkedTargets) : {}
93
88
  });
94
89
  }
95
90
 
@@ -135,10 +130,8 @@ export {
135
130
  createInstallReport,
136
131
  finalizeProviderInstallMetadata,
137
132
  getPreviousManagedCodexAgentFiles,
138
- getPreviousManagedCodexAuthKeys,
139
133
  mergeInstallReports,
140
134
  mergeLinkedTargets,
141
- metadataPathFor,
142
135
  readInstallMetadata,
143
136
  writeInstallMetadata
144
137
  };
package/lib/paths.mjs CHANGED
@@ -25,10 +25,8 @@ function createPaths({
25
25
  workflowTemplateRoot,
26
26
  installMetadataName: ".abelworkflow-install.json",
27
27
  claudeSettingsPath: join(absoluteHomeDir, ".claude", "settings.json"),
28
- claudeMetaConfigPath: join(absoluteHomeDir, ".claude.json"),
29
28
  codexConfigPath: join(absoluteHomeDir, ".codex", "config.toml"),
30
29
  codexAuthPath: join(absoluteHomeDir, ".codex", "auth.json"),
31
- codexTemplateConfigPath: join(codexTemplateRoot, "config-base.toml"),
32
30
  codexTemplateAgentsPath: join(codexTemplateRoot, "agents"),
33
31
  piAgentDir,
34
32
  piAuthPath: join(piAgentDir, "auth.json"),
@@ -48,7 +46,7 @@ function assertSeparateSourceAndDeployment(packageRoot, agentsDir) {
48
46
  throw new Error(
49
47
  `源码目录 packageRoot (${packageRoot}) 与部署目录 agentsDir (${agentsDir}) 不能相同或互相嵌套。`
50
48
  + "请将源码克隆到独立目录(例如 ~/src/AbelWorkflow),再用 --agents-dir 指向 ~/.agents;"
51
- + "请手动迁移已有源码克隆,安装器不会自动移动或删除数据,--force 也不能绕过此限制。"
49
+ + "请手动移动已有源码目录,安装器不会自动移动或删除数据,--force 也不能绕过此限制。"
52
50
  );
53
51
  }
54
52
 
@@ -78,16 +76,18 @@ function containsPath(parent, child, pathOps = { isAbsolute, relative, sep }) {
78
76
 
79
77
  const defaultPaths = createPaths();
80
78
 
81
- function getPlatform() {
82
- return process.env.ABELWORKFLOW_TEST_PLATFORM || process.platform;
79
+ function getPlatform(platform = process.platform) {
80
+ return platform;
83
81
  }
84
82
 
85
- function isWindows() {
86
- return getPlatform() === "win32";
83
+ function isWindows(platform = getPlatform()) {
84
+ return platform === "win32";
87
85
  }
88
86
 
89
- function pathToLabel(path, homeDir = defaultPaths.homeDir) {
90
- return path.replace(homeDir, "~");
87
+ function pathToLabel(path, homeDir = defaultPaths.homeDir, pathOps = { isAbsolute, relative, sep }) {
88
+ if (!containsPath(homeDir, path, pathOps)) return path;
89
+ const homeRelativePath = pathOps.relative(homeDir, path);
90
+ return homeRelativePath ? `~${pathOps.sep}${homeRelativePath}` : "~";
91
91
  }
92
92
 
93
93
  function maskSecret(value) {