abelworkflow 1.1.3 → 1.2.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.
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 -497
  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
@@ -2,7 +2,6 @@ import * as fs from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import {
4
4
  backupIfNeeded,
5
- pathExists,
6
5
  pathTargetExists,
7
6
  readJsonFileSafe,
8
7
  writeText
@@ -17,9 +16,13 @@ const ignoredSkillPathPatterns = [
17
16
  /(^|\/)__pycache__(\/|$)/u,
18
17
  /(^|\/)node_modules(\/|$)/u,
19
18
  /(^|\/)tmp(\/|$)/u,
20
- /(^|\/)build(\/|$)/u,
21
- /^dev-browser\/profiles(\/|$)/u,
22
- /^dev-browser\/tmp(\/|$)/u
19
+ /(^|\/)build(\/|$)/u
20
+ ];
21
+
22
+ const devBrowserRuntimePathPatterns = [
23
+ /^dev-browser\/SKILL\.md$/u,
24
+ /^dev-browser\/package[^/]*\.json$/u,
25
+ /^dev-browser\/(?:dist|references)(?:\/|$)/u
23
26
  ];
24
27
 
25
28
  function normalizeRelativePath(path) {
@@ -72,7 +75,11 @@ function mapGitignoreTemplate(relativePath) {
72
75
  function shouldCopySkillPath(skillsRoot, sourcePath) {
73
76
  const relativePath = normalizeRelativePath(relative(skillsRoot, sourcePath));
74
77
  if (!relativePath) return true;
75
- if (/(^|\/)dist(\/|$)/u.test(relativePath) && !/^dev-browser\/dist(\/|$)/u.test(relativePath)) return false;
78
+ if (relativePath === "dev-browser") return true;
79
+ if (relativePath.startsWith("dev-browser/")) {
80
+ return devBrowserRuntimePathPatterns.some((pattern) => pattern.test(relativePath));
81
+ }
82
+ if (/(^|\/)dist(\/|$)/u.test(relativePath)) return false;
76
83
  return !ignoredSkillPathPatterns.some((pattern) => pattern.test(relativePath));
77
84
  }
78
85
 
@@ -84,8 +91,7 @@ async function collectFiles(root, outputPrefix, { filter, mapTargetPath } = {})
84
91
  const sourcePath = join(directory, entry.name);
85
92
  if (filter && !filter(root, sourcePath)) continue;
86
93
  if (entry.isDirectory()) {
87
- const nested = await visit(sourcePath);
88
- for (const [key, value] of nested) assets.set(key, value);
94
+ await visit(sourcePath);
89
95
  continue;
90
96
  }
91
97
  if (!entry.isFile() && !entry.isSymbolicLink()) continue;
@@ -100,14 +106,12 @@ async function collectFiles(root, outputPrefix, { filter, mapTargetPath } = {})
100
106
  assets.set(targetRelativePath, {
101
107
  content: sourceContent,
102
108
  hash: hashBytes(sourceContent),
103
- sourceHash: hashBytes(sourceContent),
104
- mode: sourceStat.mode & 0o777,
105
- sourcePath
109
+ mode: sourceStat.mode & 0o777
106
110
  });
107
111
  }
108
- return assets;
109
112
  }
110
- return visit(root);
113
+ await visit(root);
114
+ return assets;
111
115
  }
112
116
 
113
117
  async function addAssetTree(manifest, root, outputPrefix, options = {}) {
@@ -131,9 +135,7 @@ async function collectManagedAssets(paths) {
131
135
  manifest.set("README.md", {
132
136
  content: readmeContent,
133
137
  hash: hashBytes(readmeContent),
134
- sourceHash: hashBytes(readmeContent),
135
- mode: (await fs.stat(readmePath)).mode & 0o777,
136
- sourcePath: readmePath
138
+ mode: (await fs.stat(readmePath)).mode & 0o777
137
139
  });
138
140
 
139
141
  const skillsRoot = join(paths.packageRoot, "skills");
@@ -171,23 +173,30 @@ async function readRegularTarget(targetPath) {
171
173
  }
172
174
  if (!targetStat.isFile()) return { exists: true, regular: false };
173
175
  const content = await fs.readFile(targetPath);
174
- return { exists: true, regular: true, hash: hashBytes(content) };
176
+ return {
177
+ exists: true,
178
+ regular: true,
179
+ hash: hashBytes(content),
180
+ mode: targetStat.mode & 0o777
181
+ };
175
182
  } catch (error) {
176
183
  if (error?.code === "ENOENT") return { exists: false, regular: false };
177
184
  throw error;
178
185
  }
179
186
  }
