@agentxm/extension-lifecycle 0.28.4-bootstrap.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/LICENSE +110 -0
- package/dist/src/configured-entry-resolution.d.ts +110 -0
- package/dist/src/configured-entry-resolution.js +723 -0
- package/dist/src/errors.d.ts +39 -0
- package/dist/src/errors.js +45 -0
- package/dist/src/failure-adapter.d.ts +35 -0
- package/dist/src/failure-adapter.js +23 -0
- package/dist/src/hooks/configured-agent-outcomes-provider.d.ts +18 -0
- package/dist/src/hooks/configured-agent-outcomes-provider.js +39 -0
- package/dist/src/hooks/manager.d.ts +15 -0
- package/dist/src/hooks/manager.js +751 -0
- package/dist/src/index.d.ts +38 -0
- package/dist/src/index.js +41 -0
- package/dist/src/internal/fs-helpers.d.ts +12 -0
- package/dist/src/internal/fs-helpers.js +13 -0
- package/dist/src/internal/integrity.d.ts +13 -0
- package/dist/src/internal/integrity.js +17 -0
- package/dist/src/knowledge/manager.d.ts +11 -0
- package/dist/src/knowledge/manager.js +655 -0
- package/dist/src/live.d.ts +18 -0
- package/dist/src/live.js +18 -0
- package/dist/src/mcps/manager.d.ts +16 -0
- package/dist/src/mcps/manager.js +213 -0
- package/dist/src/mcps/operations/artifact.d.ts +24 -0
- package/dist/src/mcps/operations/artifact.js +74 -0
- package/dist/src/mcps/operations/disable.d.ts +18 -0
- package/dist/src/mcps/operations/disable.js +113 -0
- package/dist/src/mcps/operations/enable.d.ts +18 -0
- package/dist/src/mcps/operations/enable.js +218 -0
- package/dist/src/mcps/operations/install.d.ts +59 -0
- package/dist/src/mcps/operations/install.js +566 -0
- package/dist/src/mcps/operations/sync-outcome.d.ts +10 -0
- package/dist/src/mcps/operations/sync-outcome.js +27 -0
- package/dist/src/mcps/operations/uninstall.d.ts +41 -0
- package/dist/src/mcps/operations/uninstall.js +189 -0
- package/dist/src/packs/dependency-resolution.d.ts +65 -0
- package/dist/src/packs/dependency-resolution.js +414 -0
- package/dist/src/packs/expansion.d.ts +59 -0
- package/dist/src/packs/expansion.js +44 -0
- package/dist/src/packs/manager.d.ts +16 -0
- package/dist/src/packs/manager.js +181 -0
- package/dist/src/packs/operations/install.d.ts +67 -0
- package/dist/src/packs/operations/install.js +156 -0
- package/dist/src/packs/resolved-dependency.d.ts +29 -0
- package/dist/src/packs/resolved-dependency.js +28 -0
- package/dist/src/registry-materialization.d.ts +52 -0
- package/dist/src/registry-materialization.js +67 -0
- package/dist/src/resolution-progress.d.ts +18 -0
- package/dist/src/resolution-progress.js +11 -0
- package/dist/src/resolution-timeout.d.ts +5 -0
- package/dist/src/resolution-timeout.js +11 -0
- package/dist/src/rules/manager.d.ts +20 -0
- package/dist/src/rules/manager.js +475 -0
- package/dist/src/skills/manager.d.ts +18 -0
- package/dist/src/skills/manager.js +346 -0
- package/dist/src/skills/materialization.d.ts +54 -0
- package/dist/src/skills/materialization.js +158 -0
- package/dist/src/skills/operations/disable.d.ts +31 -0
- package/dist/src/skills/operations/disable.js +110 -0
- package/dist/src/skills/operations/enable.d.ts +31 -0
- package/dist/src/skills/operations/enable.js +113 -0
- package/dist/src/skills/operations/install-result.d.ts +33 -0
- package/dist/src/skills/operations/install-result.js +10 -0
- package/dist/src/skills/operations/install.d.ts +67 -0
- package/dist/src/skills/operations/install.js +590 -0
- package/dist/src/skills/operations/source-hash.d.ts +2 -0
- package/dist/src/skills/operations/source-hash.js +6 -0
- package/dist/src/skills/operations/uninstall.d.ts +40 -0
- package/dist/src/skills/operations/uninstall.js +157 -0
- package/dist/src/skills/utils.d.ts +11 -0
- package/dist/src/skills/utils.js +29 -0
- package/dist/src/subagents/manager.d.ts +18 -0
- package/dist/src/subagents/manager.js +677 -0
- package/dist/src/subagents/operations/artifact.d.ts +18 -0
- package/dist/src/subagents/operations/artifact.js +43 -0
- package/dist/src/subagents/operations/disable.d.ts +32 -0
- package/dist/src/subagents/operations/disable.js +116 -0
- package/dist/src/subagents/operations/enable.d.ts +30 -0
- package/dist/src/subagents/operations/enable.js +160 -0
- package/dist/src/workflows/install-command/workflow.d.ts +57 -0
- package/dist/src/workflows/install-command/workflow.js +41 -0
- package/dist/src/workflows/uninstall-command/workflow.d.ts +42 -0
- package/dist/src/workflows/uninstall-command/workflow.js +29 -0
- package/package.json +62 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install skill executor — orchestrates the full per-skill installation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to a per-refType install function via `switch(ref.refType)`, then
|
|
5
|
+
* runs shared post-install steps (agent symlinks, lockfile/settings writes).
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
*/
|
|
9
|
+
import * as FileSystem from "effect/FileSystem";
|
|
10
|
+
import * as Path from "effect/Path";
|
|
11
|
+
import * as Array from "effect/Array";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import { printSourceParams } from "@agentxm/extension-model/unstable/sources/printer";
|
|
15
|
+
import { printSkillLockSourceLocator } from "@agentxm/workspace-state";
|
|
16
|
+
import { sourceToLockEntry } from "@agentxm/workspace-state";
|
|
17
|
+
import { canReuseInstalledPackage, materializeExternalPackage } from "@agentxm/extension-workspace";
|
|
18
|
+
import { materializeRegistryPackage } from "../../registry-materialization.js";
|
|
19
|
+
import { computePackageContentHash } from "@agentxm/workspace-state";
|
|
20
|
+
import { RenderedFilePathSchema, } from "@agentxm/workspace-state";
|
|
21
|
+
import { computeMaterializedTreeIntegrity } from "@agentxm/workspace-state";
|
|
22
|
+
import * as Schema from "effect/Schema";
|
|
23
|
+
import { SourceHostProviders } from "@agentxm/extension-sources";
|
|
24
|
+
import { CodingAgentRepository, validateAxmSkillCandidate } from "@agentxm/extension-workspace";
|
|
25
|
+
import { stripFileProtocol } from "../../internal/fs-helpers.js";
|
|
26
|
+
import { isPathSafe } from "@agentxm/workspace-state";
|
|
27
|
+
import { makeWorkspaceRelativeSourcePath } from "@agentxm/extension-model/unstable/path-types";
|
|
28
|
+
import { createSymlink } from "@agentxm/workspace-state";
|
|
29
|
+
import { validatePathSafety } from "@agentxm/workspace-state";
|
|
30
|
+
import { StepFailure } from "@agentxm/workspace-operations";
|
|
31
|
+
import { acceptedRegistryVersionForRef, validateExactResolvedVersion, } from "@agentxm/workspace-state";
|
|
32
|
+
import { ExtensionLifecycleFailed } from "../../errors.js";
|
|
33
|
+
import { LifecycleFailureAdapter, withAdaptedStepFailures } from "../../failure-adapter.js";
|
|
34
|
+
import { appendWarningsToMessage } from "@agentxm/workspace-operations";
|
|
35
|
+
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
36
|
+
import { copyExtensionDirectory, formatCopyExtensionDirectoryFailure, } from "@agentxm/extension-workspace";
|
|
37
|
+
import { artifactAgentIdsFromTargets, artifactTargetAgentIds, groupInstallTargetsByDirectory, } from "@agentxm/extension-workspace";
|
|
38
|
+
import { sanitizeName } from "@agentxm/workspace-state";
|
|
39
|
+
import { computeSkillSourceHash } from "./source-hash.js";
|
|
40
|
+
const countFiles = (dir) => Effect.gen(function* () {
|
|
41
|
+
const fs = yield* FileSystem.FileSystem;
|
|
42
|
+
const path = yield* Path.Path;
|
|
43
|
+
const entries = yield* fs.readDirectory(dir).pipe(Effect.catch(() => Effect.succeed([])));
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const fullPath = path.join(dir, entry);
|
|
47
|
+
const statOption = yield* fs.stat(fullPath).pipe(Effect.option);
|
|
48
|
+
if (Option.isNone(statOption))
|
|
49
|
+
continue;
|
|
50
|
+
if (statOption.value.type === "Directory") {
|
|
51
|
+
total += yield* countFiles(fullPath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
total += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return total;
|
|
58
|
+
});
|
|
59
|
+
const expectedSkillSrcPath = (ref) => Effect.gen(function* () {
|
|
60
|
+
const ws = yield* WorkspaceMutations;
|
|
61
|
+
switch (ref.refType) {
|
|
62
|
+
case "registry": {
|
|
63
|
+
const { skillSrcPath } = yield* ws.getSkillDir(ref.skill.name, ref);
|
|
64
|
+
return skillSrcPath;
|
|
65
|
+
}
|
|
66
|
+
case "workspace": {
|
|
67
|
+
const { skillSrcPath } = yield* ws.getSkillDir(ref.skill.name, ref);
|
|
68
|
+
return skillSrcPath;
|
|
69
|
+
}
|
|
70
|
+
case "git-hosted":
|
|
71
|
+
case "local": {
|
|
72
|
+
const { skillSrcPath } = yield* ws.getSkillDir(ref.skill.name, ref);
|
|
73
|
+
return skillSrcPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const existingSourceHash = (ref) => Effect.gen(function* () {
|
|
78
|
+
const fs = yield* FileSystem.FileSystem;
|
|
79
|
+
const previousPath = yield* expectedSkillSrcPath(ref);
|
|
80
|
+
const exists = yield* fs.exists(previousPath).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
81
|
+
if (!exists)
|
|
82
|
+
return undefined;
|
|
83
|
+
return yield* computeSkillSourceHash(previousPath);
|
|
84
|
+
});
|
|
85
|
+
const symlinkResultToChange = (result) => {
|
|
86
|
+
switch (result) {
|
|
87
|
+
case "created":
|
|
88
|
+
return "created";
|
|
89
|
+
case "replaced":
|
|
90
|
+
return "updated";
|
|
91
|
+
case "no-op":
|
|
92
|
+
case "skipped":
|
|
93
|
+
return "unchanged";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const artifactChangeFromTargets = (fallback, targets) => {
|
|
97
|
+
if (targets.length === 0)
|
|
98
|
+
return fallback;
|
|
99
|
+
if (targets.some((target) => target.change === "created"))
|
|
100
|
+
return "created";
|
|
101
|
+
if (targets.some((target) => target.change === "updated" || target.change === undefined)) {
|
|
102
|
+
return "updated";
|
|
103
|
+
}
|
|
104
|
+
return fallback === "updated" ? "updated" : "unchanged";
|
|
105
|
+
};
|
|
106
|
+
const gitHostedSourceOrigin = (ref) => {
|
|
107
|
+
const source = ref.source;
|
|
108
|
+
switch (source.type) {
|
|
109
|
+
case "github":
|
|
110
|
+
case "gitlab":
|
|
111
|
+
case "bitbucket":
|
|
112
|
+
return `${source.url.origin}/${source.owner}/${source.repo}`;
|
|
113
|
+
case "azurerepos":
|
|
114
|
+
return `${source.url.origin}/${source.organization}/${source.project}/_git/${source.repo}`;
|
|
115
|
+
case "git":
|
|
116
|
+
return source.url.href;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
export const gitHostedSkillArtifactSource = (ref) => {
|
|
120
|
+
if (ref.refType !== "git-hosted")
|
|
121
|
+
return undefined;
|
|
122
|
+
const gitTreeHash = ref.gitTreeSha;
|
|
123
|
+
const gitRef = Option.getOrUndefined(ref.source.ref);
|
|
124
|
+
const directory = ref.sourcePath === undefined || ref.sourcePath.length === 0 ? "." : ref.sourcePath;
|
|
125
|
+
return {
|
|
126
|
+
type: ref.source.type,
|
|
127
|
+
origin: gitHostedSourceOrigin(ref),
|
|
128
|
+
...(gitRef !== undefined ? { ref: gitRef } : {}),
|
|
129
|
+
directory,
|
|
130
|
+
gitTreeHash,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
// -----------------------------------------------------------------------------
|
|
134
|
+
// Shared helpers
|
|
135
|
+
// -----------------------------------------------------------------------------
|
|
136
|
+
const preCleanAndCopy = (sanitizedName, sourcePath, copyTarget) => Effect.gen(function* () {
|
|
137
|
+
const adapter = yield* LifecycleFailureAdapter;
|
|
138
|
+
const fs = yield* FileSystem.FileSystem;
|
|
139
|
+
const ws = yield* WorkspaceMutations;
|
|
140
|
+
const sourceExists = yield* fs
|
|
141
|
+
.exists(sourcePath)
|
|
142
|
+
.pipe(Effect.catch(() => Effect.succeed(false)));
|
|
143
|
+
if (!sourceExists) {
|
|
144
|
+
return yield* new ExtensionLifecycleFailed({
|
|
145
|
+
category: "validation",
|
|
146
|
+
detail: formatCopyExtensionDirectoryFailure({
|
|
147
|
+
sourcePath,
|
|
148
|
+
targetPath: copyTarget,
|
|
149
|
+
subject: "skill files",
|
|
150
|
+
sourceExists,
|
|
151
|
+
}),
|
|
152
|
+
suggestions: [{ description: "Check the extension package and try again." }],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
yield* materializeExternalPackage({
|
|
156
|
+
baseDir: ws.baseDir,
|
|
157
|
+
canonicalPath: copyTarget,
|
|
158
|
+
sourceLocation: sourcePath,
|
|
159
|
+
copyFailureCode: "internal",
|
|
160
|
+
copyFailureDetail: () => formatCopyExtensionDirectoryFailure({
|
|
161
|
+
sourcePath,
|
|
162
|
+
targetPath: copyTarget,
|
|
163
|
+
subject: "skill files",
|
|
164
|
+
sourceExists,
|
|
165
|
+
}),
|
|
166
|
+
}).pipe(Effect.mapError((error) => new ExtensionLifecycleFailed({
|
|
167
|
+
category: "validation",
|
|
168
|
+
detail: adapter.describeFailure(error),
|
|
169
|
+
suggestions: [{ description: "Check the extension package and try again." }],
|
|
170
|
+
cause: error,
|
|
171
|
+
})));
|
|
172
|
+
});
|
|
173
|
+
const decodeRenderedFilePath = Schema.decodeUnknownSync(RenderedFilePathSchema);
|
|
174
|
+
export { computeSkillSourceHash } from "./source-hash.js";
|
|
175
|
+
/**
|
|
176
|
+
* Build a RenderedFilesMap from per-agent copy-mode install results.
|
|
177
|
+
* Only includes agents where mode === "copy" and success === true.
|
|
178
|
+
*
|
|
179
|
+
* @internal Exported for testing only.
|
|
180
|
+
*/
|
|
181
|
+
export const buildRenderedFilesFromResults = (installableTargets, agentResults, toWorkspaceRelativePath) => {
|
|
182
|
+
const result = {};
|
|
183
|
+
for (const [target, installResult] of Array.zip(installableTargets, agentResults)) {
|
|
184
|
+
if (installResult.mode === "copy" && installResult.success) {
|
|
185
|
+
result[target.agentId] = [
|
|
186
|
+
{ path: decodeRenderedFilePath(toWorkspaceRelativePath(installResult.path)) },
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
// -----------------------------------------------------------------------------
|
|
193
|
+
// Per-refType install functions
|
|
194
|
+
// -----------------------------------------------------------------------------
|
|
195
|
+
const installFromGitHosted = (ref, sanitizedName) => Effect.gen(function* () {
|
|
196
|
+
const path = yield* Path.Path;
|
|
197
|
+
const ws = yield* WorkspaceMutations;
|
|
198
|
+
const { canonicalPath, skillSrcPath } = yield* ws.getSkillDir(ref.skill.name, ref);
|
|
199
|
+
yield* validatePathSafety(path, ws.baseDir, canonicalPath);
|
|
200
|
+
const packageRoot = stripFileProtocol(ref.location);
|
|
201
|
+
const sourceSkillPath = ref.portable === true ? packageRoot : path.join(packageRoot, "src");
|
|
202
|
+
yield* validateAxmSkillCandidate({
|
|
203
|
+
ref,
|
|
204
|
+
packageRoot,
|
|
205
|
+
skillSourcePath: sourceSkillPath,
|
|
206
|
+
});
|
|
207
|
+
yield* preCleanAndCopy(sanitizedName, packageRoot, canonicalPath);
|
|
208
|
+
return { skillSrcPath, versionRange: Option.none() };
|
|
209
|
+
});
|
|
210
|
+
const installFromLocal = (ref, sanitizedName) => Effect.gen(function* () {
|
|
211
|
+
const path = yield* Path.Path;
|
|
212
|
+
const ws = yield* WorkspaceMutations;
|
|
213
|
+
const { canonicalPath, skillSrcPath } = yield* ws.getSkillDir(ref.skill.name, ref);
|
|
214
|
+
yield* validatePathSafety(path, ws.baseDir, canonicalPath);
|
|
215
|
+
const packageRoot = stripFileProtocol(ref.location);
|
|
216
|
+
const sourceSkillPath = ref.portable === true ? packageRoot : path.join(packageRoot, "src");
|
|
217
|
+
yield* validateAxmSkillCandidate({
|
|
218
|
+
ref,
|
|
219
|
+
packageRoot,
|
|
220
|
+
skillSourcePath: sourceSkillPath,
|
|
221
|
+
});
|
|
222
|
+
const isSelfCopy = path.resolve(packageRoot) === path.resolve(canonicalPath);
|
|
223
|
+
if (!isSelfCopy) {
|
|
224
|
+
yield* preCleanAndCopy(sanitizedName, packageRoot, canonicalPath);
|
|
225
|
+
}
|
|
226
|
+
return { skillSrcPath, versionRange: Option.none() };
|
|
227
|
+
});
|
|
228
|
+
const installFromRegistry = (ref, sanitizedName, versionRange, reuse) => Effect.gen(function* () {
|
|
229
|
+
const path = yield* Path.Path;
|
|
230
|
+
const ws = yield* WorkspaceMutations;
|
|
231
|
+
const { canonicalPath, skillSrcPath } = yield* ws.getSkillDir(ref.skill.name, ref);
|
|
232
|
+
yield* validatePathSafety(path, ws.baseDir, canonicalPath);
|
|
233
|
+
const useExisting = yield* canReuseInstalledPackage({
|
|
234
|
+
installedPath: canonicalPath,
|
|
235
|
+
force: reuse.force,
|
|
236
|
+
refVersion: ref.version,
|
|
237
|
+
hasIntegrity: Option.isSome(ref.integrity),
|
|
238
|
+
...(reuse.lockedVersion === undefined ? {} : { lockedVersion: reuse.lockedVersion }),
|
|
239
|
+
existsFailureDetail: (target) => `Failed to check if canonical path exists: ${target}`,
|
|
240
|
+
});
|
|
241
|
+
if (!useExisting) {
|
|
242
|
+
yield* materializeRegistryPackage({
|
|
243
|
+
baseDir: ws.baseDir,
|
|
244
|
+
destinationPath: canonicalPath,
|
|
245
|
+
sourceLocation: ref.source.location,
|
|
246
|
+
owner: ref.owner,
|
|
247
|
+
type: "skill",
|
|
248
|
+
name: ref.name,
|
|
249
|
+
version: ref.version,
|
|
250
|
+
integrity: ref.integrity,
|
|
251
|
+
messages: {
|
|
252
|
+
integrityMismatchDetail: `Integrity mismatch for ${ref.name}@${ref.version}`,
|
|
253
|
+
},
|
|
254
|
+
validate: (stagingPath) => validateAxmSkillCandidate({
|
|
255
|
+
ref,
|
|
256
|
+
packageRoot: stagingPath,
|
|
257
|
+
skillSourcePath: path.join(stagingPath, "src"),
|
|
258
|
+
}).pipe(Effect.asVoid),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
yield* validateAxmSkillCandidate({
|
|
263
|
+
ref,
|
|
264
|
+
packageRoot: canonicalPath,
|
|
265
|
+
skillSourcePath: skillSrcPath,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return { skillSrcPath, versionRange };
|
|
269
|
+
});
|
|
270
|
+
const installFromWorkspace = (ref) => Effect.gen(function* () {
|
|
271
|
+
const fs = yield* FileSystem.FileSystem;
|
|
272
|
+
const path = yield* Path.Path;
|
|
273
|
+
const ws = yield* WorkspaceMutations;
|
|
274
|
+
if (ref.scope !== ws.scope) {
|
|
275
|
+
return yield* new ExtensionLifecycleFailed({
|
|
276
|
+
category: "validation",
|
|
277
|
+
detail: `Workspace skill ${ref.name} belongs to ${ref.scope} scope, not ${ws.scope} scope`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
yield* validatePathSafety(path, ws.baseDir, ref.location);
|
|
281
|
+
const skillSrcPath = path.join(ref.location, "src");
|
|
282
|
+
const exists = yield* fs.exists(skillSrcPath).pipe(Effect.mapError((error) => new ExtensionLifecycleFailed({
|
|
283
|
+
category: "internal",
|
|
284
|
+
detail: `Failed to inspect workspace skill source: ${skillSrcPath}`,
|
|
285
|
+
cause: error,
|
|
286
|
+
})));
|
|
287
|
+
if (!exists) {
|
|
288
|
+
return yield* new ExtensionLifecycleFailed({
|
|
289
|
+
category: "validation",
|
|
290
|
+
detail: `Workspace skill source is missing: ${skillSrcPath}`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
yield* validateAxmSkillCandidate({
|
|
294
|
+
ref,
|
|
295
|
+
packageRoot: ref.location,
|
|
296
|
+
skillSourcePath: skillSrcPath,
|
|
297
|
+
});
|
|
298
|
+
return { skillSrcPath, versionRange: Option.none() };
|
|
299
|
+
});
|
|
300
|
+
// -----------------------------------------------------------------------------
|
|
301
|
+
// Agent symlink helper
|
|
302
|
+
// -----------------------------------------------------------------------------
|
|
303
|
+
const installForDirectory = (opts) => Effect.gen(function* () {
|
|
304
|
+
const path = yield* Path.Path;
|
|
305
|
+
const ws = yield* WorkspaceMutations;
|
|
306
|
+
const agentSkillPath = path.join(opts.targetDir, opts.sanitizedName);
|
|
307
|
+
// Validate agent path safety
|
|
308
|
+
if (!isPathSafe(path, ws.baseDir, agentSkillPath)) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
mode: "symlink",
|
|
312
|
+
symlinkFailed: false,
|
|
313
|
+
error: Option.some(`Path traversal detected for target directory ${opts.targetDir}`),
|
|
314
|
+
path: agentSkillPath,
|
|
315
|
+
canonicalPath: opts.canonicalSkillSrcPath,
|
|
316
|
+
change: "updated",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// Try symlink, fall back to copy
|
|
320
|
+
return yield* createSymlink({
|
|
321
|
+
target: opts.canonicalSkillSrcPath,
|
|
322
|
+
link: agentSkillPath,
|
|
323
|
+
}).pipe(Effect.map((result) => ({
|
|
324
|
+
success: true,
|
|
325
|
+
mode: "symlink",
|
|
326
|
+
symlinkFailed: false,
|
|
327
|
+
error: Option.none(),
|
|
328
|
+
path: agentSkillPath,
|
|
329
|
+
canonicalPath: opts.canonicalSkillSrcPath,
|
|
330
|
+
change: symlinkResultToChange(result),
|
|
331
|
+
})), Effect.catch(() => copyExtensionDirectory(opts.canonicalSkillSrcPath, agentSkillPath).pipe(Effect.map(() => ({
|
|
332
|
+
success: true,
|
|
333
|
+
mode: "copy",
|
|
334
|
+
symlinkFailed: true,
|
|
335
|
+
error: Option.none(),
|
|
336
|
+
path: agentSkillPath,
|
|
337
|
+
canonicalPath: opts.canonicalSkillSrcPath,
|
|
338
|
+
change: "updated",
|
|
339
|
+
})), Effect.catch((copyErr) => Effect.succeed({
|
|
340
|
+
success: false,
|
|
341
|
+
mode: "copy",
|
|
342
|
+
symlinkFailed: true,
|
|
343
|
+
error: Option.some(`Copy fallback failed: ${copyErr.message}`),
|
|
344
|
+
path: agentSkillPath,
|
|
345
|
+
canonicalPath: opts.canonicalSkillSrcPath,
|
|
346
|
+
change: "updated",
|
|
347
|
+
})))));
|
|
348
|
+
});
|
|
349
|
+
const materializeSkill = (ref, sanitizedName, versionRange, reuse) => {
|
|
350
|
+
switch (ref.refType) {
|
|
351
|
+
case "git-hosted":
|
|
352
|
+
return installFromGitHosted(ref, sanitizedName);
|
|
353
|
+
case "registry":
|
|
354
|
+
return installFromRegistry(ref, sanitizedName, versionRange, reuse);
|
|
355
|
+
case "local":
|
|
356
|
+
return installFromLocal(ref, sanitizedName);
|
|
357
|
+
case "workspace":
|
|
358
|
+
return installFromWorkspace(ref);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
// -----------------------------------------------------------------------------
|
|
362
|
+
// Public API
|
|
363
|
+
// -----------------------------------------------------------------------------
|
|
364
|
+
/**
|
|
365
|
+
* Install-skill operation handler.
|
|
366
|
+
*
|
|
367
|
+
* Dispatches to a per-refType install function producing a MaterializedSkill,
|
|
368
|
+
* then runs shared post-install steps:
|
|
369
|
+
* 1. Create symlinks from each agent's skills dir (concurrent)
|
|
370
|
+
* 2. Update lockfile/settings entry (failures logged as warnings)
|
|
371
|
+
* 3. Compute and return overall result
|
|
372
|
+
*/
|
|
373
|
+
export const installSkill = (op) => Effect.gen(function* () {
|
|
374
|
+
const adapter = yield* LifecycleFailureAdapter;
|
|
375
|
+
const ws = yield* WorkspaceMutations;
|
|
376
|
+
const path = yield* Path.Path;
|
|
377
|
+
const agentRepo = yield* CodingAgentRepository;
|
|
378
|
+
const { ref } = op.args;
|
|
379
|
+
const sanitizedName = sanitizeName(ref.skill.name);
|
|
380
|
+
const strictUnknownAgents = Option.getOrElse(op.args.strictUnknownAgents, () => false);
|
|
381
|
+
const previousLock = yield* ws.getLockedSkill(ref.skill.name);
|
|
382
|
+
const previousVersion = ref.refType === "registry" ? acceptedRegistryVersionForRef(previousLock, ref) : undefined;
|
|
383
|
+
const previouslyAccepted = Option.isSome(previousLock);
|
|
384
|
+
const sourceHashBeforeInstall = yield* existingSourceHash(ref);
|
|
385
|
+
// ── Per-refType: resolve source, copy to canonical ──────────────
|
|
386
|
+
const materialized = yield* materializeSkill(ref, sanitizedName, op.args.versionRange, {
|
|
387
|
+
force: op.args.force,
|
|
388
|
+
lockedVersion: previousVersion,
|
|
389
|
+
});
|
|
390
|
+
// ── Shared: resolve agent targets + install once per distinct dir ────────
|
|
391
|
+
const configuredAgents = yield* agentRepo
|
|
392
|
+
.getMaterializationAgents()
|
|
393
|
+
.pipe(Effect.provideService(WorkspaceMutations, ws));
|
|
394
|
+
const unknownConfiguredAgentIds = yield* agentRepo
|
|
395
|
+
.getUnknownConfiguredAgentIds()
|
|
396
|
+
.pipe(Effect.provideService(WorkspaceMutations, ws));
|
|
397
|
+
if (strictUnknownAgents && unknownConfiguredAgentIds.length > 0) {
|
|
398
|
+
const message = `Unknown configured agents in strict mode: ${unknownConfiguredAgentIds.join(", ")}`;
|
|
399
|
+
return {
|
|
400
|
+
result: "error",
|
|
401
|
+
message,
|
|
402
|
+
error: new StepFailure({
|
|
403
|
+
category: "not_found",
|
|
404
|
+
detail: message,
|
|
405
|
+
}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const unknownAgentWarning = unknownConfiguredAgentIds.length === 0
|
|
409
|
+
? undefined
|
|
410
|
+
: `Skipping unknown configured agents: ${unknownConfiguredAgentIds.join(", ")}`;
|
|
411
|
+
const resolvedAgents = yield* Effect.forEach(configuredAgents, (agent) => agent
|
|
412
|
+
.resolveEffectiveSkillsDir({ workspaceRoot: ws.baseDir })
|
|
413
|
+
.pipe(Effect.map((outcome) => ({ agentId: agent.id, outcome }))), { concurrency: "unbounded" });
|
|
414
|
+
const misconfigured = Array.filter(resolvedAgents, ({ outcome }) => outcome._tag === "misconfigured");
|
|
415
|
+
if (misconfigured.length > 0) {
|
|
416
|
+
const message = `Failed to resolve skills directories for ${ref.skill.name}`;
|
|
417
|
+
return {
|
|
418
|
+
result: "error",
|
|
419
|
+
message,
|
|
420
|
+
error: new StepFailure({
|
|
421
|
+
category: "validation",
|
|
422
|
+
detail: message,
|
|
423
|
+
}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const skippedByOutcome = Array.filter(resolvedAgents, ({ outcome }) => outcome._tag === "unsupported" || outcome._tag === "disabled");
|
|
427
|
+
const skippedOutcomeWarning = skippedByOutcome.length === 0
|
|
428
|
+
? undefined
|
|
429
|
+
: `Skipping non-installable configured agents: ${skippedByOutcome
|
|
430
|
+
.map(({ agentId, outcome }) => outcome._tag === "supported"
|
|
431
|
+
? `${agentId}: not skipped`
|
|
432
|
+
: `${agentId}: ${outcome.reason}`)
|
|
433
|
+
.join(", ")}`;
|
|
434
|
+
const installableTargets = [];
|
|
435
|
+
for (const { agentId, outcome } of resolvedAgents) {
|
|
436
|
+
if (outcome._tag === "supported") {
|
|
437
|
+
installableTargets.push({
|
|
438
|
+
agentId,
|
|
439
|
+
targetDir: path.normalize(outcome.dir),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const targetLocations = yield* groupInstallTargetsByDirectory(installableTargets, ws.baseDir);
|
|
444
|
+
const displayTargetDir = targetLocations[0]?.targetDir;
|
|
445
|
+
const perDirectoryResults = yield* Effect.forEach(targetLocations, (location) => installForDirectory({
|
|
446
|
+
targetDir: location.targetDir,
|
|
447
|
+
canonicalSkillSrcPath: materialized.skillSrcPath,
|
|
448
|
+
sanitizedName,
|
|
449
|
+
}).pipe(Effect.map((result) => ({ location, result }))), { concurrency: "unbounded" });
|
|
450
|
+
const agentResults = installableTargets.map((target) => {
|
|
451
|
+
const matched = perDirectoryResults.find((item) => item.location.agentIds.includes(target.agentId));
|
|
452
|
+
if (matched === undefined) {
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
mode: "copy",
|
|
456
|
+
symlinkFailed: true,
|
|
457
|
+
error: Option.some(`No installation result for target directory ${target.targetDir}`),
|
|
458
|
+
path: target.targetDir,
|
|
459
|
+
canonicalPath: materialized.skillSrcPath,
|
|
460
|
+
change: "updated",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return matched.result;
|
|
464
|
+
});
|
|
465
|
+
const sourceHash = ref.refType === "workspace"
|
|
466
|
+
? ref.sourceHash
|
|
467
|
+
: ref.refType === "registry"
|
|
468
|
+
? yield* computePackageContentHash(path.dirname(materialized.skillSrcPath))
|
|
469
|
+
: yield* computeSkillSourceHash(materialized.skillSrcPath);
|
|
470
|
+
// ── Shared: update lockfile + settings ──────────────────────────
|
|
471
|
+
const workspaceRelativeLocalSourcePath = ref.refType === "local"
|
|
472
|
+
? makeWorkspaceRelativeSourcePath(path, ws.baseDir, ref.sourcePath ?? stripFileProtocol(ref.location))
|
|
473
|
+
: Option.none();
|
|
474
|
+
if (ref.refType === "local" && Option.isNone(workspaceRelativeLocalSourcePath)) {
|
|
475
|
+
return yield* new ExtensionLifecycleFailed({
|
|
476
|
+
category: "validation",
|
|
477
|
+
detail: `Local skill source path must stay within the workspace root: ${ref.source.path}`,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const baseLockEntry = sourceToLockEntry({
|
|
481
|
+
ref,
|
|
482
|
+
sourceName: op.args.sourceName,
|
|
483
|
+
contentIdentity: sourceHash,
|
|
484
|
+
treeIntegrity: yield* computeMaterializedTreeIntegrity((yield* ws.getSkillDir(ref.skill.name, ref)).canonicalPath),
|
|
485
|
+
workspaceRelativeLocalSourcePath,
|
|
486
|
+
});
|
|
487
|
+
if (baseLockEntry?.type === "registry") {
|
|
488
|
+
yield* validateExactResolvedVersion(`skills.${ref.skill.name}.resolvedVersion`, baseLockEntry.resolvedVersion);
|
|
489
|
+
}
|
|
490
|
+
// ── Shared: compute result ──────────────────────────────────────
|
|
491
|
+
const anyFailed = agentResults.some((r) => !r.success);
|
|
492
|
+
const fileCount = yield* countFiles(materialized.skillSrcPath);
|
|
493
|
+
const currentSourceHash = yield* computeSkillSourceHash(materialized.skillSrcPath);
|
|
494
|
+
const displayPath = displayTargetDir === undefined
|
|
495
|
+
? path.relative(ws.baseDir, materialized.skillSrcPath)
|
|
496
|
+
: path.join(path.relative(ws.baseDir, displayTargetDir), sanitizedName);
|
|
497
|
+
const artifactAgents = artifactAgentIdsFromTargets(installableTargets);
|
|
498
|
+
const artifactTargets = perDirectoryResults.map(({ location, result }) => {
|
|
499
|
+
const agentIds = artifactTargetAgentIds(location.agentIds);
|
|
500
|
+
return {
|
|
501
|
+
path: path.relative(ws.baseDir, result.path),
|
|
502
|
+
change: result.change ?? "updated",
|
|
503
|
+
...(agentIds.length > 0 ? { agentIds } : {}),
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
const version = baseLockEntry?.type === "registry"
|
|
507
|
+
? baseLockEntry.resolvedVersion
|
|
508
|
+
: Option.getOrUndefined(op.args.versionRange);
|
|
509
|
+
const sameVersion = previousVersion === version;
|
|
510
|
+
const sameSource = sourceHashBeforeInstall === currentSourceHash;
|
|
511
|
+
const fallbackChange = !previouslyAccepted
|
|
512
|
+
? "created"
|
|
513
|
+
: sameVersion && sameSource
|
|
514
|
+
? "unchanged"
|
|
515
|
+
: "updated";
|
|
516
|
+
const artifactChange = artifactChangeFromTargets(fallbackChange, artifactTargets);
|
|
517
|
+
if (anyFailed) {
|
|
518
|
+
const failedAgents = agentResults
|
|
519
|
+
.filter((r) => !r.success)
|
|
520
|
+
.map((r) => Option.getOrElse(r.error, () => "unknown error"));
|
|
521
|
+
const message = `Failed to install ${ref.skill.name} for some agents: ${failedAgents.join(", ")}`;
|
|
522
|
+
return {
|
|
523
|
+
result: "error",
|
|
524
|
+
message,
|
|
525
|
+
error: new StepFailure({
|
|
526
|
+
category: "internal",
|
|
527
|
+
detail: message,
|
|
528
|
+
}),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const skipSettings = Option.getOrElse(op.args.skipSettings, () => false);
|
|
532
|
+
const writeEffect = baseLockEntry === undefined
|
|
533
|
+
? skipSettings
|
|
534
|
+
? Effect.void
|
|
535
|
+
: ws.setSkillEntry(ref.skill.name, {
|
|
536
|
+
source: ref.refType === "workspace" ? "workspace" : printSourceParams(ref.source),
|
|
537
|
+
enabled: true,
|
|
538
|
+
})
|
|
539
|
+
: skipSettings
|
|
540
|
+
? ws.setSkillLock({
|
|
541
|
+
name: ref.skill.name,
|
|
542
|
+
lockEntry: baseLockEntry,
|
|
543
|
+
versionRange: materialized.versionRange,
|
|
544
|
+
})
|
|
545
|
+
: ws.setSkill({
|
|
546
|
+
name: ref.skill.name,
|
|
547
|
+
lockEntry: baseLockEntry,
|
|
548
|
+
versionRange: materialized.versionRange,
|
|
549
|
+
});
|
|
550
|
+
const writeFailure = yield* writeEffect.pipe(Effect.result);
|
|
551
|
+
if (writeFailure._tag === "Failure") {
|
|
552
|
+
const failure = writeFailure.failure;
|
|
553
|
+
const detail = `Installed ${ref.skill.name}, but failed to record desired state: ${adapter.describeFailure(failure)}`;
|
|
554
|
+
return {
|
|
555
|
+
result: "error",
|
|
556
|
+
message: detail,
|
|
557
|
+
error: new StepFailure({
|
|
558
|
+
category: adapter.toStepFailure(failure).category,
|
|
559
|
+
detail,
|
|
560
|
+
suggestions: [
|
|
561
|
+
{
|
|
562
|
+
description: "Retry the install after repairing workspace write access.",
|
|
563
|
+
cmd: `axm skills install ${baseLockEntry === undefined ? printSourceParams(ref.source) : printSkillLockSourceLocator(ref.skill.name, baseLockEntry)}`,
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
cause: failure,
|
|
567
|
+
}),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const warnings = [unknownAgentWarning, skippedOutcomeWarning].filter((warning) => warning !== undefined);
|
|
571
|
+
const sourceDetails = gitHostedSkillArtifactSource(ref);
|
|
572
|
+
return {
|
|
573
|
+
result: "success",
|
|
574
|
+
message: appendWarningsToMessage(`Installed ${ref.skill.name}`, warnings),
|
|
575
|
+
artifact: {
|
|
576
|
+
path: displayPath.length === 0 ? "." : displayPath,
|
|
577
|
+
scope: ws.scope,
|
|
578
|
+
agents: artifactAgents,
|
|
579
|
+
...(version !== undefined ? { version } : {}),
|
|
580
|
+
change: artifactChange,
|
|
581
|
+
...(previousVersion !== undefined && previousVersion !== version
|
|
582
|
+
? { previousVersion }
|
|
583
|
+
: {}),
|
|
584
|
+
fileCount,
|
|
585
|
+
...(artifactTargets.length > 0 ? { targets: artifactTargets } : {}),
|
|
586
|
+
...(sourceDetails !== undefined ? { source: sourceDetails } : {}),
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}).pipe(withAdaptedStepFailures);
|
|
590
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const computeSkillSourceHash: (packageDir: string) => import("effect/Effect").Effect<import("@agentxm/extension-model/unstable/sources/source-hash").SourceHash, import("@agentxm/workspace-state").PackageContentHashFailed, import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
|
|
2
|
+
//# sourceMappingURL=source-hash.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { computePackageContentHash } from "@agentxm/workspace-state";
|
|
2
|
+
// Source hashes are advisory change markers. Reusing the package-content
|
|
3
|
+
// algorithm gives every relative path and byte sequence an unambiguous NUL-
|
|
4
|
+
// separated representation.
|
|
5
|
+
export const computeSkillSourceHash = computePackageContentHash;
|
|
6
|
+
//# sourceMappingURL=source-hash.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall skill executor — orchestrates per-skill removal pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: resolve desired/observed state -> remove agent artifacts
|
|
5
|
+
* concurrently -> remove canonical source -> clear settings and accepted resolution.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
*/
|
|
9
|
+
import * as FileSystem from "effect/FileSystem";
|
|
10
|
+
import * as Path from "effect/Path";
|
|
11
|
+
import { LifecycleFailureAdapter } from "../../failure-adapter.js";
|
|
12
|
+
import type { OperationHandler } from "@agentxm/workspace-operations";
|
|
13
|
+
import type { Operation } from "@agentxm/workspace-operations";
|
|
14
|
+
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
15
|
+
/**
|
|
16
|
+
* Args for the uninstall-skill operation.
|
|
17
|
+
*/
|
|
18
|
+
export interface UninstallSkillOperationArgs {
|
|
19
|
+
readonly skillName: string;
|
|
20
|
+
/** Agent filter for partial uninstall. Empty = all agents. */
|
|
21
|
+
readonly agents: ReadonlyArray<string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Remove a skill from the workspace.
|
|
25
|
+
*
|
|
26
|
+
* @experimental This API is unstable and may change without notice.
|
|
27
|
+
*/
|
|
28
|
+
export type UninstallSkillOperation = Operation<"uninstall-skill", UninstallSkillOperationArgs>;
|
|
29
|
+
/**
|
|
30
|
+
* Uninstall-skill operation handler.
|
|
31
|
+
*
|
|
32
|
+
* Reads workspace paths from the WorkspaceMutations service, then orchestrates:
|
|
33
|
+
* 1. Sanitize skill name for filesystem
|
|
34
|
+
* 2. Resolve configured and observed state
|
|
35
|
+
* 3. Remove agent symlinks concurrently (skip missing)
|
|
36
|
+
* 4. Remove from all known canonical locations (full uninstall only)
|
|
37
|
+
* 5. Remove or update settings and accepted resolution
|
|
38
|
+
*/
|
|
39
|
+
export declare const uninstallSkill: OperationHandler<UninstallSkillOperation, FileSystem.FileSystem | Path.Path | WorkspaceMutations | LifecycleFailureAdapter>;
|
|
40
|
+
//# sourceMappingURL=uninstall.d.ts.map
|