@moda-ai/cli 1.10.2 → 1.12.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/dist/agent-schemas.js +60 -3
- package/dist/auth.js +5 -1
- package/dist/cli.js +124 -38
- package/dist/config.js +5 -3
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +40 -0
- package/dist/exit-funnel.d.ts +14 -0
- package/dist/exit-funnel.js +49 -0
- package/dist/harness.js +18 -6
- package/dist/init/index.js +57 -4
- package/dist/init/sdk-guide.d.ts +28 -1
- package/dist/init/sdk-guide.js +102 -98
- package/dist/init/sdk-integration.d.ts +23 -3
- package/dist/init/sdk-integration.js +106 -24
- package/dist/init/skill-resolver.d.ts +19 -0
- package/dist/init/skill-resolver.js +78 -0
- package/dist/init/tenant.d.ts +6 -1
- package/dist/init/tenant.js +35 -11
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js +111 -0
- package/dist/output.d.ts +44 -4
- package/dist/output.js +337 -32
- package/dist/prompts.d.ts +2 -1
- package/dist/prompts.js +38 -13
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +81 -0
- package/dist/schemas.d.ts +2 -2
- package/dist/skills-bundled.d.ts +35 -0
- package/dist/skills-bundled.js +157 -0
- package/dist/skills-manifest.d.ts +33 -0
- package/dist/skills-manifest.js +244 -0
- package/dist/skills.d.ts +8 -2
- package/dist/skills.js +55 -161
- package/package.json +6 -4
- package/skills/integration/index.json +123 -0
- package/skills/integration/integration-node-anthropic/EXAMPLE.md +59 -0
- package/skills/integration/integration-node-anthropic/SKILL.md +150 -0
- package/skills/integration/integration-node-generic/EXAMPLE.md +77 -0
- package/skills/integration/integration-node-generic/SKILL.md +170 -0
- package/skills/integration/integration-node-langchain/EXAMPLE.md +59 -0
- package/skills/integration/integration-node-langchain/SKILL.md +144 -0
- package/skills/integration/integration-node-openai/EXAMPLE.md +55 -0
- package/skills/integration/integration-node-openai/SKILL.md +148 -0
- package/skills/integration/integration-node-vercel-ai-sdk/EXAMPLE.md +55 -0
- package/skills/integration/integration-node-vercel-ai-sdk/SKILL.md +179 -0
- package/skills/integration/integration-node-vercel-ai-sdk/references/prompt-attribution.md +31 -0
- package/skills/integration/integration-python-anthropic/EXAMPLE.md +66 -0
- package/skills/integration/integration-python-anthropic/SKILL.md +153 -0
- package/skills/integration/integration-python-claude-agent-sdk/EXAMPLE.md +61 -0
- package/skills/integration/integration-python-claude-agent-sdk/SKILL.md +129 -0
- package/skills/integration/integration-python-generic/EXAMPLE.md +46 -0
- package/skills/integration/integration-python-generic/SKILL.md +146 -0
- package/skills/integration/integration-python-langchain/EXAMPLE.md +63 -0
- package/skills/integration/integration-python-langchain/SKILL.md +141 -0
- package/skills/integration/integration-python-openai/EXAMPLE.md +65 -0
- package/skills/integration/integration-python-openai/SKILL.md +151 -0
- package/skills/moda-cli/SKILL.md +51 -7
- package/dist/init/detect.d.ts +0 -9
- package/dist/init/detect.js +0 -179
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface BundledSkillIndexEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
version: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
targets: {
|
|
6
|
+
language: string;
|
|
7
|
+
frameworks: string[];
|
|
8
|
+
providers: string[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface BundledSkillIndex {
|
|
12
|
+
schema_version: string;
|
|
13
|
+
bundled_at: string;
|
|
14
|
+
cli_version: string;
|
|
15
|
+
skills: BundledSkillIndexEntry[];
|
|
16
|
+
}
|
|
17
|
+
export interface IntegrationSkillInstallResult {
|
|
18
|
+
skillId: string;
|
|
19
|
+
status: 'installed' | 'unchanged';
|
|
20
|
+
paths: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Directory holding the bundled skills (overridable for tests). */
|
|
23
|
+
export declare function bundledSkillsDir(): string;
|
|
24
|
+
/** Read the bundled skill menu, or null when no bundle is present. */
|
|
25
|
+
export declare function readBundledIndex(): BundledSkillIndex | null;
|
|
26
|
+
/** Read a bundled skill's SKILL.md, or null when it is not bundled. */
|
|
27
|
+
export declare function readBundledSkillMd(skillId: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Materialize a bundled integration skill into the project.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: unchanged files are left untouched, and a second install with
|
|
32
|
+
* identical content reports status 'unchanged' without writing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function installIntegrationSkill(skillId: string, cwd?: string): IntegrationSkillInstallResult;
|
|
35
|
+
//# sourceMappingURL=skills-bundled.d.ts.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// src/skills-bundled.ts — read and materialize the integration skills bundled
|
|
2
|
+
// inside the npm package (skills/integration/, generated at build time by
|
|
3
|
+
// scripts/bundle-skills.ts from packages/integration-skills/).
|
|
4
|
+
//
|
|
5
|
+
// Materialization copies the full skill directory (SKILL.md, EXAMPLE.md,
|
|
6
|
+
// references/) into the project's .claude/skills/<id>/, translates SKILL.md
|
|
7
|
+
// into a Cursor rule when the project uses Cursor, and records the install in
|
|
8
|
+
// .moda/skills.yml with source: integration-bundled so `skills sync` never
|
|
9
|
+
// pushes these files and harness release changes never prune them.
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join, relative } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { claudeSkillToCursorRule } from './init/rules.js';
|
|
15
|
+
import { detectAgents } from './init/mcp.js';
|
|
16
|
+
import { readSkillsManifest, SKILL_SOURCE_INTEGRATION_BUNDLED, upsertSkillManifestRecord, } from './skills-manifest.js';
|
|
17
|
+
// Resolved relative to this module so it works from src/ in dev and from the
|
|
18
|
+
// published dist/ inside node_modules (same pattern as src/init/rules.ts).
|
|
19
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, 'skills', 'integration');
|
|
21
|
+
/** Directory holding the bundled skills (overridable for tests). */
|
|
22
|
+
export function bundledSkillsDir() {
|
|
23
|
+
return process.env.MODA_BUNDLED_SKILLS_DIR || BUNDLED_SKILLS_DIR;
|
|
24
|
+
}
|
|
25
|
+
/** Read the bundled skill menu, or null when no bundle is present. */
|
|
26
|
+
export function readBundledIndex() {
|
|
27
|
+
const indexPath = join(bundledSkillsDir(), 'index.json');
|
|
28
|
+
if (!existsSync(indexPath))
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
32
|
+
if (!Array.isArray(parsed.skills))
|
|
33
|
+
return null;
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Read a bundled skill's SKILL.md, or null when it is not bundled. */
|
|
41
|
+
export function readBundledSkillMd(skillId) {
|
|
42
|
+
const path = join(bundledSkillsDir(), skillId, 'SKILL.md');
|
|
43
|
+
if (!existsSync(path))
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
return readFileSync(path, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Materialize a bundled integration skill into the project.
|
|
54
|
+
*
|
|
55
|
+
* Idempotent: unchanged files are left untouched, and a second install with
|
|
56
|
+
* identical content reports status 'unchanged' without writing.
|
|
57
|
+
*/
|
|
58
|
+
export function installIntegrationSkill(skillId, cwd = process.cwd()) {
|
|
59
|
+
if (!/^[a-z0-9-]+$/.test(skillId)) {
|
|
60
|
+
throw new Error(`Invalid integration skill id: ${JSON.stringify(skillId).slice(0, 80)}`);
|
|
61
|
+
}
|
|
62
|
+
const sourceDir = join(bundledSkillsDir(), skillId);
|
|
63
|
+
if (!existsSync(join(sourceDir, 'SKILL.md'))) {
|
|
64
|
+
const bundled = readBundledIndex()?.skills.map((skill) => skill.id) ?? [];
|
|
65
|
+
throw new Error(`Integration skill '${skillId}' is not bundled with this CLI.${bundled.length > 0 ? ` Bundled skills: ${bundled.join(', ')}` : ''}`);
|
|
66
|
+
}
|
|
67
|
+
const files = listFilesRecursive(sourceDir).sort();
|
|
68
|
+
// Plan every write up front (skill files + optional Cursor rule) so the
|
|
69
|
+
// ownership guard can reject the whole install before touching disk.
|
|
70
|
+
const plannedWrites = [];
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const relPath = relative(sourceDir, file).replace(/\\/g, '/');
|
|
73
|
+
plannedWrites.push({
|
|
74
|
+
targetRelPath: `.claude/skills/${skillId}/${relPath}`,
|
|
75
|
+
targetPath: join(cwd, '.claude', 'skills', skillId, relPath),
|
|
76
|
+
content: readFileSync(file, 'utf8'),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Cursor translation of the entry point, when the project uses Cursor.
|
|
80
|
+
const writeCursor = detectAgents().includes('Cursor') || existsSync(join(cwd, '.cursor'));
|
|
81
|
+
if (writeCursor) {
|
|
82
|
+
const skillMd = readFileSync(join(sourceDir, 'SKILL.md'), 'utf8');
|
|
83
|
+
plannedWrites.push({
|
|
84
|
+
targetRelPath: `.cursor/rules/${skillId}.mdc`,
|
|
85
|
+
targetPath: join(cwd, '.cursor', 'rules', `${skillId}.mdc`),
|
|
86
|
+
content: claudeSkillToCursorRule(skillMd),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Ownership guard: never clobber a file Moda does not already manage. A repo
|
|
90
|
+
// that happens to carry a user-authored .claude/skills/<id>/... or
|
|
91
|
+
// .cursor/rules/<id>.mdc at one of our target paths must not be silently
|
|
92
|
+
// overwritten. Paths recorded in .moda/skills.yml (any source) are ours, so
|
|
93
|
+
// idempotent re-installs still pass; only genuinely foreign files trip this.
|
|
94
|
+
const ownedPaths = new Set((readSkillsManifest(cwd)?.records ?? []).flatMap((record) => record.paths));
|
|
95
|
+
const collisions = plannedWrites.filter((write) => existsSync(write.targetPath) && !ownedPaths.has(write.targetRelPath));
|
|
96
|
+
if (collisions.length > 0) {
|
|
97
|
+
throw new Error(`Refusing to overwrite ${collisions.length} existing file(s) not managed by Moda: ` +
|
|
98
|
+
`${collisions.map((write) => write.targetRelPath).join(', ')}. ` +
|
|
99
|
+
'Move or remove them, then re-run to install the bundled integration skill.');
|
|
100
|
+
}
|
|
101
|
+
const paths = [];
|
|
102
|
+
let wroteAny = false;
|
|
103
|
+
for (const write of plannedWrites) {
|
|
104
|
+
if (writeIfChanged(write.targetPath, write.content))
|
|
105
|
+
wroteAny = true;
|
|
106
|
+
paths.push(write.targetRelPath);
|
|
107
|
+
}
|
|
108
|
+
upsertSkillManifestRecord(cwd, {
|
|
109
|
+
skillId,
|
|
110
|
+
contentHash: hashSkillDir(sourceDir, files),
|
|
111
|
+
source: SKILL_SOURCE_INTEGRATION_BUNDLED,
|
|
112
|
+
paths,
|
|
113
|
+
});
|
|
114
|
+
return { skillId, status: wroteAny ? 'installed' : 'unchanged', paths };
|
|
115
|
+
}
|
|
116
|
+
function writeIfChanged(path, content) {
|
|
117
|
+
if (existsSync(path)) {
|
|
118
|
+
try {
|
|
119
|
+
if (readFileSync(path, 'utf8') === content)
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Unreadable existing file: fall through and rewrite.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
127
|
+
writeFileSync(path, content);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function listFilesRecursive(dir) {
|
|
131
|
+
const files = [];
|
|
132
|
+
const stack = [dir];
|
|
133
|
+
while (stack.length > 0) {
|
|
134
|
+
const current = stack.pop();
|
|
135
|
+
if (!current)
|
|
136
|
+
continue;
|
|
137
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
138
|
+
const path = join(current, entry.name);
|
|
139
|
+
if (entry.isDirectory())
|
|
140
|
+
stack.push(path);
|
|
141
|
+
else if (entry.isFile())
|
|
142
|
+
files.push(path);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return files;
|
|
146
|
+
}
|
|
147
|
+
function hashSkillDir(sourceDir, files) {
|
|
148
|
+
const hash = createHash('sha256');
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
hash.update(relative(sourceDir, file).replace(/\\/g, '/'));
|
|
151
|
+
hash.update('\0');
|
|
152
|
+
hash.update(readFileSync(file));
|
|
153
|
+
hash.update('\0');
|
|
154
|
+
}
|
|
155
|
+
return hash.digest('hex');
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=skills-bundled.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const SKILLS_MANIFEST_PATH: string;
|
|
2
|
+
export declare const SKILL_SOURCE_HARNESS = "moda";
|
|
3
|
+
export declare const SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
|
|
4
|
+
export interface SkillInstallRecord {
|
|
5
|
+
skillId: string;
|
|
6
|
+
contentHash: string;
|
|
7
|
+
source: string;
|
|
8
|
+
paths: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ManifestHeader {
|
|
11
|
+
tenantId: string;
|
|
12
|
+
releaseKey: string;
|
|
13
|
+
statusFilter: string;
|
|
14
|
+
generationRunId: string;
|
|
15
|
+
generationVersion: number;
|
|
16
|
+
releasedAt: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SkillsManifest extends ManifestHeader {
|
|
19
|
+
managedPaths: string[];
|
|
20
|
+
records: SkillInstallRecord[];
|
|
21
|
+
}
|
|
22
|
+
export declare function readSkillsManifest(cwd: string): SkillsManifest | null;
|
|
23
|
+
export declare function manifestHeaderFrom(manifest: SkillsManifest | null): ManifestHeader;
|
|
24
|
+
export declare function writeSkillsManifest(cwd: string, header: ManifestHeader, records: SkillInstallRecord[]): void;
|
|
25
|
+
/**
|
|
26
|
+
* Upsert a skill record into .moda/skills.yml, preserving the harness release
|
|
27
|
+
* header and all other records. Used by the bundled integration-skill
|
|
28
|
+
* installer so its materialized files become managed paths (excluded from
|
|
29
|
+
* `skills sync`, never pruned by harness release changes).
|
|
30
|
+
*/
|
|
31
|
+
export declare function upsertSkillManifestRecord(cwd: string, record: SkillInstallRecord): void;
|
|
32
|
+
export declare function pruneManagedSkillPaths(cwd: string, managedPaths: string[], log: (msg: string) => void): number;
|
|
33
|
+
//# sourceMappingURL=skills-manifest.d.ts.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// src/skills-manifest.ts — the .moda/skills.yml install manifest.
|
|
2
|
+
//
|
|
3
|
+
// The manifest records every skill file Moda manages inside a user's repo so
|
|
4
|
+
// that (a) `skills sync` never pushes Moda-managed files back up, and (b) a
|
|
5
|
+
// harness release change prunes exactly the files the previous release wrote.
|
|
6
|
+
// Two producers write records here: `skills pull` (harness-generated skills,
|
|
7
|
+
// source 'moda') and the bundled integration-skill installer
|
|
8
|
+
// (src/skills-bundled.ts, source 'integration-bundled').
|
|
9
|
+
//
|
|
10
|
+
// IMPORTANT: the YAML here is hand-rolled on both sides — writeSkillsManifest
|
|
11
|
+
// emits a fixed 2/4/6-space layout and readSkillRecords/readManagedPaths parse
|
|
12
|
+
// exactly that layout. Keep reader and writer in lockstep; they live in this
|
|
13
|
+
// module together so a format change cannot land on one side only.
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
15
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve } from 'node:path';
|
|
16
|
+
export const SKILLS_MANIFEST_PATH = join('.moda', 'skills.yml');
|
|
17
|
+
// Per-record `source` values. Harness-generated skills (pulled from the
|
|
18
|
+
// tenant store) keep the legacy value 'moda'; skills materialized from the
|
|
19
|
+
// CLI's bundled integration set use 'integration-bundled'. Pruning on a
|
|
20
|
+
// release change is scoped to harness records only, and `skills sync`
|
|
21
|
+
// excludes every managed path regardless of source.
|
|
22
|
+
export const SKILL_SOURCE_HARNESS = 'moda';
|
|
23
|
+
export const SKILL_SOURCE_INTEGRATION_BUNDLED = 'integration-bundled';
|
|
24
|
+
export function readSkillsManifest(cwd) {
|
|
25
|
+
const manifestPath = join(cwd, SKILLS_MANIFEST_PATH);
|
|
26
|
+
if (!existsSync(manifestPath))
|
|
27
|
+
return null;
|
|
28
|
+
let text = '';
|
|
29
|
+
try {
|
|
30
|
+
text = readFileSync(manifestPath, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
tenantId: readYamlScalar(text, 'tenant_id'),
|
|
37
|
+
generationRunId: readYamlScalar(text, 'generation_run_id'),
|
|
38
|
+
generationVersion: Number(readYamlScalar(text, 'generation_version') || 0),
|
|
39
|
+
releaseKey: readYamlScalar(text, 'release_key'),
|
|
40
|
+
statusFilter: readYamlScalar(text, 'status_filter'),
|
|
41
|
+
releasedAt: readYamlScalar(text, 'released_at'),
|
|
42
|
+
managedPaths: readManagedPaths(text),
|
|
43
|
+
records: readSkillRecords(text),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function manifestHeaderFrom(manifest) {
|
|
47
|
+
return {
|
|
48
|
+
tenantId: manifest?.tenantId ?? '',
|
|
49
|
+
releaseKey: manifest?.releaseKey ?? '',
|
|
50
|
+
statusFilter: manifest?.statusFilter ?? '',
|
|
51
|
+
generationRunId: manifest?.generationRunId ?? '',
|
|
52
|
+
generationVersion: manifest?.generationVersion ?? 0,
|
|
53
|
+
releasedAt: manifest?.releasedAt ?? '',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function writeSkillsManifest(cwd, header, records) {
|
|
57
|
+
const manifestPath = join(cwd, SKILLS_MANIFEST_PATH);
|
|
58
|
+
const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
|
|
59
|
+
const lines = [
|
|
60
|
+
'version: 1',
|
|
61
|
+
'source: moda',
|
|
62
|
+
`tenant_id: ${yamlQuote(header.tenantId)}`,
|
|
63
|
+
`release_key: ${yamlQuote(header.releaseKey)}`,
|
|
64
|
+
`status_filter: ${yamlQuote(header.statusFilter)}`,
|
|
65
|
+
`generation_run_id: ${yamlQuote(header.generationRunId)}`,
|
|
66
|
+
`generation_version: ${header.generationVersion}`,
|
|
67
|
+
`released_at: ${yamlQuote(header.releasedAt)}`,
|
|
68
|
+
`installed_at: ${yamlQuote(new Date().toISOString())}`,
|
|
69
|
+
'managed_paths:',
|
|
70
|
+
...managedPaths.map((path) => ` - ${yamlQuote(path)}`),
|
|
71
|
+
'skills:',
|
|
72
|
+
...records.flatMap((record) => [
|
|
73
|
+
` - skill_id: ${yamlQuote(record.skillId)}`,
|
|
74
|
+
` content_hash: ${yamlQuote(record.contentHash)}`,
|
|
75
|
+
` source: ${yamlQuote(record.source)}`,
|
|
76
|
+
' paths:',
|
|
77
|
+
...record.paths.map((path) => ` - ${yamlQuote(path)}`),
|
|
78
|
+
]),
|
|
79
|
+
'',
|
|
80
|
+
];
|
|
81
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
82
|
+
writeFileSync(manifestPath, lines.join('\n'));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Upsert a skill record into .moda/skills.yml, preserving the harness release
|
|
86
|
+
* header and all other records. Used by the bundled integration-skill
|
|
87
|
+
* installer so its materialized files become managed paths (excluded from
|
|
88
|
+
* `skills sync`, never pruned by harness release changes).
|
|
89
|
+
*/
|
|
90
|
+
export function upsertSkillManifestRecord(cwd, record) {
|
|
91
|
+
const manifest = readSkillsManifest(cwd);
|
|
92
|
+
const records = (manifest?.records ?? []).filter((existing) => !(existing.skillId === record.skillId && existing.source === record.source));
|
|
93
|
+
records.push(record);
|
|
94
|
+
writeSkillsManifest(cwd, manifestHeaderFrom(manifest), records);
|
|
95
|
+
}
|
|
96
|
+
export function pruneManagedSkillPaths(cwd, managedPaths, log) {
|
|
97
|
+
let pruned = 0;
|
|
98
|
+
for (const rawPath of [...new Set(managedPaths)]) {
|
|
99
|
+
const safePath = safeManagedRelativePath(rawPath);
|
|
100
|
+
if (!safePath)
|
|
101
|
+
continue;
|
|
102
|
+
const absolutePath = absoluteManagedPath(cwd, safePath);
|
|
103
|
+
if (!absolutePath || !existsSync(absolutePath))
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
unlinkSync(absolutePath);
|
|
107
|
+
pruned++;
|
|
108
|
+
log(` pruned ${safePath}`);
|
|
109
|
+
pruneEmptyClaudeSkillDir(cwd, safePath);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Best-effort cleanup. The following write pass can still repair files
|
|
113
|
+
// that remain writable.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return pruned;
|
|
117
|
+
}
|
|
118
|
+
function pruneEmptyClaudeSkillDir(cwd, relativePath) {
|
|
119
|
+
if (!relativePath.startsWith('.claude/skills/') || !relativePath.endsWith('/SKILL.md'))
|
|
120
|
+
return;
|
|
121
|
+
const dir = dirname(join(cwd, relativePath));
|
|
122
|
+
try {
|
|
123
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
|
124
|
+
rmdirSync(dir);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Empty-dir pruning is only cosmetic.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function absoluteManagedPath(cwd, relativePath) {
|
|
132
|
+
const abs = resolve(cwd, relativePath);
|
|
133
|
+
const root = resolve(cwd);
|
|
134
|
+
const rel = relative(root, abs);
|
|
135
|
+
if (rel.startsWith('..') || isAbsolute(rel))
|
|
136
|
+
return null;
|
|
137
|
+
return abs;
|
|
138
|
+
}
|
|
139
|
+
function safeManagedRelativePath(rawPath) {
|
|
140
|
+
if (!rawPath || rawPath.includes('\0') || isAbsolute(rawPath))
|
|
141
|
+
return null;
|
|
142
|
+
const normalized = normalize(rawPath).replace(/\\/g, '/');
|
|
143
|
+
if (normalized.startsWith('../') || normalized === '..' || normalized.includes('/../'))
|
|
144
|
+
return null;
|
|
145
|
+
const isClaudeSkill = normalized.startsWith('.claude/skills/') && normalized.endsWith('/SKILL.md');
|
|
146
|
+
const isCursorRule = normalized.startsWith('.cursor/rules/') && normalized.endsWith('.mdc');
|
|
147
|
+
return isClaudeSkill || isCursorRule ? normalized : null;
|
|
148
|
+
}
|
|
149
|
+
function readYamlScalar(text, key) {
|
|
150
|
+
const match = text.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, 'm'));
|
|
151
|
+
return match ? parseYamlScalar(match[1]) : '';
|
|
152
|
+
}
|
|
153
|
+
/** Parse the `skills:` record list from the hand-rolled manifest YAML. */
|
|
154
|
+
function readSkillRecords(text) {
|
|
155
|
+
const records = [];
|
|
156
|
+
let inSkills = false;
|
|
157
|
+
let current = null;
|
|
158
|
+
let inPaths = false;
|
|
159
|
+
for (const line of text.split(/\r?\n/)) {
|
|
160
|
+
if (line.trim() === 'skills:') {
|
|
161
|
+
inSkills = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!inSkills)
|
|
165
|
+
continue;
|
|
166
|
+
if (line.length > 0 && !line.startsWith(' '))
|
|
167
|
+
break;
|
|
168
|
+
const skillIdMatch = line.match(/^ {2}- skill_id:\s*(.*?)\s*$/);
|
|
169
|
+
if (skillIdMatch) {
|
|
170
|
+
if (current)
|
|
171
|
+
records.push(current);
|
|
172
|
+
current = {
|
|
173
|
+
skillId: parseYamlScalar(skillIdMatch[1]),
|
|
174
|
+
contentHash: '',
|
|
175
|
+
// Legacy manifests predate per-record sources; they were all pulls.
|
|
176
|
+
source: SKILL_SOURCE_HARNESS,
|
|
177
|
+
paths: [],
|
|
178
|
+
};
|
|
179
|
+
inPaths = false;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!current)
|
|
183
|
+
continue;
|
|
184
|
+
const hashMatch = line.match(/^ {4}content_hash:\s*(.*?)\s*$/);
|
|
185
|
+
if (hashMatch) {
|
|
186
|
+
current.contentHash = parseYamlScalar(hashMatch[1]);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const sourceMatch = line.match(/^ {4}source:\s*(.*?)\s*$/);
|
|
190
|
+
if (sourceMatch) {
|
|
191
|
+
current.source = parseYamlScalar(sourceMatch[1]) || SKILL_SOURCE_HARNESS;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (/^ {4}paths:\s*$/.test(line)) {
|
|
195
|
+
inPaths = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const pathMatch = inPaths ? line.match(/^ {6}-\s*(.*?)\s*$/) : null;
|
|
199
|
+
if (pathMatch) {
|
|
200
|
+
const value = parseYamlScalar(pathMatch[1]);
|
|
201
|
+
if (value)
|
|
202
|
+
current.paths.push(value);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (current)
|
|
206
|
+
records.push(current);
|
|
207
|
+
return records;
|
|
208
|
+
}
|
|
209
|
+
function readManagedPaths(text) {
|
|
210
|
+
const paths = [];
|
|
211
|
+
let inManagedPaths = false;
|
|
212
|
+
for (const line of text.split(/\r?\n/)) {
|
|
213
|
+
if (line.trim() === 'managed_paths:') {
|
|
214
|
+
inManagedPaths = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (inManagedPaths && line.length > 0 && !line.startsWith(' '))
|
|
218
|
+
break;
|
|
219
|
+
if (!inManagedPaths)
|
|
220
|
+
continue;
|
|
221
|
+
const match = line.match(/^\s*-\s*(.*?)\s*$/);
|
|
222
|
+
if (match)
|
|
223
|
+
paths.push(parseYamlScalar(match[1]));
|
|
224
|
+
}
|
|
225
|
+
return paths.filter(Boolean);
|
|
226
|
+
}
|
|
227
|
+
function parseYamlScalar(raw) {
|
|
228
|
+
const value = raw.trim();
|
|
229
|
+
if (!value)
|
|
230
|
+
return '';
|
|
231
|
+
if (value.startsWith('"')) {
|
|
232
|
+
try {
|
|
233
|
+
return String(JSON.parse(value));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return value.replace(/^"|"$/g, '');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return value.replace(/^'|'$/g, '');
|
|
240
|
+
}
|
|
241
|
+
function yamlQuote(value) {
|
|
242
|
+
return JSON.stringify(value);
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=skills-manifest.js.map
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CommandContext } from './command-context.js';
|
|
2
2
|
import type { ProfileIOOptions } from './profiles.js';
|
|
3
3
|
interface PullSummary {
|
|
4
4
|
total: number;
|
|
@@ -46,6 +46,12 @@ export declare function runSkillSync(flags?: Record<string, string>, profileOpti
|
|
|
46
46
|
/**
|
|
47
47
|
* `moda skills <subcommand>` dispatcher.
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
type LegacySkillsArgs = {
|
|
50
|
+
command: string;
|
|
51
|
+
positional: string | undefined;
|
|
52
|
+
positionals: string[];
|
|
53
|
+
flags: Record<string, string>;
|
|
54
|
+
};
|
|
55
|
+
export declare function runSkillsCommand(input: CommandContext | LegacySkillsArgs): Promise<number>;
|
|
50
56
|
export {};
|
|
51
57
|
//# sourceMappingURL=skills.d.ts.map
|