@compilr-dev/sdk 0.29.0 → 0.29.2
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/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/skills/index.d.ts +4 -2
- package/dist/skills/index.js +2 -1
- package/dist/skills/install.d.ts +95 -0
- package/dist/skills/install.js +214 -0
- package/dist/skills/loader.js +3 -0
- package/dist/skills/patch.d.ts +13 -2
- package/dist/skills/patch.js +10 -0
- package/dist/skills/types.d.ts +13 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export type { GuideEntry, ContentTopic, ContentSection, GuideToolConfig } from '
|
|
|
67
67
|
export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, createCanvasTools, createImageTools, ProjectAnchorStore, FileArtifactService, } from './platform/index.js';
|
|
68
68
|
export type { ProjectAnchorStoreConfig, FileArtifactServiceConfig, ImageToolsConfig, ImageResizer, } from './platform/index.js';
|
|
69
69
|
export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './platform/index.js';
|
|
70
|
-
export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker, SkillEligibilityContext, SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './skills/index.js';
|
|
70
|
+
export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker, InstalledFromMarker, SkillEligibilityContext, SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './skills/index.js';
|
|
71
71
|
export { RESERVED_MACRO_NAMES, isReservedMacroName, parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills, resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill as validateSkillQuality, getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readSkillScopeConfig, readSkillScopeConfigSync, writeSkillScopeConfig, getSkillBindings, resolveSkillBinding, } from './skills/index.js';
|
|
72
72
|
export type { SkillScope, SkillPromptResolution, AvailableSkillEntry, SkillSources, } from './skills/index.js';
|
|
73
73
|
export { resolveSkillPrompt, getAllAvailableSkills } from './skills/index.js';
|
|
@@ -75,7 +75,10 @@ export { buildMacroList } from './skills/index.js';
|
|
|
75
75
|
export { buildMacroInvocation } from './skills/index.js';
|
|
76
76
|
export { skillReachability, isUnreachable } from './skills/index.js';
|
|
77
77
|
export { patchSkillFrontmatter } from './skills/index.js';
|
|
78
|
+
export { isPlaceholderDescription } from './skills/index.js';
|
|
78
79
|
export { readSkillFolder, unreadSkillFiles } from './skills/index.js';
|
|
80
|
+
export { findInstallableSkills, normaliseSkillName, resolveInstallName, planInstall, describeInstallPlan, buildInstalledFromPatch, } from './skills/index.js';
|
|
81
|
+
export type { InstallCandidate, InstallPlanItem } from './skills/index.js';
|
|
79
82
|
export type { SkillFolderEntry } from './skills/index.js';
|
|
80
83
|
export type { FrontmatterPatch } from './skills/index.js';
|
|
81
84
|
export type { SkillReachability } from './skills/index.js';
|
package/dist/index.js
CHANGED
|
@@ -158,7 +158,9 @@ export { buildMacroList } from './skills/index.js';
|
|
|
158
158
|
export { buildMacroInvocation } from './skills/index.js';
|
|
159
159
|
export { skillReachability, isUnreachable } from './skills/index.js';
|
|
160
160
|
export { patchSkillFrontmatter } from './skills/index.js';
|
|
161
|
+
export { isPlaceholderDescription } from './skills/index.js';
|
|
161
162
|
export { readSkillFolder, unreadSkillFiles } from './skills/index.js';
|
|
163
|
+
export { findInstallableSkills, normaliseSkillName, resolveInstallName, planInstall, describeInstallPlan, buildInstalledFromPatch, } from './skills/index.js';
|
|
162
164
|
export { generateSkillCatalog, toCatalogEntry, createLoadSkillTool } from './skills/index.js';
|
|
163
165
|
export { platformMacros, designMacro, sketchMacro, prdMacro, refineMacro, refineItemMacro, architectureMacro, sessionNotesMacro, buildMacro, scaffoldMacro, outlineMacro, literatureReviewMacro, draftSectionMacro, peerReviewMacro, researchScaffoldMacro, businessVisionMacro, marketAnalysisMacro, competitorAnalysisMacro, financialModelMacro, pitchOutlineMacro, businessReviewMacro, brandSetupMacro, contentStrategyMacro, contentCalendarMacro, createContentMacro, contentReviewMacro, curriculumDesignMacro, lessonPlanMacro, assessmentDesignMacro, courseReviewMacro, bookOutlineMacro, characterDesignMacro, plotThreadsMacro, sceneBreakdownMacro, bookReviewMacro, } from './skills/index.js';
|
|
164
166
|
// =============================================================================
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Platform Skills — barrel export
|
|
3
3
|
*/
|
|
4
|
-
export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker } from './types.js';
|
|
4
|
+
export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker, InstalledFromMarker, } from './types.js';
|
|
5
5
|
export { RESERVED_MACRO_NAMES, isReservedMacroName } from './types.js';
|
|
6
6
|
export { parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills } from './loader.js';
|
|
7
7
|
export type { SkillEligibilityContext } from './resolver.js';
|
|
@@ -11,7 +11,7 @@ export type { SkillCatalogEntry } from './catalog.js';
|
|
|
11
11
|
export { createLoadSkillTool } from './load-tool.js';
|
|
12
12
|
export type { SkillSource } from './load-tool.js';
|
|
13
13
|
export type { SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './operations.js';
|
|
14
|
-
export { detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill, } from './operations.js';
|
|
14
|
+
export { detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill, isPlaceholderDescription, } from './operations.js';
|
|
15
15
|
export type { SkillScope } from './paths.js';
|
|
16
16
|
export { getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readScopeConfig as readSkillScopeConfig, readScopeConfigSync as readSkillScopeConfigSync, writeScopeConfig as writeSkillScopeConfig, getAllBindings as getSkillBindings, resolveBinding as resolveSkillBinding, } from './paths.js';
|
|
17
17
|
export type { SkillPromptResolution, AvailableSkillEntry, SkillSources, } from './prompt-resolver.js';
|
|
@@ -26,4 +26,6 @@ export type { SkillReachability } from './reachability.js';
|
|
|
26
26
|
export { patchSkillFrontmatter } from './patch.js';
|
|
27
27
|
export type { FrontmatterPatch } from './patch.js';
|
|
28
28
|
export { readSkillFolder, unreadSkillFiles } from './folder.js';
|
|
29
|
+
export { findInstallableSkills, normaliseSkillName, resolveInstallName, planInstall, describeInstallPlan, buildInstalledFromPatch, } from './install.js';
|
|
30
|
+
export type { InstallCandidate, InstallPlanItem } from './install.js';
|
|
29
31
|
export type { SkillFolderEntry } from './folder.js';
|
package/dist/skills/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills } from './lo
|
|
|
3
3
|
export { resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, } from './resolver.js';
|
|
4
4
|
export { generateSkillCatalog, toCatalogEntry } from './catalog.js';
|
|
5
5
|
export { createLoadSkillTool } from './load-tool.js';
|
|
6
|
-
export { detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill, } from './operations.js';
|
|
6
|
+
export { detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill, isPlaceholderDescription, } from './operations.js';
|
|
7
7
|
export { getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readScopeConfig as readSkillScopeConfig, readScopeConfigSync as readSkillScopeConfigSync, writeScopeConfig as writeSkillScopeConfig, getAllBindings as getSkillBindings, resolveBinding as resolveSkillBinding, } from './paths.js';
|
|
8
8
|
export { resolveSkillPrompt, getAllAvailableSkills } from './prompt-resolver.js';
|
|
9
9
|
export { platformMacros, designMacro, sketchMacro, prdMacro, refineMacro, refineItemMacro, architectureMacro, sessionNotesMacro, buildMacro, scaffoldMacro, outlineMacro, literatureReviewMacro, draftSectionMacro, peerReviewMacro, researchScaffoldMacro, businessVisionMacro, marketAnalysisMacro, competitorAnalysisMacro, financialModelMacro, pitchOutlineMacro, businessReviewMacro, brandSetupMacro, contentStrategyMacro, contentCalendarMacro, createContentMacro, contentReviewMacro, curriculumDesignMacro, lessonPlanMacro, assessmentDesignMacro, courseReviewMacro, bookOutlineMacro, characterDesignMacro, plotThreadsMacro, sceneBreakdownMacro, bookReviewMacro, } from './platform-macros.js';
|
|
@@ -12,3 +12,4 @@ export { buildMacroInvocation } from './macro-invocation.js';
|
|
|
12
12
|
export { skillReachability, isUnreachable } from './reachability.js';
|
|
13
13
|
export { patchSkillFrontmatter } from './patch.js';
|
|
14
14
|
export { readSkillFolder, unreadSkillFiles } from './folder.js';
|
|
15
|
+
export { findInstallableSkills, normaliseSkillName, resolveInstallName, planInstall, describeInstallPlan, buildInstalledFromPatch, } from './install.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installing a skill someone else wrote.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ COPY, NEVER LINK. Skills load from exactly two directories, so *where the folder sits is its
|
|
5
|
+
* scope*. A reference would have no scope and would break the moment the source moved. A copy also
|
|
6
|
+
* means the edits are the user's own — which is the whole point of installing rather than reading.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ NOTHING AUTO-UPDATES, and that is deliberate. A copy has no link home. We record where it came
|
|
9
|
+
* from so a host can *tell* the user the source changed; we never reach back and change their file.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is planned before a single byte is written. A source may hold dozens of skills
|
|
12
|
+
* (anthropics/skills does), two of them may want the same name, and a name may already be taken in
|
|
13
|
+
* the destination — the user sees every one of those resolutions *in advance*, as a name they can
|
|
14
|
+
* read, with the option to cancel. Silent resolution is the failure this module exists to avoid.
|
|
15
|
+
*/
|
|
16
|
+
import type { FrontmatterPatch } from './patch.js';
|
|
17
|
+
import type { InstalledFromMarker } from './types.js';
|
|
18
|
+
export interface InstallCandidate {
|
|
19
|
+
/** Folder name at the source. Not necessarily a legal skill name. */
|
|
20
|
+
folderName: string;
|
|
21
|
+
/** Absolute path of the folder holding SKILL.md. */
|
|
22
|
+
sourceDir: string;
|
|
23
|
+
/** From the file, so the selection list reads like the catalogue it will join. */
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
bodyLines: number;
|
|
27
|
+
/** Files beyond SKILL.md that will be copied along. */
|
|
28
|
+
extraFiles: number;
|
|
29
|
+
/**
|
|
30
|
+
* ⚠️ WE READ FRONTMATTER AND BODY ONLY. A skill that points at `references/` half-works and says
|
|
31
|
+
* nothing about it, so the count travels with the candidate and the host can warn before the copy
|
|
32
|
+
* rather than leaving it to a confused agent.
|
|
33
|
+
*/
|
|
34
|
+
unreadFiles: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every installable skill under `root`, depth-first to `maxDepth`.
|
|
38
|
+
*
|
|
39
|
+
* A folder holding SKILL.md IS a skill, so we never descend into one — its `references/` cannot
|
|
40
|
+
* contain skills, and a nested SKILL.md there would be part of the parent, not a sibling.
|
|
41
|
+
*
|
|
42
|
+
* Unreadable or unparseable folders are skipped rather than thrown: a source directory is someone
|
|
43
|
+
* else's, and one bad file must not make the other forty uninstallable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findInstallableSkills(root: string, maxDepth?: number): InstallCandidate[];
|
|
46
|
+
/**
|
|
47
|
+
* A folder name turned into a legal skill name: lowercase, digits and hyphens, starting with a
|
|
48
|
+
* letter. Returns null when nothing legal survives, because inventing a name for someone else's
|
|
49
|
+
* skill is worse than refusing it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function normaliseSkillName(folderName: string): string | null;
|
|
52
|
+
/**
|
|
53
|
+
* The name this skill will actually land as, given what is already there.
|
|
54
|
+
*
|
|
55
|
+
* `pdf` when free, otherwise `pdf-2`, `pdf-3`… The suffix is plain and always appended, so the
|
|
56
|
+
* result is predictable from the inputs — and it is shown to the user before anything is written,
|
|
57
|
+
* which is what makes an ugly-but-honest name better than a clever one.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveInstallName(desired: string, taken: Iterable<string>): string;
|
|
60
|
+
export interface InstallPlanItem {
|
|
61
|
+
candidate: InstallCandidate;
|
|
62
|
+
/** The folder name it will be written as. */
|
|
63
|
+
finalName: string;
|
|
64
|
+
/** Set when the source folder name was not a legal skill name. */
|
|
65
|
+
renamedFrom: string | null;
|
|
66
|
+
/** Set when `finalName` differs from the wanted name because something already holds it. */
|
|
67
|
+
collidedWith: string | null;
|
|
68
|
+
/** Why it cannot be installed at all. When set, nothing about this item is written. */
|
|
69
|
+
blocked: string | null;
|
|
70
|
+
/** The built-in name it would have shadowed, which is why it was renamed. */
|
|
71
|
+
shadowedBuiltin: string | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* What installing this selection would do, resolved in full before anything is written.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ COLLISIONS ACCUMULATE WITHIN THE BATCH. Two sources both called `pdf` collide with each other,
|
|
77
|
+
* not just with the destination — so each resolved name joins the taken set as it is decided.
|
|
78
|
+
* Getting this wrong means the second copy silently overwrites the first.
|
|
79
|
+
*/
|
|
80
|
+
export declare function planInstall(candidates: InstallCandidate[], existingNames: Iterable<string>): InstallPlanItem[];
|
|
81
|
+
/**
|
|
82
|
+
* One sentence per resolution, for showing the user before they commit.
|
|
83
|
+
*
|
|
84
|
+
* Returns [] when nothing needs saying — which is the common case, and must not render as an empty
|
|
85
|
+
* warning box.
|
|
86
|
+
*/
|
|
87
|
+
export declare function describeInstallPlan(plan: InstallPlanItem[]): string[];
|
|
88
|
+
/**
|
|
89
|
+
* The frontmatter edit that records where a skill came from.
|
|
90
|
+
*
|
|
91
|
+
* ⚠️ A PATCH, NOT A REWRITE (D-2). This runs on a file someone else wrote — their comments, key
|
|
92
|
+
* order and fields we have never modelled all have to survive being installed, or we have silently
|
|
93
|
+
* edited a stranger's skill on its way in.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildInstalledFromPatch(marker: InstalledFromMarker): FrontmatterPatch;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installing a skill someone else wrote.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ COPY, NEVER LINK. Skills load from exactly two directories, so *where the folder sits is its
|
|
5
|
+
* scope*. A reference would have no scope and would break the moment the source moved. A copy also
|
|
6
|
+
* means the edits are the user's own — which is the whole point of installing rather than reading.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ NOTHING AUTO-UPDATES, and that is deliberate. A copy has no link home. We record where it came
|
|
9
|
+
* from so a host can *tell* the user the source changed; we never reach back and change their file.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is planned before a single byte is written. A source may hold dozens of skills
|
|
12
|
+
* (anthropics/skills does), two of them may want the same name, and a name may already be taken in
|
|
13
|
+
* the destination — the user sees every one of those resolutions *in advance*, as a name they can
|
|
14
|
+
* read, with the option to cancel. Silent resolution is the failure this module exists to avoid.
|
|
15
|
+
*/
|
|
16
|
+
import { readdirSync, statSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join, basename } from 'node:path';
|
|
18
|
+
import { isValidSkillName } from './paths.js';
|
|
19
|
+
import { isReservedMacroName } from './types.js';
|
|
20
|
+
import { parseSkillMarkdown } from './loader.js';
|
|
21
|
+
import { readSkillFolder } from './folder.js';
|
|
22
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.github', 'dist', 'build', '__pycache__']);
|
|
23
|
+
/**
|
|
24
|
+
* Every installable skill under `root`, depth-first to `maxDepth`.
|
|
25
|
+
*
|
|
26
|
+
* A folder holding SKILL.md IS a skill, so we never descend into one — its `references/` cannot
|
|
27
|
+
* contain skills, and a nested SKILL.md there would be part of the parent, not a sibling.
|
|
28
|
+
*
|
|
29
|
+
* Unreadable or unparseable folders are skipped rather than thrown: a source directory is someone
|
|
30
|
+
* else's, and one bad file must not make the other forty uninstallable.
|
|
31
|
+
*/
|
|
32
|
+
export function findInstallableSkills(root, maxDepth = 3) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const visit = (dir, depth) => {
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = readdirSync(dir);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (entries.includes('SKILL.md')) {
|
|
43
|
+
const candidate = describeCandidate(dir);
|
|
44
|
+
if (candidate)
|
|
45
|
+
out.push(candidate);
|
|
46
|
+
return; // a skill is a leaf
|
|
47
|
+
}
|
|
48
|
+
if (depth >= maxDepth)
|
|
49
|
+
return;
|
|
50
|
+
for (const name of entries.sort()) {
|
|
51
|
+
if (SKIP_DIRS.has(name) || name.startsWith('.'))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
if (statSync(join(dir, name)).isDirectory())
|
|
55
|
+
visit(join(dir, name), depth + 1);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* unreadable entry — skip it, not the whole source */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
visit(root, 0);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function describeCandidate(dir) {
|
|
66
|
+
let content;
|
|
67
|
+
try {
|
|
68
|
+
content = readFileSync(join(dir, 'SKILL.md'), 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const parsed = parseSkillMarkdown(content, dir);
|
|
74
|
+
if (!parsed)
|
|
75
|
+
return null;
|
|
76
|
+
const body = parsed.prompt.trim();
|
|
77
|
+
return {
|
|
78
|
+
folderName: basename(dir),
|
|
79
|
+
sourceDir: dir,
|
|
80
|
+
name: parsed.name,
|
|
81
|
+
description: parsed.description,
|
|
82
|
+
bodyLines: body === '' ? 0 : body.split('\n').length,
|
|
83
|
+
extraFiles: readSkillFolder(dir).length,
|
|
84
|
+
unreadFiles: readSkillFolder(dir).filter((f) => f.kind === 'reference' || f.kind === 'script')
|
|
85
|
+
.length,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A folder name turned into a legal skill name: lowercase, digits and hyphens, starting with a
|
|
90
|
+
* letter. Returns null when nothing legal survives, because inventing a name for someone else's
|
|
91
|
+
* skill is worse than refusing it.
|
|
92
|
+
*/
|
|
93
|
+
export function normaliseSkillName(folderName) {
|
|
94
|
+
const slug = folderName
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
97
|
+
.replace(/^-+|-+$/g, '')
|
|
98
|
+
.slice(0, 64)
|
|
99
|
+
.replace(/-+$/, '');
|
|
100
|
+
return isValidSkillName(slug) ? slug : null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The name this skill will actually land as, given what is already there.
|
|
104
|
+
*
|
|
105
|
+
* `pdf` when free, otherwise `pdf-2`, `pdf-3`… The suffix is plain and always appended, so the
|
|
106
|
+
* result is predictable from the inputs — and it is shown to the user before anything is written,
|
|
107
|
+
* which is what makes an ugly-but-honest name better than a clever one.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveInstallName(desired, taken) {
|
|
110
|
+
const used = new Set(taken);
|
|
111
|
+
if (!used.has(desired))
|
|
112
|
+
return desired;
|
|
113
|
+
for (let n = 2; n < 1000; n++) {
|
|
114
|
+
const next = `${desired}-${String(n)}`;
|
|
115
|
+
if (!used.has(next))
|
|
116
|
+
return next;
|
|
117
|
+
}
|
|
118
|
+
return `${desired}-${String(Date.now())}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What installing this selection would do, resolved in full before anything is written.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ COLLISIONS ACCUMULATE WITHIN THE BATCH. Two sources both called `pdf` collide with each other,
|
|
124
|
+
* not just with the destination — so each resolved name joins the taken set as it is decided.
|
|
125
|
+
* Getting this wrong means the second copy silently overwrites the first.
|
|
126
|
+
*/
|
|
127
|
+
export function planInstall(candidates, existingNames) {
|
|
128
|
+
const taken = new Set(existingNames);
|
|
129
|
+
return candidates.map((candidate) => {
|
|
130
|
+
const wanted = normaliseSkillName(candidate.folderName);
|
|
131
|
+
if (wanted === null) {
|
|
132
|
+
return {
|
|
133
|
+
candidate,
|
|
134
|
+
finalName: candidate.folderName,
|
|
135
|
+
renamedFrom: null,
|
|
136
|
+
collidedWith: null,
|
|
137
|
+
blocked: `"${candidate.folderName}" cannot become a skill name — rename the folder and try again.`,
|
|
138
|
+
shadowedBuiltin: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/*
|
|
142
|
+
⚠️ A RESERVED NAME IS NOT A FREE NAME. Project and user scope outrank `sdk` in
|
|
143
|
+
resolveSkillPrompt, so installing someone's `design` would silently become /design in both
|
|
144
|
+
hosts and the collision detector would stay quiet — the same defect `skills:create` already
|
|
145
|
+
refuses. Renaming rather than blocking: the user still gets the skill, it just cannot
|
|
146
|
+
impersonate a built-in.
|
|
147
|
+
*/
|
|
148
|
+
if (isReservedMacroName(wanted)) {
|
|
149
|
+
const renamed = resolveInstallName(`${wanted}-installed`, taken);
|
|
150
|
+
taken.add(renamed);
|
|
151
|
+
return {
|
|
152
|
+
candidate,
|
|
153
|
+
finalName: renamed,
|
|
154
|
+
renamedFrom: candidate.folderName === renamed ? null : candidate.folderName,
|
|
155
|
+
collidedWith: wanted,
|
|
156
|
+
blocked: null,
|
|
157
|
+
shadowedBuiltin: wanted,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const finalName = resolveInstallName(wanted, taken);
|
|
161
|
+
taken.add(finalName);
|
|
162
|
+
return {
|
|
163
|
+
candidate,
|
|
164
|
+
finalName,
|
|
165
|
+
renamedFrom: wanted === candidate.folderName ? null : candidate.folderName,
|
|
166
|
+
collidedWith: finalName === wanted ? null : wanted,
|
|
167
|
+
blocked: null,
|
|
168
|
+
shadowedBuiltin: null,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* One sentence per resolution, for showing the user before they commit.
|
|
174
|
+
*
|
|
175
|
+
* Returns [] when nothing needs saying — which is the common case, and must not render as an empty
|
|
176
|
+
* warning box.
|
|
177
|
+
*/
|
|
178
|
+
export function describeInstallPlan(plan) {
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const item of plan) {
|
|
181
|
+
if (item.blocked) {
|
|
182
|
+
out.push(item.blocked);
|
|
183
|
+
}
|
|
184
|
+
else if (item.shadowedBuiltin) {
|
|
185
|
+
out.push(`${item.shadowedBuiltin} is a built-in — installing it under that name would silently replace it everywhere, so this will land as ${item.finalName}.`);
|
|
186
|
+
}
|
|
187
|
+
else if (item.collidedWith && item.renamedFrom) {
|
|
188
|
+
out.push(`"${item.renamedFrom}" is not a legal skill name and ${item.collidedWith} is already here — it will land as ${item.finalName}.`);
|
|
189
|
+
}
|
|
190
|
+
else if (item.collidedWith) {
|
|
191
|
+
out.push(`A skill called ${item.collidedWith} already exists here — this will land as ${item.finalName}.`);
|
|
192
|
+
}
|
|
193
|
+
else if (item.renamedFrom) {
|
|
194
|
+
out.push(`"${item.renamedFrom}" is not a legal skill name — it will land as ${item.finalName}.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The frontmatter edit that records where a skill came from.
|
|
201
|
+
*
|
|
202
|
+
* ⚠️ A PATCH, NOT A REWRITE (D-2). This runs on a file someone else wrote — their comments, key
|
|
203
|
+
* order and fields we have never modelled all have to survive being installed, or we have silently
|
|
204
|
+
* edited a stranger's skill on its way in.
|
|
205
|
+
*/
|
|
206
|
+
export function buildInstalledFromPatch(marker) {
|
|
207
|
+
return {
|
|
208
|
+
installedFrom: {
|
|
209
|
+
source: marker.source,
|
|
210
|
+
skill: marker.skill,
|
|
211
|
+
installedAt: marker.installedAt,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
package/dist/skills/loader.js
CHANGED
|
@@ -93,6 +93,9 @@ export function parseSkillMarkdown(content, sourcePath, source) {
|
|
|
93
93
|
if (meta['forkedFrom'] && typeof meta['forkedFrom'] === 'object') {
|
|
94
94
|
skill.forkedFrom = meta['forkedFrom'];
|
|
95
95
|
}
|
|
96
|
+
if (meta['installedFrom'] && typeof meta['installedFrom'] === 'object') {
|
|
97
|
+
skill.installedFrom = meta['installedFrom'];
|
|
98
|
+
}
|
|
96
99
|
return skill;
|
|
97
100
|
}
|
|
98
101
|
/**
|
package/dist/skills/patch.d.ts
CHANGED
|
@@ -14,8 +14,19 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Decision D-2: project-docs/.../compilr-dev-skills/implementation-plan.md
|
|
16
16
|
*/
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `undefined` leaves a key alone; `null` removes it; anything else sets it.
|
|
19
|
+
*
|
|
20
|
+
* A plain object becomes a nested block — the shape `forkedFrom` already has on disk:
|
|
21
|
+
*
|
|
22
|
+
* installedFrom:
|
|
23
|
+
* source: "/home/me/skills"
|
|
24
|
+
* skill: "pdf"
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ NOT A ONE-LINE FLOW MAPPING. `{ a: 1 }` passed as a string would sail past the 80-character
|
|
27
|
+
* threshold below, come back out as a block scalar, and parse as a STRING that reads like a map.
|
|
28
|
+
*/
|
|
29
|
+
export type FrontmatterPatch = Record<string, string | number | boolean | Record<string, string> | null | undefined>;
|
|
19
30
|
/**
|
|
20
31
|
* Apply `patch` to `content`'s frontmatter, leaving everything else byte-identical.
|
|
21
32
|
*
|
package/dist/skills/patch.js
CHANGED
|
@@ -51,7 +51,17 @@ function keyBlockEnd(lines, start) {
|
|
|
51
51
|
end--;
|
|
52
52
|
return end;
|
|
53
53
|
}
|
|
54
|
+
/** YAML double-quoted scalar. Quoted always, so a value starting with `/` or `{` stays a string. */
|
|
55
|
+
function quote(value) {
|
|
56
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
57
|
+
}
|
|
54
58
|
function render(key, value) {
|
|
59
|
+
if (typeof value === 'object') {
|
|
60
|
+
const body = Object.entries(value)
|
|
61
|
+
.map(([k, v]) => ` ${k}: ${quote(v)}`)
|
|
62
|
+
.join('\n');
|
|
63
|
+
return body === '' ? `${key}: {}` : `${key}:\n${body}`;
|
|
64
|
+
}
|
|
55
65
|
if (typeof value === 'string' && (value.includes('\n') || value.length > 80)) {
|
|
56
66
|
const body = value
|
|
57
67
|
.split('\n')
|
package/dist/skills/types.d.ts
CHANGED
|
@@ -45,6 +45,17 @@ export interface ForkedFromMarker {
|
|
|
45
45
|
version: string;
|
|
46
46
|
forkedAt: string;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Where an installed skill came from. Recorded on the way in, never used to fetch: nothing
|
|
50
|
+
* auto-updates, so this exists so a host can TELL the user the source moved on.
|
|
51
|
+
*/
|
|
52
|
+
export interface InstalledFromMarker {
|
|
53
|
+
/** Verbatim: the folder path or repo URL the user gave. */
|
|
54
|
+
source: string;
|
|
55
|
+
/** The folder name at the source, which differs from the installed name after a collision. */
|
|
56
|
+
skill: string;
|
|
57
|
+
installedAt: string;
|
|
58
|
+
}
|
|
48
59
|
/**
|
|
49
60
|
* Custom skill — matches Anthropic spec + compilr extensions.
|
|
50
61
|
*
|
|
@@ -96,6 +107,8 @@ export interface CustomSkill {
|
|
|
96
107
|
compilr?: CompilrSkillExtension;
|
|
97
108
|
/** Set when this skill was created via /skill fork. */
|
|
98
109
|
forkedFrom?: ForkedFromMarker;
|
|
110
|
+
/** Present only on skills installed from someone else's folder or repo. */
|
|
111
|
+
installedFrom?: InstalledFromMarker;
|
|
99
112
|
/** Absolute path to the skill directory (containing SKILL.md). */
|
|
100
113
|
sourcePath?: string;
|
|
101
114
|
/** Where this skill came from. */
|