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/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# cmx-core (TypeScript)
|
|
2
|
+
|
|
3
|
+
Native Bun/TypeScript port of the `cmx-core` skill-install surface. Published to
|
|
4
|
+
npm as `cmx-core`; the source lives in the `cmx-core-ts/` directory of the
|
|
5
|
+
[context-mixer2](https://github.com/svetzal/context-mixer2) repo, alongside the
|
|
6
|
+
Rust reference and the shared conformance fixtures.
|
|
7
|
+
|
|
8
|
+
It exposes the same embeddable shape as the Rust library:
|
|
9
|
+
|
|
10
|
+
- `ToolIdentity`
|
|
11
|
+
- `BundledSkill`
|
|
12
|
+
- `SkillInstaller`
|
|
13
|
+
- `ConfigPaths`
|
|
14
|
+
- `NodeFilesystem`
|
|
15
|
+
- `SystemClock`
|
|
16
|
+
|
|
17
|
+
The library scope is bundled skill installation only. It implements:
|
|
18
|
+
|
|
19
|
+
- `plan`
|
|
20
|
+
- `apply`
|
|
21
|
+
- `status`
|
|
22
|
+
- `remove`
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
BundledSkill,
|
|
29
|
+
ConfigPaths,
|
|
30
|
+
NodeFilesystem,
|
|
31
|
+
SkillInstaller,
|
|
32
|
+
SystemClock,
|
|
33
|
+
ToolIdentity,
|
|
34
|
+
} from "cmx-core";
|
|
35
|
+
|
|
36
|
+
const installer = new SkillInstaller(new ToolIdentity("mytool", "1.2.0"));
|
|
37
|
+
const skill = BundledSkill.singleMd("---\nname: mytool\n---\n# My skill\n");
|
|
38
|
+
const context = {
|
|
39
|
+
fs: new NodeFilesystem(),
|
|
40
|
+
clock: new SystemClock(),
|
|
41
|
+
paths: ConfigPaths.fromEnv("claude"),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const plan = await installer.plan(skill, "global", false, context);
|
|
45
|
+
const report = await installer.apply(skill, plan, context);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Conformance
|
|
49
|
+
|
|
50
|
+
Run the full fixture suite from this package directory:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bun install
|
|
54
|
+
bun test
|
|
55
|
+
bunx tsc --noEmit
|
|
56
|
+
bunx biome check .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The test harness consumes the committed fixtures in `../cmx-core/conformance/` and checks:
|
|
60
|
+
|
|
61
|
+
- checksum parity
|
|
62
|
+
- byte-exact frontmatter reconciliation
|
|
63
|
+
- version-guard decisions
|
|
64
|
+
- platform paths and lock names
|
|
65
|
+
- target resolution
|
|
66
|
+
- end-to-end install behavior, tree snapshots, lock JSON values, and normalized reports
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Filesystem } from "./gateway.js";
|
|
2
|
+
export interface ChecksumEntry {
|
|
3
|
+
relPath: string;
|
|
4
|
+
bytes: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export declare const normalizeRelPath: (relPath: string) => string;
|
|
7
|
+
export declare const relPathComponents: (relPath: string) => string[];
|
|
8
|
+
export declare const relPathKey: (relPath: string) => string;
|
|
9
|
+
export declare const isCanonicalRelPath: (relPath: string) => boolean;
|
|
10
|
+
export declare const checksumInMemory: (entries: Iterable<{
|
|
11
|
+
relPath: string;
|
|
12
|
+
bytes: Uint8Array;
|
|
13
|
+
}>) => string;
|
|
14
|
+
export declare const checksumDir: (rootDir: string, fs: Filesystem) => Promise<string>;
|
package/dist/checksum.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path/posix";
|
|
3
|
+
const transientNames = new Set(["node_modules", "__pycache__", ".git", ".DS_Store"]);
|
|
4
|
+
const comparePathKeys = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
5
|
+
export const normalizeRelPath = (relPath) => relPath.replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
|
6
|
+
export const relPathComponents = (relPath) => normalizeRelPath(relPath)
|
|
7
|
+
.split("/")
|
|
8
|
+
.filter((component) => component.length > 0);
|
|
9
|
+
export const relPathKey = (relPath) => relPathComponents(relPath).join("/");
|
|
10
|
+
export const isCanonicalRelPath = (relPath) => relPathComponents(relPath).every((component) => {
|
|
11
|
+
if (component.startsWith(".")) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (transientNames.has(component)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return !component.toLowerCase().endsWith(".pyc");
|
|
18
|
+
});
|
|
19
|
+
export const checksumInMemory = (entries) => {
|
|
20
|
+
const hasher = createHash("sha256");
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
hasher.update(relPathKey(entry.relPath), "utf8");
|
|
23
|
+
hasher.update(entry.bytes);
|
|
24
|
+
}
|
|
25
|
+
return `sha256:${hasher.digest("hex")}`;
|
|
26
|
+
};
|
|
27
|
+
const collectFilesRecursive = async (rootDir, currentDir, fs) => {
|
|
28
|
+
if (!(await fs.exists(currentDir))) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const entries = await fs.listDir(currentDir);
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const absolutePath = path.join(currentDir, entry.fileName);
|
|
35
|
+
if (entry.isDirectory) {
|
|
36
|
+
files.push(...(await collectFilesRecursive(rootDir, absolutePath, fs)));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const relPath = path.relative(rootDir, absolutePath);
|
|
40
|
+
files.push({
|
|
41
|
+
relPath,
|
|
42
|
+
bytes: await fs.read(absolutePath),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
};
|
|
47
|
+
export const checksumDir = async (rootDir, fs) => {
|
|
48
|
+
const entries = await collectFilesRecursive(rootDir, rootDir, fs);
|
|
49
|
+
const canonicalEntries = entries
|
|
50
|
+
.filter((entry) => isCanonicalRelPath(entry.relPath))
|
|
51
|
+
.sort((left, right) => comparePathKeys(relPathKey(left.relPath), relPathKey(right.relPath)));
|
|
52
|
+
return checksumInMemory(canonicalEntries);
|
|
53
|
+
};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Filesystem } from "./gateway.js";
|
|
2
|
+
import type { ConfigPaths } from "./paths.js";
|
|
3
|
+
import { type Platform } from "./platform.js";
|
|
4
|
+
import { type CmxConfig, type SourceEntry, type SourcesFile } from "./types.js";
|
|
5
|
+
export declare const loadSources: (fs: Filesystem, paths: ConfigPaths) => Promise<SourcesFile>;
|
|
6
|
+
export declare const saveSources: (sources: SourcesFile, fs: Filesystem, paths: ConfigPaths) => Promise<void>;
|
|
7
|
+
export declare const mutateSources: <T>(fs: Filesystem, paths: ConfigPaths, mutator: (sources: SourcesFile) => T | Promise<T>) => Promise<T>;
|
|
8
|
+
export declare const loadConfig: (fs: Filesystem, paths: ConfigPaths) => Promise<CmxConfig>;
|
|
9
|
+
export declare const saveConfig: (config: CmxConfig, fs: Filesystem, paths: ConfigPaths) => Promise<void>;
|
|
10
|
+
export declare const managedPlatforms: (fs: Filesystem, paths: ConfigPaths) => Promise<Platform[] | undefined>;
|
|
11
|
+
export declare const managedOrAllPlatforms: (fs: Filesystem, paths: ConfigPaths) => Promise<Platform[]>;
|
|
12
|
+
export declare const resolveArtifactHome: (config: CmxConfig, paths: ConfigPaths) => string;
|
|
13
|
+
export declare const resolveLocalPath: (entry: SourceEntry) => string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadJson, saveJson } from "./json-file.js";
|
|
2
|
+
import { isPlatform, PLATFORM_VALUES } from "./platform.js";
|
|
3
|
+
import { defaultCmxConfig, defaultSourcesFile, } from "./types.js";
|
|
4
|
+
const normalizeConfig = (config) => ({
|
|
5
|
+
version: config.version ?? 1,
|
|
6
|
+
llm: config.llm ?? defaultCmxConfig().llm,
|
|
7
|
+
home: config.home,
|
|
8
|
+
external: config.external ?? [],
|
|
9
|
+
platforms: (config.platforms ?? []).filter(isPlatform),
|
|
10
|
+
});
|
|
11
|
+
const normalizeSources = (sources) => ({
|
|
12
|
+
version: sources.version ?? 1,
|
|
13
|
+
sources: sources.sources ?? {},
|
|
14
|
+
});
|
|
15
|
+
export const loadSources = async (fs, paths) => normalizeSources(await loadJson(paths.sourcesPath(), fs, defaultSourcesFile));
|
|
16
|
+
export const saveSources = async (sources, fs, paths) => {
|
|
17
|
+
await saveJson(normalizeSources(sources), paths.sourcesPath(), fs);
|
|
18
|
+
};
|
|
19
|
+
export const mutateSources = async (fs, paths, mutator) => {
|
|
20
|
+
const sources = await loadSources(fs, paths);
|
|
21
|
+
const result = await mutator(sources);
|
|
22
|
+
await saveSources(sources, fs, paths);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
export const loadConfig = async (fs, paths) => normalizeConfig(await loadJson(paths.configPath(), fs, defaultCmxConfig));
|
|
26
|
+
export const saveConfig = async (config, fs, paths) => {
|
|
27
|
+
await saveJson(normalizeConfig(config), paths.configPath(), fs);
|
|
28
|
+
};
|
|
29
|
+
export const managedPlatforms = async (fs, paths) => {
|
|
30
|
+
const config = await loadConfig(fs, paths);
|
|
31
|
+
return config.platforms.length > 0 ? [...config.platforms] : undefined;
|
|
32
|
+
};
|
|
33
|
+
export const managedOrAllPlatforms = async (fs, paths) => (await managedPlatforms(fs, paths)) ?? [...PLATFORM_VALUES];
|
|
34
|
+
export const resolveArtifactHome = (config, paths) => config.home ?? paths.defaultArtifactHome();
|
|
35
|
+
export const resolveLocalPath = (entry) => {
|
|
36
|
+
if (entry.type === "local" && entry.path !== undefined) {
|
|
37
|
+
return entry.path;
|
|
38
|
+
}
|
|
39
|
+
if (entry.type === "git" && entry.local_clone !== undefined) {
|
|
40
|
+
return entry.local_clone;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(entry.type === "local"
|
|
43
|
+
? "Local source has no path configured"
|
|
44
|
+
: "Git source has no local clone path configured");
|
|
45
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const textDecoder = new TextDecoder();
|
|
2
|
+
const textEncoder = new TextEncoder();
|
|
3
|
+
export const reconcileSkillVersion = (files, version) => files.map((file) => {
|
|
4
|
+
if (file.relPath !== "SKILL.md") {
|
|
5
|
+
return { ...file, bytes: new Uint8Array(file.bytes) };
|
|
6
|
+
}
|
|
7
|
+
const content = textDecoder.decode(file.bytes);
|
|
8
|
+
const reconciled = setMetadataVersion(content, version);
|
|
9
|
+
return {
|
|
10
|
+
relPath: file.relPath,
|
|
11
|
+
bytes: textEncoder.encode(reconciled),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
export const setMetadataVersion = (content, version) => {
|
|
15
|
+
const value = `"${version}"`;
|
|
16
|
+
const open = content.startsWith("---\n")
|
|
17
|
+
? "---\n"
|
|
18
|
+
: content.startsWith("---\r\n")
|
|
19
|
+
? "---\r\n"
|
|
20
|
+
: undefined;
|
|
21
|
+
if (open === undefined) {
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
24
|
+
const afterOpen = content.slice(open.length);
|
|
25
|
+
const fenceStart = findClosingFence(afterOpen);
|
|
26
|
+
if (fenceStart === undefined) {
|
|
27
|
+
return content;
|
|
28
|
+
}
|
|
29
|
+
const inner = afterOpen.slice(0, fenceStart);
|
|
30
|
+
const closingAndRest = afterOpen.slice(fenceStart);
|
|
31
|
+
return `${open}${reconcileInner(inner, value)}${closingAndRest}`;
|
|
32
|
+
};
|
|
33
|
+
const findClosingFence = (afterOpen) => {
|
|
34
|
+
let lineStart = 0;
|
|
35
|
+
while (lineStart <= afterOpen.length) {
|
|
36
|
+
const rest = afterOpen.slice(lineStart);
|
|
37
|
+
const newlineIndex = rest.indexOf("\n");
|
|
38
|
+
const hasNewline = newlineIndex !== -1;
|
|
39
|
+
const line = hasNewline ? rest.slice(0, newlineIndex) : rest;
|
|
40
|
+
if (line.replace(/\r$/u, "") === "---") {
|
|
41
|
+
return lineStart;
|
|
42
|
+
}
|
|
43
|
+
if (!hasNewline) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
lineStart += line.length + 1;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
const reconcileInner = (inner, value) => {
|
|
51
|
+
const lines = inner.match(/[^\n]*\n|[^\n]+/gu) ?? [];
|
|
52
|
+
const withoutTopLevelVersion = lines.filter((line) => !isTopLevelKey(line, "version"));
|
|
53
|
+
const metadataIndex = withoutTopLevelVersion.findIndex((line) => isTopLevelKey(line, "metadata"));
|
|
54
|
+
if (metadataIndex === -1) {
|
|
55
|
+
ensureTrailingNewline(withoutTopLevelVersion);
|
|
56
|
+
withoutTopLevelVersion.push("metadata:\n");
|
|
57
|
+
withoutTopLevelVersion.push(` version: ${value}\n`);
|
|
58
|
+
return withoutTopLevelVersion.join("");
|
|
59
|
+
}
|
|
60
|
+
setVersionInMetadataBlock(withoutTopLevelVersion, metadataIndex, value);
|
|
61
|
+
return withoutTopLevelVersion.join("");
|
|
62
|
+
};
|
|
63
|
+
const setVersionInMetadataBlock = (lines, metadataIndex, value) => {
|
|
64
|
+
let firstChildIndent;
|
|
65
|
+
let versionIndex;
|
|
66
|
+
for (let index = metadataIndex + 1; index < lines.length; index += 1) {
|
|
67
|
+
const line = lines[index] ?? "";
|
|
68
|
+
const trimmed = line.replace(/[\n\r]+$/u, "");
|
|
69
|
+
if (trimmed.trim().length === 0) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const indent = leadingWhitespace(trimmed);
|
|
73
|
+
if (indent.length === 0) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
firstChildIndent ??= indent;
|
|
77
|
+
if (trimmed.trimStart().startsWith("version:")) {
|
|
78
|
+
versionIndex = index;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (versionIndex !== undefined) {
|
|
83
|
+
const indent = leadingWhitespace(lines[versionIndex] ?? "");
|
|
84
|
+
lines[versionIndex] = `${indent}version: ${value}\n`;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
lines.splice(metadataIndex + 1, 0, `${firstChildIndent ?? " "}version: ${value}\n`);
|
|
88
|
+
};
|
|
89
|
+
const isTopLevelKey = (line, key) => {
|
|
90
|
+
const trimmed = line.replace(/[\n\r]+$/u, "");
|
|
91
|
+
return (!line.startsWith(" ") &&
|
|
92
|
+
!line.startsWith("\t") &&
|
|
93
|
+
trimmed.startsWith(key) &&
|
|
94
|
+
trimmed.slice(key.length).startsWith(":"));
|
|
95
|
+
};
|
|
96
|
+
const leadingWhitespace = (value) => value.match(/^[ \t]*/u)?.[0] ?? "";
|
|
97
|
+
const ensureTrailingNewline = (lines) => {
|
|
98
|
+
if (lines.length === 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const lastIndex = lines.length - 1;
|
|
102
|
+
if (!lines[lastIndex]?.endsWith("\n")) {
|
|
103
|
+
lines[lastIndex] = `${lines[lastIndex] ?? ""}\n`;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface DirEntry {
|
|
2
|
+
fileName: string;
|
|
3
|
+
isDirectory: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface Filesystem {
|
|
6
|
+
exists(path: string): Promise<boolean>;
|
|
7
|
+
read(path: string): Promise<Uint8Array>;
|
|
8
|
+
readText(path: string): Promise<string>;
|
|
9
|
+
write(path: string, content: string): Promise<void>;
|
|
10
|
+
writeBytes(path: string, content: Uint8Array): Promise<void>;
|
|
11
|
+
createDirAll(path: string): Promise<void>;
|
|
12
|
+
removeDirAll(path: string): Promise<void>;
|
|
13
|
+
rename(from: string, to: string): Promise<void>;
|
|
14
|
+
listDir(path: string): Promise<DirEntry[]>;
|
|
15
|
+
isDirectory(path: string): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
export interface Clock {
|
|
18
|
+
now(): Date;
|
|
19
|
+
}
|
|
20
|
+
export declare class SystemClock implements Clock {
|
|
21
|
+
now(): Date;
|
|
22
|
+
}
|
|
23
|
+
export declare class NodeFilesystem implements Filesystem {
|
|
24
|
+
exists(targetPath: string): Promise<boolean>;
|
|
25
|
+
read(targetPath: string): Promise<Uint8Array>;
|
|
26
|
+
readText(targetPath: string): Promise<string>;
|
|
27
|
+
write(targetPath: string, content: string): Promise<void>;
|
|
28
|
+
writeBytes(targetPath: string, content: Uint8Array): Promise<void>;
|
|
29
|
+
createDirAll(targetPath: string): Promise<void>;
|
|
30
|
+
removeDirAll(targetPath: string): Promise<void>;
|
|
31
|
+
rename(from: string, to: string): Promise<void>;
|
|
32
|
+
listDir(targetPath: string): Promise<DirEntry[]>;
|
|
33
|
+
isDirectory(targetPath: string): Promise<boolean>;
|
|
34
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { access, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
export class SystemClock {
|
|
3
|
+
now() {
|
|
4
|
+
return new Date();
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class NodeFilesystem {
|
|
8
|
+
async exists(targetPath) {
|
|
9
|
+
try {
|
|
10
|
+
await access(targetPath);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async read(targetPath) {
|
|
18
|
+
return await readFile(targetPath);
|
|
19
|
+
}
|
|
20
|
+
async readText(targetPath) {
|
|
21
|
+
return await readFile(targetPath, "utf8");
|
|
22
|
+
}
|
|
23
|
+
async write(targetPath, content) {
|
|
24
|
+
await writeFile(targetPath, content, "utf8");
|
|
25
|
+
}
|
|
26
|
+
async writeBytes(targetPath, content) {
|
|
27
|
+
await writeFile(targetPath, content);
|
|
28
|
+
}
|
|
29
|
+
async createDirAll(targetPath) {
|
|
30
|
+
await mkdir(targetPath, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
async removeDirAll(targetPath) {
|
|
33
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
async rename(from, to) {
|
|
36
|
+
await rename(from, to);
|
|
37
|
+
}
|
|
38
|
+
async listDir(targetPath) {
|
|
39
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
40
|
+
return entries.map((entry) => ({
|
|
41
|
+
fileName: entry.name,
|
|
42
|
+
isDirectory: entry.isDirectory(),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
async isDirectory(targetPath) {
|
|
46
|
+
try {
|
|
47
|
+
return (await stat(targetPath)).isDirectory();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./checksum.js";
|
|
2
|
+
export * from "./config.js";
|
|
3
|
+
export * from "./frontmatter.js";
|
|
4
|
+
export * from "./gateway.js";
|
|
5
|
+
export * from "./json-file.js";
|
|
6
|
+
export * from "./lockfile.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./platform.js";
|
|
9
|
+
export * from "./skill-fs.js";
|
|
10
|
+
export * from "./skill-installer.js";
|
|
11
|
+
export * from "./targets.js";
|
|
12
|
+
export * from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./checksum.js";
|
|
2
|
+
export * from "./config.js";
|
|
3
|
+
export * from "./frontmatter.js";
|
|
4
|
+
export * from "./gateway.js";
|
|
5
|
+
export * from "./json-file.js";
|
|
6
|
+
export * from "./lockfile.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./platform.js";
|
|
9
|
+
export * from "./skill-fs.js";
|
|
10
|
+
export * from "./skill-installer.js";
|
|
11
|
+
export * from "./targets.js";
|
|
12
|
+
export * from "./types.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Filesystem } from "./gateway.js";
|
|
2
|
+
export declare const tmpPath: (targetPath: string) => string;
|
|
3
|
+
export declare const loadJson: <T>(targetPath: string, fs: Filesystem, defaultValue: () => T) => Promise<T>;
|
|
4
|
+
export declare const saveJson: <T>(value: T, targetPath: string, fs: Filesystem) => Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path/posix";
|
|
2
|
+
const sortJsonValue = (value) => {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
return value.map(sortJsonValue);
|
|
5
|
+
}
|
|
6
|
+
if (value !== null && typeof value === "object") {
|
|
7
|
+
return Object.fromEntries(Object.entries(value)
|
|
8
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
9
|
+
.map(([key, nested]) => [key, sortJsonValue(nested)]));
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
export const tmpPath = (targetPath) => path.join(path.dirname(targetPath), `${path.basename(targetPath)}.tmp`);
|
|
14
|
+
export const loadJson = async (targetPath, fs, defaultValue) => {
|
|
15
|
+
if (!(await fs.exists(targetPath))) {
|
|
16
|
+
return defaultValue();
|
|
17
|
+
}
|
|
18
|
+
const content = await fs.readText(targetPath);
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new Error(`Failed to parse ${targetPath}`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const saveJson = async (value, targetPath, fs) => {
|
|
27
|
+
const parent = path.dirname(targetPath);
|
|
28
|
+
await fs.createDirAll(parent);
|
|
29
|
+
const content = `${JSON.stringify(sortJsonValue(value), null, 2)}\n`;
|
|
30
|
+
const tempPath = tmpPath(targetPath);
|
|
31
|
+
await fs.write(tempPath, content);
|
|
32
|
+
await fs.rename(tempPath, targetPath);
|
|
33
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Filesystem } from "./gateway.js";
|
|
2
|
+
import type { ConfigPaths } from "./paths.js";
|
|
3
|
+
import { type InstallScope, type LockFile } from "./types.js";
|
|
4
|
+
export declare const loadLockFileFrom: (targetPath: string, fs: Filesystem) => Promise<LockFile>;
|
|
5
|
+
export declare const saveLockFileTo: (lockFile: LockFile, targetPath: string, fs: Filesystem) => Promise<void>;
|
|
6
|
+
export declare const loadLockFile: (scope: InstallScope, fs: Filesystem, paths: ConfigPaths) => Promise<LockFile>;
|
|
7
|
+
export declare const saveLockFile: (lockFile: LockFile, scope: InstallScope, fs: Filesystem, paths: ConfigPaths) => Promise<void>;
|
|
8
|
+
export declare const mutateLockFile: <T>(scope: InstallScope, fs: Filesystem, paths: ConfigPaths, mutator: (lockFile: LockFile) => T | Promise<T>) => Promise<T>;
|
package/dist/lockfile.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { loadJson, saveJson } from "./json-file.js";
|
|
2
|
+
import { defaultLockFile } from "./types.js";
|
|
3
|
+
const normalizeLockEntry = (entry) => ({
|
|
4
|
+
type: entry.type,
|
|
5
|
+
version: entry.version,
|
|
6
|
+
installed_at: entry.installed_at,
|
|
7
|
+
source: {
|
|
8
|
+
repo: entry.source.repo,
|
|
9
|
+
path: entry.source.path,
|
|
10
|
+
},
|
|
11
|
+
source_checksum: entry.source_checksum,
|
|
12
|
+
installed_checksum: entry.installed_checksum,
|
|
13
|
+
});
|
|
14
|
+
const normalizeLockFile = (lockFile) => ({
|
|
15
|
+
version: lockFile.version ?? 1,
|
|
16
|
+
packages: Object.fromEntries(Object.entries(lockFile.packages ?? {})
|
|
17
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
18
|
+
.map(([name, entry]) => [name, normalizeLockEntry(entry)])),
|
|
19
|
+
});
|
|
20
|
+
export const loadLockFileFrom = async (targetPath, fs) => normalizeLockFile(await loadJson(targetPath, fs, defaultLockFile));
|
|
21
|
+
export const saveLockFileTo = async (lockFile, targetPath, fs) => {
|
|
22
|
+
await saveJson(normalizeLockFile(lockFile), targetPath, fs);
|
|
23
|
+
};
|
|
24
|
+
export const loadLockFile = async (scope, fs, paths) => await loadLockFileFrom(paths.lockPath(scope), fs);
|
|
25
|
+
export const saveLockFile = async (lockFile, scope, fs, paths) => {
|
|
26
|
+
await saveLockFileTo(lockFile, paths.lockPath(scope), fs);
|
|
27
|
+
};
|
|
28
|
+
export const mutateLockFile = async (scope, fs, paths, mutator) => {
|
|
29
|
+
const lockFile = await loadLockFile(scope, fs, paths);
|
|
30
|
+
const result = await mutator(lockFile);
|
|
31
|
+
await saveLockFile(lockFile, scope, fs, paths);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Platform } from "./platform.js";
|
|
2
|
+
import { type ArtifactKind, type InstallScope } from "./types.js";
|
|
3
|
+
export declare class ConfigPaths {
|
|
4
|
+
readonly configDir: string;
|
|
5
|
+
readonly homeDir: string;
|
|
6
|
+
readonly platform: Platform;
|
|
7
|
+
readonly projectRoot: string;
|
|
8
|
+
constructor(args: {
|
|
9
|
+
configDir: string;
|
|
10
|
+
homeDir: string;
|
|
11
|
+
platform: Platform;
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
});
|
|
14
|
+
static fromEnv(platform: Platform, projectRoot?: string): ConfigPaths;
|
|
15
|
+
withPlatform(platform: Platform): ConfigPaths;
|
|
16
|
+
sourcesPath(): string;
|
|
17
|
+
gitClonesDir(): string;
|
|
18
|
+
configPath(): string;
|
|
19
|
+
defaultArtifactHome(): string;
|
|
20
|
+
setsPath(scope: InstallScope): string;
|
|
21
|
+
lockPath(scope: InstallScope): string;
|
|
22
|
+
installDir(kind: ArtifactKind, scope: InstallScope): string | null;
|
|
23
|
+
installedArtifactPath(kind: ArtifactKind, name: string, scope: InstallScope): string | null;
|
|
24
|
+
requireInstallDir(kind: ArtifactKind, scope: InstallScope): string;
|
|
25
|
+
ensureSupports(kind: ArtifactKind): void;
|
|
26
|
+
}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path/posix";
|
|
3
|
+
import { agentExtension, lockFileName, platformInstallSubpath, supportsArtifact, } from "./platform.js";
|
|
4
|
+
import { installedArtifactPath } from "./types.js";
|
|
5
|
+
export class ConfigPaths {
|
|
6
|
+
configDir;
|
|
7
|
+
homeDir;
|
|
8
|
+
platform;
|
|
9
|
+
projectRoot;
|
|
10
|
+
constructor(args) {
|
|
11
|
+
this.configDir = args.configDir;
|
|
12
|
+
this.homeDir = args.homeDir;
|
|
13
|
+
this.platform = args.platform;
|
|
14
|
+
this.projectRoot = args.projectRoot;
|
|
15
|
+
}
|
|
16
|
+
static fromEnv(platform, projectRoot = process.cwd()) {
|
|
17
|
+
const homeDir = os.homedir().replaceAll("\\", "/");
|
|
18
|
+
return new ConfigPaths({
|
|
19
|
+
configDir: path.join(homeDir, ".config", "context-mixer"),
|
|
20
|
+
homeDir,
|
|
21
|
+
platform,
|
|
22
|
+
projectRoot: projectRoot.replaceAll("\\", "/"),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
withPlatform(platform) {
|
|
26
|
+
return new ConfigPaths({
|
|
27
|
+
configDir: this.configDir,
|
|
28
|
+
homeDir: this.homeDir,
|
|
29
|
+
platform,
|
|
30
|
+
projectRoot: this.projectRoot,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
sourcesPath() {
|
|
34
|
+
return path.join(this.configDir, "sources.json");
|
|
35
|
+
}
|
|
36
|
+
gitClonesDir() {
|
|
37
|
+
return path.join(this.configDir, "sources");
|
|
38
|
+
}
|
|
39
|
+
configPath() {
|
|
40
|
+
return path.join(this.configDir, "config.json");
|
|
41
|
+
}
|
|
42
|
+
defaultArtifactHome() {
|
|
43
|
+
return path.join(this.configDir, "home");
|
|
44
|
+
}
|
|
45
|
+
setsPath(scope) {
|
|
46
|
+
return scope === "local"
|
|
47
|
+
? path.join(this.projectRoot, ".context-mixer", "sets.json")
|
|
48
|
+
: path.join(this.configDir, "sets.json");
|
|
49
|
+
}
|
|
50
|
+
lockPath(scope) {
|
|
51
|
+
const fileName = lockFileName(this.platform);
|
|
52
|
+
return scope === "local"
|
|
53
|
+
? path.join(this.projectRoot, ".context-mixer", fileName)
|
|
54
|
+
: path.join(this.configDir, fileName);
|
|
55
|
+
}
|
|
56
|
+
installDir(kind, scope) {
|
|
57
|
+
const subpath = platformInstallSubpath(this.platform, kind, scope);
|
|
58
|
+
if (subpath === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return scope === "local"
|
|
62
|
+
? path.join(this.projectRoot, subpath)
|
|
63
|
+
: path.join(this.homeDir, subpath);
|
|
64
|
+
}
|
|
65
|
+
installedArtifactPath(kind, name, scope) {
|
|
66
|
+
const installDir = this.installDir(kind, scope);
|
|
67
|
+
if (installDir === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return installedArtifactPath(kind, name, installDir, agentExtension(this.platform));
|
|
71
|
+
}
|
|
72
|
+
requireInstallDir(kind, scope) {
|
|
73
|
+
const installDir = this.installDir(kind, scope);
|
|
74
|
+
if (installDir === null) {
|
|
75
|
+
throw new Error(`The ${this.platform} platform does not support ${kind}s. ${this.platform} has no native ${kind} concept.`);
|
|
76
|
+
}
|
|
77
|
+
return installDir;
|
|
78
|
+
}
|
|
79
|
+
ensureSupports(kind) {
|
|
80
|
+
if (!supportsArtifact(this.platform, kind)) {
|
|
81
|
+
throw new Error(`The ${this.platform} platform does not support ${kind}s. ${this.platform} has no native ${kind} concept.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ArtifactKind, InstallScope } from "./types.js";
|
|
2
|
+
export declare const PLATFORM_VALUES: readonly ["claude", "copilot", "cursor", "windsurf", "gemini", "opencode", "codex", "pi", "crush", "amp", "zed", "openhands", "hermes", "devin"];
|
|
3
|
+
export type Platform = (typeof PLATFORM_VALUES)[number];
|
|
4
|
+
export declare const isPlatform: (value: string) => value is Platform;
|
|
5
|
+
export declare const platformSlug: (platform: Platform) => string;
|
|
6
|
+
export declare const platformInstallSubpath: (platform: Platform, kind: ArtifactKind, scope: InstallScope) => string | null;
|
|
7
|
+
export declare const supportsArtifact: (platform: Platform, kind: ArtifactKind) => boolean;
|
|
8
|
+
export declare const agentExtension: (platform: Platform) => "md" | "toml";
|
|
9
|
+
export declare const lockFileName: (platform: Platform) => string;
|
|
10
|
+
export declare const platformsLabel: (platforms: readonly Platform[]) => string;
|