cmx-core 0.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.
- package/README.md +66 -0
- package/dist/checksum.d.ts +14 -0
- package/dist/checksum.js +53 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +45 -0
- package/dist/frontmatter.d.ts +3 -0
- package/dist/frontmatter.js +105 -0
- package/dist/gateway.d.ts +34 -0
- package/dist/gateway.js +53 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/json-file.d.ts +4 -0
- package/dist/json-file.js +33 -0
- package/dist/lockfile.d.ts +8 -0
- package/dist/lockfile.js +33 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +84 -0
- package/dist/platform.d.ts +10 -0
- package/dist/platform.js +137 -0
- package/dist/skill-fs.d.ts +9 -0
- package/dist/skill-fs.js +23 -0
- package/dist/skill-installer.d.ts +113 -0
- package/dist/skill-installer.js +348 -0
- package/dist/targets.d.ts +9 -0
- package/dist/targets.js +23 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +30 -0
- package/package.json +51 -0
package/dist/platform.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export const PLATFORM_VALUES = [
|
|
2
|
+
"claude",
|
|
3
|
+
"copilot",
|
|
4
|
+
"cursor",
|
|
5
|
+
"windsurf",
|
|
6
|
+
"gemini",
|
|
7
|
+
"opencode",
|
|
8
|
+
"codex",
|
|
9
|
+
"pi",
|
|
10
|
+
"crush",
|
|
11
|
+
"amp",
|
|
12
|
+
"zed",
|
|
13
|
+
"openhands",
|
|
14
|
+
"hermes",
|
|
15
|
+
"devin",
|
|
16
|
+
];
|
|
17
|
+
const sharedAgentsSkills = ".agents/skills";
|
|
18
|
+
const PLATFORM_SPECS = {
|
|
19
|
+
claude: {
|
|
20
|
+
slug: "",
|
|
21
|
+
skillSubpath: { global: ".claude/skills", local: ".claude/skills" },
|
|
22
|
+
supportsAgent: true,
|
|
23
|
+
agentExtension: "md",
|
|
24
|
+
},
|
|
25
|
+
copilot: {
|
|
26
|
+
slug: "copilot",
|
|
27
|
+
skillSubpath: { global: ".copilot/skills", local: ".github/skills" },
|
|
28
|
+
supportsAgent: true,
|
|
29
|
+
agentExtension: "md",
|
|
30
|
+
},
|
|
31
|
+
cursor: {
|
|
32
|
+
slug: "cursor",
|
|
33
|
+
skillSubpath: { global: ".cursor/skills", local: ".cursor/skills" },
|
|
34
|
+
supportsAgent: true,
|
|
35
|
+
agentExtension: "md",
|
|
36
|
+
},
|
|
37
|
+
windsurf: {
|
|
38
|
+
slug: "windsurf",
|
|
39
|
+
skillSubpath: {
|
|
40
|
+
global: ".codeium/windsurf/skills",
|
|
41
|
+
local: ".windsurf/skills",
|
|
42
|
+
},
|
|
43
|
+
supportsAgent: true,
|
|
44
|
+
agentExtension: "md",
|
|
45
|
+
},
|
|
46
|
+
gemini: {
|
|
47
|
+
slug: "gemini",
|
|
48
|
+
skillSubpath: { global: ".gemini/skills", local: ".gemini/skills" },
|
|
49
|
+
supportsAgent: true,
|
|
50
|
+
agentExtension: "md",
|
|
51
|
+
},
|
|
52
|
+
opencode: {
|
|
53
|
+
slug: "opencode",
|
|
54
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
55
|
+
supportsAgent: true,
|
|
56
|
+
agentExtension: "md",
|
|
57
|
+
},
|
|
58
|
+
codex: {
|
|
59
|
+
slug: "codex",
|
|
60
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
61
|
+
supportsAgent: true,
|
|
62
|
+
agentExtension: "toml",
|
|
63
|
+
},
|
|
64
|
+
pi: {
|
|
65
|
+
slug: "pi",
|
|
66
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
67
|
+
supportsAgent: false,
|
|
68
|
+
agentExtension: "md",
|
|
69
|
+
},
|
|
70
|
+
crush: {
|
|
71
|
+
slug: "crush",
|
|
72
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
73
|
+
supportsAgent: false,
|
|
74
|
+
agentExtension: "md",
|
|
75
|
+
},
|
|
76
|
+
amp: {
|
|
77
|
+
slug: "amp",
|
|
78
|
+
skillSubpath: { global: ".config/agents/skills", local: sharedAgentsSkills },
|
|
79
|
+
supportsAgent: false,
|
|
80
|
+
agentExtension: "md",
|
|
81
|
+
},
|
|
82
|
+
zed: {
|
|
83
|
+
slug: "zed",
|
|
84
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
85
|
+
supportsAgent: false,
|
|
86
|
+
agentExtension: "md",
|
|
87
|
+
},
|
|
88
|
+
openhands: {
|
|
89
|
+
slug: "openhands",
|
|
90
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
91
|
+
supportsAgent: false,
|
|
92
|
+
agentExtension: "md",
|
|
93
|
+
},
|
|
94
|
+
hermes: {
|
|
95
|
+
slug: "hermes",
|
|
96
|
+
skillSubpath: { global: ".hermes/skills", local: sharedAgentsSkills },
|
|
97
|
+
supportsAgent: false,
|
|
98
|
+
agentExtension: "md",
|
|
99
|
+
},
|
|
100
|
+
devin: {
|
|
101
|
+
slug: "devin",
|
|
102
|
+
skillSubpath: { global: sharedAgentsSkills, local: sharedAgentsSkills },
|
|
103
|
+
supportsAgent: false,
|
|
104
|
+
agentExtension: "md",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
export const isPlatform = (value) => PLATFORM_VALUES.includes(value);
|
|
108
|
+
export const platformSlug = (platform) => PLATFORM_SPECS[platform].slug;
|
|
109
|
+
export const platformInstallSubpath = (platform, kind, scope) => {
|
|
110
|
+
if (kind === "skill") {
|
|
111
|
+
return PLATFORM_SPECS[platform].skillSubpath[scope];
|
|
112
|
+
}
|
|
113
|
+
if (!PLATFORM_SPECS[platform].supportsAgent) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (platform === "copilot") {
|
|
117
|
+
const base = scope === "local" ? ".github" : ".copilot";
|
|
118
|
+
return `${base}/agents`;
|
|
119
|
+
}
|
|
120
|
+
if (platform === "windsurf") {
|
|
121
|
+
return scope === "local" ? ".windsurf/agents" : ".codeium/windsurf/agents";
|
|
122
|
+
}
|
|
123
|
+
if (platform === "opencode") {
|
|
124
|
+
return scope === "local" ? ".opencode/agent" : ".config/opencode/agent";
|
|
125
|
+
}
|
|
126
|
+
if (platform === "codex") {
|
|
127
|
+
return ".codex/agents";
|
|
128
|
+
}
|
|
129
|
+
return `.${platform}/agents`;
|
|
130
|
+
};
|
|
131
|
+
export const supportsArtifact = (platform, kind) => kind === "skill" || PLATFORM_SPECS[platform].supportsAgent;
|
|
132
|
+
export const agentExtension = (platform) => PLATFORM_SPECS[platform].agentExtension;
|
|
133
|
+
export const lockFileName = (platform) => {
|
|
134
|
+
const slug = platformSlug(platform);
|
|
135
|
+
return slug === "" ? "cmx-lock.json" : `cmx-lock-${slug}.json`;
|
|
136
|
+
};
|
|
137
|
+
export const platformsLabel = (platforms) => platforms.join(", ");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Filesystem } from "./gateway.js";
|
|
2
|
+
export interface SkillFile {
|
|
3
|
+
relPath: string;
|
|
4
|
+
bytes: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export declare const textSkillFile: (relPath: string, content: string) => SkillFile;
|
|
7
|
+
export declare const canonicalFiles: (files: readonly SkillFile[]) => SkillFile[];
|
|
8
|
+
export declare const checksumBundled: (files: readonly SkillFile[]) => string;
|
|
9
|
+
export declare const writeSkillFiles: (destDir: string, files: readonly SkillFile[], fs: Filesystem) => Promise<void>;
|
package/dist/skill-fs.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "node:path/posix";
|
|
2
|
+
import { checksumInMemory, isCanonicalRelPath, relPathKey } from "./checksum.js";
|
|
3
|
+
const textEncoder = new TextEncoder();
|
|
4
|
+
export const textSkillFile = (relPath, content) => ({
|
|
5
|
+
relPath,
|
|
6
|
+
bytes: textEncoder.encode(content),
|
|
7
|
+
});
|
|
8
|
+
export const canonicalFiles = (files) => [...files]
|
|
9
|
+
.filter((file) => isCanonicalRelPath(file.relPath))
|
|
10
|
+
.sort((left, right) => {
|
|
11
|
+
const leftKey = relPathKey(left.relPath);
|
|
12
|
+
const rightKey = relPathKey(right.relPath);
|
|
13
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
14
|
+
});
|
|
15
|
+
export const checksumBundled = (files) => checksumInMemory(canonicalFiles(files));
|
|
16
|
+
export const writeSkillFiles = async (destDir, files, fs) => {
|
|
17
|
+
await fs.createDirAll(destDir);
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const destPath = path.join(destDir, file.relPath);
|
|
20
|
+
await fs.createDirAll(path.dirname(destPath));
|
|
21
|
+
await fs.writeBytes(destPath, file.bytes);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Clock, Filesystem } from "./gateway.js";
|
|
2
|
+
import type { ConfigPaths } from "./paths.js";
|
|
3
|
+
import { type Platform } from "./platform.js";
|
|
4
|
+
import { type SkillFile } from "./skill-fs.js";
|
|
5
|
+
import type { InstallScope } from "./types.js";
|
|
6
|
+
export type Scope = InstallScope;
|
|
7
|
+
export declare class ToolIdentity {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly version: string;
|
|
10
|
+
constructor(name: string, version: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class BundledSkill {
|
|
13
|
+
readonly files: SkillFile[];
|
|
14
|
+
constructor(files: SkillFile[]);
|
|
15
|
+
static fromFiles(files: SkillFile[]): BundledSkill;
|
|
16
|
+
static singleMd(content: string): BundledSkill;
|
|
17
|
+
hasSkillMd(): boolean;
|
|
18
|
+
}
|
|
19
|
+
export type DiskState = "missing" | "matches-source" | "drifted";
|
|
20
|
+
export type TargetAction = {
|
|
21
|
+
kind: "install";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "update";
|
|
24
|
+
from: string | null;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "skip";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "drifted-skip";
|
|
29
|
+
installed: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "refuse-newer";
|
|
32
|
+
installed: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "downgrade";
|
|
35
|
+
from: string;
|
|
36
|
+
};
|
|
37
|
+
export interface PlannedFile {
|
|
38
|
+
rel_path: string;
|
|
39
|
+
dest_path: string;
|
|
40
|
+
}
|
|
41
|
+
export interface TargetPlan {
|
|
42
|
+
platform: Platform;
|
|
43
|
+
scope: InstallScope;
|
|
44
|
+
dest_dir: string;
|
|
45
|
+
files: PlannedFile[];
|
|
46
|
+
action: TargetAction;
|
|
47
|
+
cmx_managed: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface InstallPlan {
|
|
50
|
+
tool: ToolIdentity;
|
|
51
|
+
scope: InstallScope;
|
|
52
|
+
source_checksum: string;
|
|
53
|
+
cmx_present: boolean;
|
|
54
|
+
force: boolean;
|
|
55
|
+
targets: TargetPlan[];
|
|
56
|
+
}
|
|
57
|
+
export interface TargetOutcome {
|
|
58
|
+
platform: Platform;
|
|
59
|
+
dest_dir: string;
|
|
60
|
+
action: TargetAction;
|
|
61
|
+
files_written: number;
|
|
62
|
+
installed_checksum: string | null;
|
|
63
|
+
discarded_paths: string[];
|
|
64
|
+
}
|
|
65
|
+
export interface Report {
|
|
66
|
+
tool: ToolIdentity;
|
|
67
|
+
scope: InstallScope;
|
|
68
|
+
targets: TargetOutcome[];
|
|
69
|
+
source_registered: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface TargetStatus {
|
|
72
|
+
platform: Platform;
|
|
73
|
+
installed: boolean;
|
|
74
|
+
installed_version: string | null;
|
|
75
|
+
drifted: boolean;
|
|
76
|
+
tracked: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface Status {
|
|
79
|
+
tool_name: string;
|
|
80
|
+
scope: InstallScope;
|
|
81
|
+
targets: TargetStatus[];
|
|
82
|
+
}
|
|
83
|
+
export interface RemoveReport {
|
|
84
|
+
tool_name: string;
|
|
85
|
+
scope: InstallScope;
|
|
86
|
+
removed_dirs: string[];
|
|
87
|
+
platforms_cleared: Platform[];
|
|
88
|
+
source_unregistered: boolean;
|
|
89
|
+
was_on_disk: boolean;
|
|
90
|
+
was_tracked: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface InstallerContext {
|
|
93
|
+
fs: Filesystem;
|
|
94
|
+
clock: Clock;
|
|
95
|
+
paths: ConfigPaths;
|
|
96
|
+
}
|
|
97
|
+
export interface VersionGuardDecisionInput {
|
|
98
|
+
bundledVersion: string;
|
|
99
|
+
tracked: boolean;
|
|
100
|
+
installedVersion: string | null;
|
|
101
|
+
diskState: DiskState;
|
|
102
|
+
force: boolean;
|
|
103
|
+
}
|
|
104
|
+
export declare const decideVersionGuardAction: (input: VersionGuardDecisionInput) => TargetAction;
|
|
105
|
+
export declare class SkillInstaller {
|
|
106
|
+
readonly tool: ToolIdentity;
|
|
107
|
+
constructor(tool: ToolIdentity);
|
|
108
|
+
plan(skill: BundledSkill, scope: Scope, force: boolean, context: InstallerContext): Promise<InstallPlan>;
|
|
109
|
+
apply(skill: BundledSkill, plan: InstallPlan, context: InstallerContext): Promise<Report>;
|
|
110
|
+
status(scope: Scope, context: InstallerContext): Promise<Status>;
|
|
111
|
+
remove(scope: Scope, context: InstallerContext): Promise<RemoveReport>;
|
|
112
|
+
private registerManagedSource;
|
|
113
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import path from "node:path/posix";
|
|
2
|
+
import { coerce, compare as compareSemver } from "semver";
|
|
3
|
+
import { checksumDir } from "./checksum.js";
|
|
4
|
+
import { loadConfig, loadSources, managedOrAllPlatforms, managedPlatforms, mutateSources, resolveArtifactHome, } from "./config.js";
|
|
5
|
+
import { reconcileSkillVersion } from "./frontmatter.js";
|
|
6
|
+
import { loadLockFile, mutateLockFile } from "./lockfile.js";
|
|
7
|
+
import { PLATFORM_VALUES } from "./platform.js";
|
|
8
|
+
import { canonicalFiles, checksumBundled, textSkillFile, writeSkillFiles, } from "./skill-fs.js";
|
|
9
|
+
import { resolveTargets } from "./targets.js";
|
|
10
|
+
export class ToolIdentity {
|
|
11
|
+
name;
|
|
12
|
+
version;
|
|
13
|
+
constructor(name, version) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.version = version;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class BundledSkill {
|
|
19
|
+
files;
|
|
20
|
+
constructor(files) {
|
|
21
|
+
this.files = files;
|
|
22
|
+
}
|
|
23
|
+
static fromFiles(files) {
|
|
24
|
+
return new BundledSkill(files);
|
|
25
|
+
}
|
|
26
|
+
static singleMd(content) {
|
|
27
|
+
return new BundledSkill([textSkillFile("SKILL.md", content)]);
|
|
28
|
+
}
|
|
29
|
+
hasSkillMd() {
|
|
30
|
+
return this.files.some((file) => file.relPath === "SKILL.md");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const willWrite = (action) => action.kind === "install" || action.kind === "update" || action.kind === "downgrade";
|
|
34
|
+
const isBlocked = (action) => action.kind === "refuse-newer";
|
|
35
|
+
const formatTimestamp = (date) => {
|
|
36
|
+
const pad = (value) => `${value}`.padStart(2, "0");
|
|
37
|
+
return (`${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}` +
|
|
38
|
+
`T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}+00:00`);
|
|
39
|
+
};
|
|
40
|
+
const compareVersions = (installedVersion, bundledVersion) => {
|
|
41
|
+
if (installedVersion === null) {
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
const installedSemver = coerce(installedVersion);
|
|
45
|
+
const bundledSemver = coerce(bundledVersion);
|
|
46
|
+
if (installedSemver !== null && bundledSemver !== null) {
|
|
47
|
+
return compareSemver(installedSemver, bundledSemver);
|
|
48
|
+
}
|
|
49
|
+
return installedVersion === bundledVersion ? 0 : -1;
|
|
50
|
+
};
|
|
51
|
+
export const decideVersionGuardAction = (input) => {
|
|
52
|
+
if (!input.tracked) {
|
|
53
|
+
return { kind: "install" };
|
|
54
|
+
}
|
|
55
|
+
const comparison = compareVersions(input.installedVersion, input.bundledVersion);
|
|
56
|
+
if (comparison < 0) {
|
|
57
|
+
return { kind: "update", from: input.installedVersion };
|
|
58
|
+
}
|
|
59
|
+
if (comparison === 0) {
|
|
60
|
+
if (input.diskState === "missing") {
|
|
61
|
+
return { kind: "install" };
|
|
62
|
+
}
|
|
63
|
+
if (input.diskState === "matches-source") {
|
|
64
|
+
return { kind: "skip" };
|
|
65
|
+
}
|
|
66
|
+
return input.force
|
|
67
|
+
? { kind: "update", from: input.installedVersion }
|
|
68
|
+
: {
|
|
69
|
+
kind: "drifted-skip",
|
|
70
|
+
installed: input.installedVersion ?? "unknown",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return input.force
|
|
74
|
+
? { kind: "downgrade", from: input.installedVersion ?? "unknown" }
|
|
75
|
+
: { kind: "refuse-newer", installed: input.installedVersion ?? "unknown" };
|
|
76
|
+
};
|
|
77
|
+
const buildLockEntry = (tool, checksum, installedAt) => ({
|
|
78
|
+
type: "skill",
|
|
79
|
+
version: tool.version,
|
|
80
|
+
installed_at: installedAt,
|
|
81
|
+
source: {
|
|
82
|
+
repo: `bundled:${tool.name}`,
|
|
83
|
+
path: `skills/${tool.name}`,
|
|
84
|
+
},
|
|
85
|
+
source_checksum: checksum,
|
|
86
|
+
installed_checksum: checksum,
|
|
87
|
+
});
|
|
88
|
+
const computeDiskState = async (skillDest, sourceChecksum, fs) => {
|
|
89
|
+
if (!(await fs.exists(skillDest))) {
|
|
90
|
+
return "missing";
|
|
91
|
+
}
|
|
92
|
+
const diskChecksum = await checksumDir(skillDest, fs);
|
|
93
|
+
return diskChecksum === sourceChecksum ? "matches-source" : "drifted";
|
|
94
|
+
};
|
|
95
|
+
const unique = (values) => [...new Set(values)];
|
|
96
|
+
const discardedPathsAgainstBundle = async (skillDest, bundledFiles, fs) => {
|
|
97
|
+
if (!(await fs.exists(skillDest))) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const installedFiles = await collectFilesRecursive(skillDest, skillDest, fs);
|
|
101
|
+
const installedByRel = new Map(installedFiles.map((file) => [file.relPath, file.bytes]));
|
|
102
|
+
const bundledByRel = new Map(canonicalFiles(bundledFiles).map((file) => [file.relPath, file.bytes]));
|
|
103
|
+
const relativePaths = unique([...installedByRel.keys(), ...bundledByRel.keys()]).sort((left, right) => left.localeCompare(right));
|
|
104
|
+
return relativePaths.flatMap((relPath) => {
|
|
105
|
+
const installed = installedByRel.get(relPath);
|
|
106
|
+
const bundled = bundledByRel.get(relPath);
|
|
107
|
+
const equal = installed !== undefined &&
|
|
108
|
+
bundled !== undefined &&
|
|
109
|
+
installed.length === bundled.length &&
|
|
110
|
+
installed.every((byte, index) => byte === bundled[index]);
|
|
111
|
+
return equal ? [] : [path.join(skillDest, relPath)];
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const collectFilesRecursive = async (rootDir, currentDir, fs) => {
|
|
115
|
+
if (!(await fs.exists(currentDir))) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const entries = await fs.listDir(currentDir);
|
|
119
|
+
const files = [];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const absolutePath = path.join(currentDir, entry.fileName);
|
|
122
|
+
if (entry.isDirectory) {
|
|
123
|
+
files.push(...(await collectFilesRecursive(rootDir, absolutePath, fs)));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
files.push({
|
|
127
|
+
relPath: path.relative(rootDir, absolutePath),
|
|
128
|
+
bytes: await fs.read(absolutePath),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return files;
|
|
132
|
+
};
|
|
133
|
+
export class SkillInstaller {
|
|
134
|
+
tool;
|
|
135
|
+
constructor(tool) {
|
|
136
|
+
this.tool = tool;
|
|
137
|
+
}
|
|
138
|
+
async plan(skill, scope, force, context) {
|
|
139
|
+
if (!skill.hasSkillMd()) {
|
|
140
|
+
throw new Error(`BundledSkill for '${this.tool.name}' is missing SKILL.md`);
|
|
141
|
+
}
|
|
142
|
+
const files = reconcileSkillVersion(skill.files, this.tool.version);
|
|
143
|
+
const sourceChecksum = checksumBundled(files);
|
|
144
|
+
const targets = await resolveTargets(undefined, "skill", scope, context);
|
|
145
|
+
const managed = await managedPlatforms(context.fs, context.paths);
|
|
146
|
+
const cmxManaged = managed !== undefined;
|
|
147
|
+
const cmxPresent = cmxManaged ||
|
|
148
|
+
(await Promise.all(PLATFORM_VALUES.map(async (platform) => {
|
|
149
|
+
const lockFile = await loadLockFile(scope, context.fs, context.paths.withPlatform(platform));
|
|
150
|
+
return Object.keys(lockFile.packages).length > 0;
|
|
151
|
+
}))).some(Boolean);
|
|
152
|
+
const targetPlans = [];
|
|
153
|
+
for (const platform of targets) {
|
|
154
|
+
const platformPaths = context.paths.withPlatform(platform);
|
|
155
|
+
const skillDest = path.join(platformPaths.requireInstallDir("skill", scope), this.tool.name);
|
|
156
|
+
const plannedFiles = files.map((file) => ({
|
|
157
|
+
rel_path: file.relPath,
|
|
158
|
+
dest_path: path.join(skillDest, file.relPath),
|
|
159
|
+
}));
|
|
160
|
+
const lockFile = await loadLockFile(scope, context.fs, platformPaths);
|
|
161
|
+
const entry = lockFile.packages[this.tool.name];
|
|
162
|
+
const action = entry === undefined
|
|
163
|
+
? { kind: "install" }
|
|
164
|
+
: decideVersionGuardAction({
|
|
165
|
+
bundledVersion: this.tool.version,
|
|
166
|
+
tracked: true,
|
|
167
|
+
installedVersion: entry.version ?? null,
|
|
168
|
+
diskState: await computeDiskState(skillDest, sourceChecksum, context.fs),
|
|
169
|
+
force,
|
|
170
|
+
});
|
|
171
|
+
targetPlans.push({
|
|
172
|
+
platform,
|
|
173
|
+
scope,
|
|
174
|
+
dest_dir: skillDest,
|
|
175
|
+
files: plannedFiles,
|
|
176
|
+
action,
|
|
177
|
+
cmx_managed: cmxManaged,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
tool: this.tool,
|
|
182
|
+
scope,
|
|
183
|
+
source_checksum: sourceChecksum,
|
|
184
|
+
cmx_present: cmxPresent,
|
|
185
|
+
force,
|
|
186
|
+
targets: targetPlans,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async apply(skill, plan, context) {
|
|
190
|
+
if (plan.targets.some((target) => isBlocked(target.action))) {
|
|
191
|
+
throw new Error(`Install plan for '${this.tool.name}' is blocked. Run with force=true to override.`);
|
|
192
|
+
}
|
|
193
|
+
const files = reconcileSkillVersion(skill.files, this.tool.version);
|
|
194
|
+
const currentChecksum = checksumBundled(files);
|
|
195
|
+
if (currentChecksum !== plan.source_checksum) {
|
|
196
|
+
throw new Error(`Parity check failed for '${this.tool.name}': the BundledSkill has changed since plan() was called.`);
|
|
197
|
+
}
|
|
198
|
+
const dirsToWrite = unique(plan.targets.filter((target) => willWrite(target.action)).map((target) => target.dest_dir));
|
|
199
|
+
const replaceDirs = unique(plan.targets
|
|
200
|
+
.filter((target) => plan.force && (target.action.kind === "update" || target.action.kind === "downgrade"))
|
|
201
|
+
.map((target) => target.dest_dir));
|
|
202
|
+
const discardedByDir = new Map();
|
|
203
|
+
for (const dir of replaceDirs) {
|
|
204
|
+
discardedByDir.set(dir, await discardedPathsAgainstBundle(dir, files, context.fs));
|
|
205
|
+
if (await context.fs.exists(dir)) {
|
|
206
|
+
await context.fs.removeDirAll(dir);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const dir of dirsToWrite) {
|
|
210
|
+
await writeSkillFiles(dir, files, context.fs);
|
|
211
|
+
}
|
|
212
|
+
const installedAt = formatTimestamp(context.clock.now());
|
|
213
|
+
const installedChecksum = plan.source_checksum;
|
|
214
|
+
const targets = [];
|
|
215
|
+
for (const target of plan.targets) {
|
|
216
|
+
if (!willWrite(target.action)) {
|
|
217
|
+
targets.push({
|
|
218
|
+
platform: target.platform,
|
|
219
|
+
dest_dir: target.dest_dir,
|
|
220
|
+
action: target.action,
|
|
221
|
+
files_written: 0,
|
|
222
|
+
installed_checksum: null,
|
|
223
|
+
discarded_paths: [],
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
await mutateLockFile(scopeFromPlan(target.scope), context.fs, context.paths.withPlatform(target.platform), (lockFile) => {
|
|
228
|
+
lockFile.packages[this.tool.name] = buildLockEntry(this.tool, installedChecksum, installedAt);
|
|
229
|
+
});
|
|
230
|
+
targets.push({
|
|
231
|
+
platform: target.platform,
|
|
232
|
+
dest_dir: target.dest_dir,
|
|
233
|
+
action: target.action,
|
|
234
|
+
files_written: target.files.length,
|
|
235
|
+
installed_checksum: installedChecksum,
|
|
236
|
+
discarded_paths: discardedByDir.get(target.dest_dir) ?? [],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const sourceRegistered = (await managedPlatforms(context.fs, context.paths)) !== undefined
|
|
240
|
+
? await this.registerManagedSource(files, context)
|
|
241
|
+
: false;
|
|
242
|
+
return {
|
|
243
|
+
tool: this.tool,
|
|
244
|
+
scope: plan.scope,
|
|
245
|
+
targets,
|
|
246
|
+
source_registered: sourceRegistered,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async status(scope, context) {
|
|
250
|
+
const targets = await resolveTargets(undefined, "skill", scope, context);
|
|
251
|
+
const statuses = [];
|
|
252
|
+
for (const platform of targets) {
|
|
253
|
+
const platformPaths = context.paths.withPlatform(platform);
|
|
254
|
+
const skillDir = path.join(platformPaths.requireInstallDir("skill", scope), this.tool.name);
|
|
255
|
+
const installed = await context.fs.exists(skillDir);
|
|
256
|
+
const lockFile = await loadLockFile(scope, context.fs, platformPaths);
|
|
257
|
+
const entry = lockFile.packages[this.tool.name];
|
|
258
|
+
const tracked = entry !== undefined;
|
|
259
|
+
const drifted = installed && tracked
|
|
260
|
+
? (await checksumDir(skillDir, context.fs)) !== entry.installed_checksum
|
|
261
|
+
: false;
|
|
262
|
+
statuses.push({
|
|
263
|
+
platform,
|
|
264
|
+
installed,
|
|
265
|
+
installed_version: entry?.version ?? null,
|
|
266
|
+
drifted,
|
|
267
|
+
tracked,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
tool_name: this.tool.name,
|
|
272
|
+
scope,
|
|
273
|
+
targets: statuses,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async remove(scope, context) {
|
|
277
|
+
const platforms = (await managedOrAllPlatforms(context.fs, context.paths)).filter((platform) => platform !== undefined);
|
|
278
|
+
const skillPlatforms = platforms;
|
|
279
|
+
const dirsToDelete = new Set();
|
|
280
|
+
const platformsCleared = [];
|
|
281
|
+
let wasTracked = false;
|
|
282
|
+
for (const platform of skillPlatforms) {
|
|
283
|
+
const platformPaths = context.paths.withPlatform(platform);
|
|
284
|
+
const skillDir = path.join(platformPaths.requireInstallDir("skill", scope), this.tool.name);
|
|
285
|
+
if (await context.fs.exists(skillDir)) {
|
|
286
|
+
dirsToDelete.add(skillDir);
|
|
287
|
+
}
|
|
288
|
+
const lockFile = await loadLockFile(scope, context.fs, platformPaths);
|
|
289
|
+
if (lockFile.packages[this.tool.name] !== undefined) {
|
|
290
|
+
await mutateLockFile(scope, context.fs, platformPaths, (mutableLock) => {
|
|
291
|
+
delete mutableLock.packages[this.tool.name];
|
|
292
|
+
});
|
|
293
|
+
platformsCleared.push(platform);
|
|
294
|
+
wasTracked = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const removedDirs = [...dirsToDelete];
|
|
298
|
+
for (const dir of removedDirs) {
|
|
299
|
+
await context.fs.removeDirAll(dir);
|
|
300
|
+
}
|
|
301
|
+
const sourceName = `bundled:${this.tool.name}`;
|
|
302
|
+
let sourceUnregistered = false;
|
|
303
|
+
try {
|
|
304
|
+
const sources = await loadSources(context.fs, context.paths);
|
|
305
|
+
const entry = sources.sources[sourceName];
|
|
306
|
+
if (entry !== undefined) {
|
|
307
|
+
if (entry.path !== undefined && (await context.fs.exists(entry.path))) {
|
|
308
|
+
await context.fs.removeDirAll(entry.path);
|
|
309
|
+
}
|
|
310
|
+
await mutateSources(context.fs, context.paths, (mutableSources) => {
|
|
311
|
+
delete mutableSources.sources[sourceName];
|
|
312
|
+
});
|
|
313
|
+
sourceUnregistered = true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
sourceUnregistered = false;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
tool_name: this.tool.name,
|
|
321
|
+
scope,
|
|
322
|
+
removed_dirs: removedDirs,
|
|
323
|
+
platforms_cleared: platformsCleared,
|
|
324
|
+
source_unregistered: sourceUnregistered,
|
|
325
|
+
was_on_disk: removedDirs.length > 0,
|
|
326
|
+
was_tracked: wasTracked,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async registerManagedSource(files, context) {
|
|
330
|
+
const config = await loadConfig(context.fs, context.paths);
|
|
331
|
+
const materializedDir = path.join(resolveArtifactHome(config, context.paths), "skills", this.tool.name);
|
|
332
|
+
await writeSkillFiles(materializedDir, files, context.fs);
|
|
333
|
+
const sourceName = `bundled:${this.tool.name}`;
|
|
334
|
+
const now = formatTimestamp(context.clock.now());
|
|
335
|
+
await mutateSources(context.fs, context.paths, (sources) => {
|
|
336
|
+
if (sources.sources[sourceName] === undefined) {
|
|
337
|
+
const entry = {
|
|
338
|
+
type: "local",
|
|
339
|
+
path: materializedDir,
|
|
340
|
+
last_updated: now,
|
|
341
|
+
};
|
|
342
|
+
sources.sources[sourceName] = entry;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const scopeFromPlan = (scope) => scope;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Filesystem } from "./gateway.js";
|
|
2
|
+
import type { ConfigPaths } from "./paths.js";
|
|
3
|
+
import { type Platform } from "./platform.js";
|
|
4
|
+
import type { ArtifactKind, InstallScope } from "./types.js";
|
|
5
|
+
export interface TargetResolutionContext {
|
|
6
|
+
fs: Filesystem;
|
|
7
|
+
paths: ConfigPaths;
|
|
8
|
+
}
|
|
9
|
+
export declare const resolveTargets: (selector: Platform | undefined, kind: ArtifactKind, scope: InstallScope, context: TargetResolutionContext) => Promise<Platform[]>;
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { managedPlatforms } from "./config.js";
|
|
2
|
+
import { loadLockFile } from "./lockfile.js";
|
|
3
|
+
import { PLATFORM_VALUES, supportsArtifact } from "./platform.js";
|
|
4
|
+
export const resolveTargets = async (selector, kind, scope, context) => {
|
|
5
|
+
if (selector !== undefined) {
|
|
6
|
+
return [selector];
|
|
7
|
+
}
|
|
8
|
+
const managed = await managedPlatforms(context.fs, context.paths);
|
|
9
|
+
if (managed !== undefined) {
|
|
10
|
+
return managed.filter((platform) => supportsArtifact(platform, kind));
|
|
11
|
+
}
|
|
12
|
+
const targets = [];
|
|
13
|
+
for (const platform of PLATFORM_VALUES) {
|
|
14
|
+
if (!supportsArtifact(platform, kind)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const lockFile = await loadLockFile(scope, context.fs, context.paths.withPlatform(platform));
|
|
18
|
+
if (Object.keys(lockFile.packages).length > 0) {
|
|
19
|
+
targets.push(platform);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return targets.length > 0 ? targets : ["claude"];
|
|
23
|
+
};
|