180
187
 
181
- async function applyManifestEntry(paths, relativePath, asset, previousHash, force, report) {
188
+ async function applyManifestEntry(paths, relativePath, asset, previousHash, force, report, beforeMutation) {
182
189
  const targetPath = await resolveManagedFilePath(paths, relativePath);
183
190
  const current = await readRegularTarget(targetPath);
184
191
  if (!current.exists) {
192
+ await beforeMutation();
185
193
  await writeText(targetPath, asset.content, { backupLimit: 3, mode: asset.mode });
186
194
  report.created.push(relativePath);
187
195
  return asset.hash;
188
196
  }
189
197
  if (!current.regular) {
190
198
  if (force && current.symbolicLink) {
199
+ await beforeMutation();
191
200
  await backupIfNeeded(targetPath);
192
201
  await writeText(targetPath, asset.content, { backupLimit: 3, mode: asset.mode });
193
202
  report.updated.push(relativePath);
@@ -197,16 +206,13 @@ async function applyManifestEntry(paths, relativePath, asset, previousHash, forc
197
206
  return previousHash;
198
207
  }
199
208
  if (current.hash === asset.hash) {
209
+ if (process.platform !== "win32" && current.mode !== asset.mode) await beforeMutation();
200
210
  const result = await writeText(targetPath, asset.content, { backupLimit: 3, mode: asset.mode });
201
211
  report[result.status === "permission-repaired" ? "updated" : "unchanged"].push(relativePath);
202
212
  return asset.hash;
203
213
  }
204
- if (!previousHash && current.hash === asset.sourceHash) {
205
- await writeText(targetPath, asset.content, { backupLimit: 3, mode: asset.mode });
206
- report.updated.push(relativePath);
207
- return asset.hash;
208
- }
209
214
  if (previousHash && current.hash === previousHash || force) {
215
+ await beforeMutation();
210
216
  await writeText(targetPath, asset.content, { backupLimit: 3, mode: asset.mode });
211
217
  report.updated.push(relativePath);
212
218
  return asset.hash;
@@ -215,7 +221,7 @@ async function applyManifestEntry(paths, relativePath, asset, previousHash, forc
215
221
  return previousHash;
216
222
  }
217
223
 
218
- async function removeStaleManagedFile(paths, relativePath, previousHash, force, report) {
224
+ async function removeStaleManagedFile(paths, relativePath, previousHash, force, report, beforeMutation) {
219
225
  if (relativePath.endsWith(".skill-lock.json")) return;
220
226
  const targetPath = await resolveManagedFilePath(paths, relativePath);
221
227
  const current = await readRegularTarget(targetPath);
@@ -224,12 +230,14 @@ async function removeStaleManagedFile(paths, relativePath, previousHash, force,
224
230
  recordConflict(report, relativePath);
225
231
  return undefined;
226
232
  }
227
- if (current.regular && current.hash === previousHash) {
233
+ if (current.hash === previousHash) {
234
+ await beforeMutation();
228
235
  await fs.rm(targetPath, { force: true });
229
236
  report.removed.push(relativePath);
230
237
  return undefined;
231
238
  }
232
239
  if (force) {
240
+ await beforeMutation();
233
241
  await backupIfNeeded(targetPath);
234
242
  report.removed.push(relativePath);
235
243
  return undefined;
@@ -250,14 +258,21 @@ async function readPackageVersion(paths) {
250
258
  async function syncManagedFiles({
251
259
  paths,
252
260
  force = false,
253
- pathPrefix = null
261
+ pathPrefix = null,
262
+ beforeMutation = async () => {}
254
263
  }) {
264
+ const packageVersion = await readPackageVersion(paths);
255
265
  const previousMetadata = await readInstallMetadata(paths);
256
266
  const previousManagedFiles = await readManagedFiles(paths, previousMetadata);
257
267
  const manifest = await collectManagedAssets(paths);
258
268
  const report = createInstallReport();
259
269
  const managedFiles = pathPrefix ? { ...previousManagedFiles } : {};
260
270
 
271
+ for (const [relativePath] of manifest) {
272
+ if (!pathPrefix || relativePath.startsWith(pathPrefix)) {
273
+ await resolveManagedFilePath(paths, relativePath);
274
+ }
275
+ }
261
276
  for (const [relativePath, asset] of manifest) {
262
277
  if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
263
278
  const managedHash = await applyManifestEntry(
@@ -266,7 +281,8 @@ async function syncManagedFiles({
266
281
  asset,
267
282
  previousManagedFiles[relativePath],
268
283
  force,
269
- report
284
+ report,
285
+ beforeMutation
270
286
  );
271
287
  if (managedHash) managedFiles[relativePath] = managedHash;
272
288
  else delete managedFiles[relativePath];
@@ -274,7 +290,14 @@ async function syncManagedFiles({
274
290
  for (const [relativePath, previousHash] of Object.entries(previousManagedFiles)) {
275
291
  if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
276
292
  if (!manifest.has(relativePath)) {
277
- const managedHash = await removeStaleManagedFile(paths, relativePath, previousHash, force, report);
293
+ const managedHash = await removeStaleManagedFile(
294
+ paths,
295
+ relativePath,
296
+ previousHash,
297
+ force,
298
+ report,
299
+ beforeMutation
300
+ );
278
301
  if (managedHash) managedFiles[relativePath] = managedHash;
279
302
  else delete managedFiles[relativePath];
280
303
  }
@@ -282,39 +305,38 @@ async function syncManagedFiles({
282
305
 
283
306
  return {
284
307
  previousMetadata,
285
- packageVersion: await readPackageVersion(paths),
308
+ packageVersion,
286
309
  managedFiles,
287
310
  report
288
311
  };
289
312
  }
290
313
 
291
- async function pathsReferToSameEntry(sourcePath, targetPath) {
292
- if (resolve(sourcePath) === resolve(targetPath)) return true;
314
+ async function assertManagedContainerDirectory(targetPath) {
293
315
  try {
294
- return await fs.realpath(sourcePath) === await fs.realpath(targetPath);
295
- } catch {
316
+ const targetStat = await fs.lstat(targetPath);
317
+ if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
318
+ throw new Error(`Managed container conflict at ${targetPath}`);
319
+ }
320
+ return true;
321
+ } catch (error) {
322
+ if (error?.code !== "ENOENT") throw error;
296
323
  return false;
297
324
  }
298
325
  }
299
326
 
300
- async function ensureManagedContainerDirectory(targetPath, sourcePath) {
301
- if (await pathExists(targetPath)) {
302
- const targetStat = await fs.lstat(targetPath);
303
- if (targetStat.isSymbolicLink() && await pathsReferToSameEntry(targetPath, sourcePath)) {
304
- await fs.unlink(targetPath);
305
- } else if (!targetStat.isDirectory()) {
306
- throw new Error(`Managed container conflict at ${targetPath}`);
307
- }
327
+ async function ensureManagedContainerDirectories(targetPaths, options = {}) {
328
+ const existing = await Promise.all(targetPaths.map(assertManagedContainerDirectory));
329
+ for (let index = 0; index < targetPaths.length; index += 1) {
330
+ const targetPath = targetPaths[index];
331
+ if (!existing[index]) await options.beforeMutation?.();
332
+ await fs.mkdir(targetPath, { recursive: true });
333
+ await assertManagedContainerDirectory(targetPath);
308
334
  }
309
- await fs.mkdir(targetPath, { recursive: true });
310
335
  }
311
336
 
312
337
  export {
313
- collectManagedAssets,
314
- ensureManagedContainerDirectory,
315
- pathsReferToSameEntry,
338
+ ensureManagedContainerDirectories,
316
339
  readManagedFiles,
317
340
  readPackageVersion,
318
- shouldCopySkillPath,
319
341
  syncManagedFiles
320
342
  };
@@ -1,7 +1,14 @@
1
- import { pathExists } from "../config/store.mjs";
1
+ import { stat } from "node:fs/promises";
2
+ import { join } from "node:path";
2
3
  import { createPaths } from "../paths.mjs";
3
- import { readManagedFiles, readPackageVersion, syncManagedFiles } from "./assets.mjs";
4
- import { linkClaude, linkCodex, linkPi } from "./links.mjs";
4
+ import { readPackageVersion, syncManagedFiles } from "./assets.mjs";
5
+ import {
6
+ linkClaude,
7
+ linkCodex,
8
+ linkPi,
9
+ linkPiExtensions
10
+ } from "./links.mjs";
11
+ import { withDeploymentLock } from "./lock.mjs";
5
12
  import {
6
13
  buildInstallMetadata,
7
14
  createInstallReport,
@@ -29,7 +36,7 @@ function validatedPaths(paths) {
29
36
  function reportFromLinkResults(results) {
30
37
  const report = createInstallReport();
31
38
  for (const result of results) {
32
- if (result.status === "linked" || result.status === "copied") report.linked.push(result.targetPath);
39
+ if (result.status === "linked") report.linked.push(result.targetPath);
33
40
  else if (result.status === "unchanged") report.unchanged.push(result.targetPath);
34
41
  else if (result.status === "removed") report.removed.push(result.targetPath);
35
42
  else if (result.status === "conflict") {
@@ -51,78 +58,228 @@ async function packageVersionFor(paths, previousMetadata) {
51
58
  return readPackageVersion(paths);
52
59
  }
53
60
 
54
- async function writeInstallState(paths, assetResult, linkedTargets) {
61
+ async function writeInstallState(paths, assetResult, linkedTargets, workflowInstalled, guard) {
55
62
  const metadata = buildInstallMetadata({
56
63
  previousMetadata: assetResult.previousMetadata,
57
64
  packageVersion: assetResult.packageVersion,
65
+ workflowInstalled,
58
66
  managedFiles: assetResult.managedFiles,
59
67
  linkedTargets
60
68
  });
61
- await writeInstallMetadata(paths, metadata);
69
+ await writeInstallMetadata(paths, metadata, { guard });
62
70
  }
63
71
 
64
- async function installWorkflow(options) {
65
- const paths = validatedPaths(options.paths);
72
+ function onceAsync(operation) {
73
+ let result;
74
+ return () => {
75
+ result ??= operation();
76
+ return result;
77
+ };
78
+ }
79
+
80
+ async function writeIncompleteInstallState(paths, previousMetadata, guard) {
81
+ const packageVersion = await packageVersionFor(paths, previousMetadata);
82
+ await writeInstallMetadata(paths, buildInstallMetadata({
83
+ previousMetadata,
84
+ packageVersion,
85
+ workflowInstalled: false,
86
+ managedFiles: previousMetadata.managedFiles ?? {},
87
+ linkedTargets: previousMetadata.linkedTargets ?? {}
88
+ }), { guard });
89
+ }
90
+
91
+ function validatedSkillName(skillName) {
92
+ if (typeof skillName !== "string"
93
+ || !skillName
94
+ || skillName === "."
95
+ || skillName === ".."
96
+ || skillName.includes("/")
97
+ || skillName.includes("\\")
98
+ || skillName.includes("\0")) {
99
+ throw new TypeError(`Invalid skill name: ${String(skillName)}`);
100
+ }
101
+ return skillName;
102
+ }
103
+
104
+ async function assertRequiredSkill(paths, skillName) {
105
+ const skillPath = join(paths.packageRoot, "skills", skillName, "SKILL.md");
106
+ try {
107
+ if ((await stat(skillPath)).isFile()) return;
108
+ } catch (error) {
109
+ if (error?.code !== "ENOENT") throw error;
110
+ }
111
+ throw new Error(`Missing required skill: ${skillPath}`);
112
+ }
113
+
114
+ async function installWorkflowLocked(paths, options, guard) {
115
+ await guard();
66
116
  const previousMetadata = await readInstallMetadata(paths);
117
+ const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
118
+ const markIncomplete = onceAsync(() => writeIncompleteInstallState(paths, previousMetadata, guard));
119
+ const beforeMutation = async () => {
120
+ await guard();
121
+ await markIncomplete();
122
+ };
67
123
  let assetResult;
68
-
69
- if (options.relinkOnly) {
70
- if (!(await pathExists(paths.agentsDir))) {
71
- throw new Error(`${paths.agentsDir} does not exist; remove --link-only or install first`);
72
- }
73
- assetResult = {
74
- previousMetadata,
75
- packageVersion: await packageVersionFor(paths, previousMetadata),
76
- managedFiles: await readManagedFiles(paths, previousMetadata),
77
- report: createInstallReport()
78
- };
79
- } else {
124
+ try {
80
125
  assetResult = await syncManagedFiles({
81
126
  paths,
82
- force: options.force ?? false
127
+ force: options.force ?? false,
128
+ beforeMutation
83
129
  });
130
+ } catch (error) {
131
+ await markIncomplete();
132
+ throw error;
84
133
  }
85
134
 
86
- const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
87
- const linkOptions = { force: options.force ?? false };
88
- const claudeResults = await linkClaude(paths, previousLinkedTargets, linkOptions);
89
- const codexResults = await linkCodex(paths, previousLinkedTargets, linkOptions);
90
- const piResults = await linkPi(paths, previousLinkedTargets, linkOptions);
91
- const linkResults = [...claudeResults, ...codexResults, ...piResults];
92
- await writeInstallState(paths, assetResult, mergeLinkedTargets(previousLinkedTargets, linkResults));
93
- return mergeInstallReports(assetResult.report, reportFromLinkResults(linkResults));
135
+ const linkResults = [];
136
+ const linkOptions = {
137
+ force: options.force ?? false,
138
+ beforeMutation,
139
+ onResult: (result) => linkResults.push(result)
140
+ };
141
+ try {
142
+ await linkClaude(paths, previousLinkedTargets, linkOptions);
143
+ await linkCodex(paths, previousLinkedTargets, linkOptions);
144
+ await linkPi(paths, previousLinkedTargets, linkOptions);
145
+ } catch (error) {
146
+ await writeInstallState(
147
+ paths,
148
+ assetResult,
149
+ mergeLinkedTargets(previousLinkedTargets, linkResults),
150
+ false,
151
+ guard
152
+ );
153
+ throw error;
154
+ }
155
+ const report = mergeInstallReports(assetResult.report, reportFromLinkResults(linkResults));
156
+ await writeInstallState(
157
+ paths,
158
+ assetResult,
159
+ mergeLinkedTargets(previousLinkedTargets, linkResults),
160
+ report.conflicts.length === 0,
161
+ guard
162
+ );
163
+ return report;
94
164
  }
95
165
 
96
- async function installManagedWorkflow(options = {}) {
97
- return installWorkflow({ ...options, paths: validatedPaths(options.paths) });
166
+ async function installWorkflow(options) {
167
+ const paths = validatedPaths(options.paths);
168
+ return withDeploymentLock(paths, (guard) => installWorkflowLocked(paths, options, guard));
98
169
  }
99
170
 
100
- async function ensureSkillPresent(inputPaths, skillName) {
101
- const paths = validatedPaths(inputPaths);
171
+ async function ensureSkillPresentLocked(paths, validatedName, guard) {
172
+ await guard();
102
173
  const previousMetadata = await readInstallMetadata(paths);
103
- const assetResult = await syncManagedFiles({
174
+ const markIncomplete = onceAsync(() => writeIncompleteInstallState(paths, previousMetadata, guard));
175
+ const beforeMutation = async () => {
176
+ await guard();
177
+ await markIncomplete();
178
+ };
179
+ let assetResult;
180
+ try {
181
+ assetResult = await syncManagedFiles({
182
+ paths,
183
+ force: false,
184
+ pathPrefix: `skills/${validatedName}/`,
185
+ beforeMutation
186
+ });
187
+ } catch (error) {
188
+ await markIncomplete();
189
+ throw error;
190
+ }
191
+ await writeInstallState(
104
192
  paths,
105
- force: false,
106
- pathPrefix: `skills/${skillName}/`
107
- });
108
- await writeInstallState(paths, assetResult, previousMetadata.linkedTargets ?? {});
193
+ assetResult,
194
+ previousMetadata.linkedTargets ?? {},
195
+ previousMetadata.workflowInstalled === true
196
+ && previousMetadata.packageVersion === assetResult.packageVersion
197
+ && assetResult.report.conflicts.length === 0,
198
+ guard
199
+ );
109
200
  return assetResult.report;
110
201
  }
111
202
 
112
- async function ensurePiResourcesLinked(inputPaths) {
203
+ async function ensureSkillPresent(inputPaths, skillName, continuation) {
204
+ if (continuation !== undefined && typeof continuation !== "function") {
205
+ throw new TypeError("Expected a skill continuation function");
206
+ }
113
207
  const paths = validatedPaths(inputPaths);
208
+ const validatedName = validatedSkillName(skillName);
209
+ await assertRequiredSkill(paths, validatedName);
210
+ return withDeploymentLock(paths, async (guard) => {
211
+ const report = await ensureSkillPresentLocked(paths, validatedName, guard);
212
+ return continuation ? continuation(report, guard) : report;
213
+ });
214
+ }
215
+
216
+ async function ensurePiExtensionsPresentLocked(paths, guard) {
217
+ await guard();
114
218
  const previousMetadata = await readInstallMetadata(paths);
115
- const assetResult = await syncManagedFiles({ paths, force: false });
116
219
  const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
117
- const piResults = await linkPi(paths, previousLinkedTargets, { force: false });
118
- await writeInstallState(paths, assetResult, mergeLinkedTargets(previousLinkedTargets, piResults));
119
- return mergeInstallReports(assetResult.report, reportFromLinkResults(piResults));
220
+ const markIncomplete = onceAsync(() => writeIncompleteInstallState(paths, previousMetadata, guard));
221
+ const beforeMutation = async () => {
222
+ await guard();
223
+ await markIncomplete();
224
+ };
225
+ let assetResult;
226
+ try {
227
+ assetResult = await syncManagedFiles({
228
+ paths,
229
+ force: false,
230
+ pathPrefix: "extensions/",
231
+ beforeMutation
232
+ });
233
+ } catch (error) {
234
+ await markIncomplete();
235
+ throw error;
236
+ }
237
+
238
+ const linkResults = [];
239
+ try {
240
+ await linkPiExtensions(paths, previousLinkedTargets, {
241
+ force: false,
242
+ beforeMutation,
243
+ onResult: (result) => linkResults.push(result)
244
+ });
245
+ } catch (error) {
246
+ await writeInstallState(
247
+ paths,
248
+ assetResult,
249
+ mergeLinkedTargets(previousLinkedTargets, linkResults),
250
+ false,
251
+ guard
252
+ );
253
+ throw error;
254
+ }
255
+
256
+ const report = mergeInstallReports(assetResult.report, reportFromLinkResults(linkResults));
257
+ await writeInstallState(
258
+ paths,
259
+ assetResult,
260
+ mergeLinkedTargets(previousLinkedTargets, linkResults),
261
+ previousMetadata.workflowInstalled === true
262
+ && previousMetadata.packageVersion === assetResult.packageVersion
263
+ && report.conflicts.length === 0,
264
+ guard
265
+ );
266
+ return report;
267
+ }
268
+
269
+ async function ensurePiExtensionsPresent(inputPaths, continuation) {
270
+ if (continuation !== undefined && typeof continuation !== "function") {
271
+ throw new TypeError("Expected an Extensions continuation function");
272
+ }
273
+ const paths = validatedPaths(inputPaths);
274
+ return withDeploymentLock(paths, async (guard) => {
275
+ const report = await ensurePiExtensionsPresentLocked(paths, guard);
276
+ return continuation ? continuation(report, guard) : report;
277
+ });
120
278
  }
121
279
 
122
280
  export {
123
- ensurePiResourcesLinked,
281
+ ensurePiExtensionsPresent,
124
282
  ensureSkillPresent,
125
- installManagedWorkflow,
126
283
  installWorkflow,
127
284
  packageVersionFor
128
285
  };