skillcaddy 0.27.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/CONTEXT.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +499 -0
  5. package/README_CN.md +482 -0
  6. package/collection-metadata/github/mattpocock.json +16 -0
  7. package/lib/canonicalJson.js +11 -0
  8. package/lib/claudeStore.js +135 -0
  9. package/lib/collectionEnablement.js +115 -0
  10. package/lib/collectionSetup.js +199 -0
  11. package/lib/enablePlan.js +116 -0
  12. package/lib/enablementScope.js +8 -0
  13. package/lib/hermesStore.js +21 -0
  14. package/lib/libraryImage.js +1785 -0
  15. package/lib/managerInstall.js +109 -0
  16. package/lib/managerSourceWorkflow.js +234 -0
  17. package/lib/projectActions.js +24 -0
  18. package/lib/projectPath.js +51 -0
  19. package/lib/roots.js +99 -0
  20. package/lib/skillLinkStore.js +68 -0
  21. package/lib/skillMetadata.js +191 -0
  22. package/lib/skillStore.js +541 -0
  23. package/lib/sourceAcquisition.js +475 -0
  24. package/lib/sourceAcquisitionAdapter.js +47 -0
  25. package/lib/sourceAcquisitionError.js +18 -0
  26. package/lib/sourceAcquisitionPolicy.js +139 -0
  27. package/lib/sourceArchive.js +363 -0
  28. package/lib/sourceArchiveWorkspace.js +36 -0
  29. package/lib/sourceGit.js +95 -0
  30. package/lib/sourceGitAcquisitionAdapter.js +145 -0
  31. package/lib/sourceGitBatchUpdate.js +168 -0
  32. package/lib/sourceGitRepair.js +227 -0
  33. package/lib/sourceGitStaging.js +139 -0
  34. package/lib/sourceGitUpgradeAdapter.js +583 -0
  35. package/lib/sourceGitUrl.js +170 -0
  36. package/lib/sourceHttp.js +302 -0
  37. package/lib/sourceLocal.js +200 -0
  38. package/lib/sourceLocalAcquisitionAdapter.js +107 -0
  39. package/lib/sourceManager.js +242 -0
  40. package/lib/sourceMigration.js +172 -0
  41. package/lib/sourceMigrationIssue.js +6 -0
  42. package/lib/sourcePath.js +6 -0
  43. package/lib/sourcePolicy.js +53 -0
  44. package/lib/sourceRecord.js +295 -0
  45. package/lib/sourceRegistry.js +155 -0
  46. package/lib/sourceRemoteArchiveAcquisitionAdapter.js +147 -0
  47. package/lib/sourceRemoteFileAcquisitionAdapter.js +95 -0
  48. package/lib/sourceRemoteFilePolicy.js +62 -0
  49. package/lib/sourceRemoteFileWorkspace.js +55 -0
  50. package/lib/sourceSkillDiscovery.js +117 -0
  51. package/lib/sourceTree.js +185 -0
  52. package/lib/sourceUpgrade.js +596 -0
  53. package/lib/sourceUpgradeAdapter.js +41 -0
  54. package/lib/sourceUpgradePolicy.js +170 -0
  55. package/lib/sourceUpgradeTransaction.js +202 -0
  56. package/lib/sourceValidation.js +120 -0
  57. package/lib/sourceWorkspace.js +37 -0
  58. package/lib/specGate.js +235 -0
  59. package/lib/tuiActions.js +169 -0
  60. package/lib/tuiInstall.js +133 -0
  61. package/lib/tuiLayout.js +94 -0
  62. package/lib/version.js +17 -0
  63. package/lib/webManager.js +370 -0
  64. package/package.json +74 -0
  65. package/public/agentsUi.js +138 -0
  66. package/public/app.js +1431 -0
  67. package/public/claude.css +2 -0
  68. package/public/claudeUi.js +119 -0
  69. package/public/emptyState.js +12 -0
  70. package/public/enabled.css +303 -0
  71. package/public/favicon.svg +5 -0
  72. package/public/index.html +199 -0
  73. package/public/skillcaddy_CN.png +0 -0
  74. package/public/skillcaddy_EN.png +0 -0
  75. package/public/styles.css +1611 -0
  76. package/scripts/cli.js +647 -0
  77. package/scripts/cron-sync-update-git.sh +128 -0
  78. package/scripts/pull-github.sh +22 -0
  79. package/scripts/source.js +638 -0
  80. package/scripts/tui.js +1058 -0
  81. package/server.js +196 -0
  82. package/skills/skillcaddy-manager/SKILL.md +93 -0
  83. package/skills/skillcaddy-manager/agents/openai.yaml +7 -0
  84. package/skills/skillcaddy-manager/references/METADATA.md +75 -0
  85. package/skills/skillcaddy-manager/references/OPERATIONS.md +315 -0
  86. package/skills/skillcaddy-manager/references/RECOMMENDATION_GUIDE.md +51 -0
  87. package/skills/skillcaddy-manager/references/featured-skills.json +564 -0
  88. package/skills/skillcaddy-manager/references/skill-platforms.json +48 -0
  89. package/skills/skillcaddy-manager/scripts/check-conflicts.cjs +117 -0
  90. package/skills/skillcaddy-manager/scripts/check-global-skills.cjs +169 -0
  91. package/skills/skillcaddy-manager/scripts/translate-skill-notes.cjs +297 -0
  92. package/skills/skillcaddy-manager/scripts/version-manager.cjs +146 -0
  93. package/skills/skillcaddy-manager/scripts/view-recommendations.cjs +279 -0
  94. package/skills/skillcaddy-manager/skillcaddy.json +9 -0
  95. package/skills/spec-pipeline/spec-direction-gate/SKILL.md +119 -0
  96. package/skills/spec-pipeline/spec-direction-gate/check-approval.py +110 -0
  97. package/skills/spec-pipeline/spec-draft-intake/SKILL.md +116 -0
  98. package/skills/spec-pipeline/spec-role-contract/SKILL.md +113 -0
  99. package/skills/spec-pipeline/spec-role-contract/check-roles.py +108 -0
@@ -0,0 +1,170 @@
1
+ import path from 'node:path';
2
+ import { canonicalJson } from './canonicalJson.js';
3
+ import { SourceAcquisitionError } from './sourceAcquisitionError.js';
4
+ import {
5
+ checksumDirectory,
6
+ sourcePathExists
7
+ } from './sourceTree.js';
8
+ import { scanEnabledSkills, scanGlobalSkills } from './skillStore.js';
9
+ import { scanHermesSkills } from './hermesStore.js';
10
+
11
+ export function fingerprintSourceUpgradePlan(plan) {
12
+ return canonicalJson({
13
+ operation: plan.operation,
14
+ status: plan.status,
15
+ sourceId: plan.sourceId,
16
+ installPath: plan.installPath,
17
+ input: plan.input,
18
+ origin: plan.origin,
19
+ integrity: plan.integrity,
20
+ currentCommit: plan.currentCommit,
21
+ incomingCommit: plan.incomingCommit,
22
+ skills: plan.skills,
23
+ warnings: plan.warnings,
24
+ skillChanges: plan.skillChanges,
25
+ changes: plan.changes,
26
+ affectedProjectLinks: plan.affectedProjectLinks,
27
+ affectedGlobalLinks: plan.affectedGlobalLinks,
28
+ affectedHermesLinks: plan.affectedHermesLinks
29
+ });
30
+ }
31
+
32
+ export function assertSameSourceUpgradePlan(
33
+ originalFingerprint,
34
+ plan,
35
+ message
36
+ ) {
37
+ if (originalFingerprint !== fingerprintSourceUpgradePlan(plan)) {
38
+ throw new SourceAcquisitionError('stale-plan', message);
39
+ }
40
+ }
41
+
42
+ export function assertNoAffectedProjectLinks(affectedProjectLinks) {
43
+ return assertNoAffectedLinks(affectedProjectLinks, []);
44
+ }
45
+
46
+ export function assertNoAffectedLinks(
47
+ affectedProjectLinks,
48
+ affectedGlobalLinks = [],
49
+ affectedHermesLinks = []
50
+ ) {
51
+ if (
52
+ affectedProjectLinks.length === 0 &&
53
+ affectedGlobalLinks.length === 0 &&
54
+ affectedHermesLinks.length === 0
55
+ ) return;
56
+ const projectAliases = affectedProjectLinks.map((link) => link.alias).join(', ');
57
+ const globalAliases = affectedGlobalLinks.map((link) => link.alias).join(', ');
58
+ const hermesAliases = affectedHermesLinks.map((link) => link.alias).join(', ');
59
+ const details = [];
60
+ if (affectedProjectLinks.length > 0) details.push(`current-project links: ${projectAliases}`);
61
+ if (affectedGlobalLinks.length > 0) details.push(`global links: ${globalAliases}`);
62
+ if (affectedHermesLinks.length > 0) details.push(`Hermes links: ${hermesAliases}`);
63
+ const error = new SourceAcquisitionError(
64
+ 'breaking-replacement',
65
+ `Source replacement would break ${details.join('; ')}. Re-run with --allow-breaking to authorize it.`,
66
+ 4
67
+ );
68
+ error.affectedProjectLinks = affectedProjectLinks.map((link) => ({
69
+ alias: link.alias,
70
+ skillPath: link.skillPath
71
+ }));
72
+ error.affectedGlobalLinks = affectedGlobalLinks.map((link) => ({
73
+ alias: link.alias,
74
+ skillPath: link.skillPath
75
+ }));
76
+ error.affectedHermesLinks = affectedHermesLinks.map((link) => ({
77
+ alias: link.alias,
78
+ skillPath: link.skillPath
79
+ }));
80
+ throw error;
81
+ }
82
+
83
+ export async function findAffectedProjectLinks({
84
+ rootDir,
85
+ projectPath,
86
+ installPath,
87
+ removedSkillPaths
88
+ }) {
89
+ if (!projectPath || removedSkillPaths.length === 0) return [];
90
+ const removedByTarget = new Map(removedSkillPaths.map((skillPath) => [
91
+ path.resolve(rootDir, installPath, skillPath),
92
+ skillPath
93
+ ]));
94
+ const links = await scanEnabledSkills(projectPath);
95
+ return links
96
+ .filter((link) => link.isSymlink && removedByTarget.has(link.targetPath))
97
+ .map((link) => ({
98
+ alias: link.alias,
99
+ skillPath: removedByTarget.get(link.targetPath)
100
+ }));
101
+ }
102
+
103
+ export async function findAffectedGlobalLinks({
104
+ rootDir,
105
+ globalDir,
106
+ installPath,
107
+ removedSkillPaths
108
+ }) {
109
+ if (removedSkillPaths.length === 0) return [];
110
+ const removedByTarget = new Map(removedSkillPaths.map((skillPath) => [
111
+ path.resolve(rootDir, installPath, skillPath),
112
+ skillPath
113
+ ]));
114
+ const links = await scanGlobalSkills({ globalDir });
115
+ return links
116
+ .filter((link) => link.isSymlink && removedByTarget.has(link.targetPath))
117
+ .map((link) => ({
118
+ alias: link.alias,
119
+ skillPath: removedByTarget.get(link.targetPath)
120
+ }));
121
+ }
122
+
123
+ export async function findAffectedHermesLinks({
124
+ rootDir,
125
+ hermesDir,
126
+ installPath,
127
+ removedSkillPaths
128
+ }) {
129
+ if (removedSkillPaths.length === 0) return [];
130
+ const removedByTarget = new Map(removedSkillPaths.map((skillPath) => [
131
+ path.resolve(rootDir, installPath, skillPath),
132
+ skillPath
133
+ ]));
134
+ const links = await scanHermesSkills({ hermesDir });
135
+ return links
136
+ .filter((link) => link.isSymlink && removedByTarget.has(link.targetPath))
137
+ .map((link) => ({
138
+ alias: link.alias,
139
+ skillPath: removedByTarget.get(link.targetPath)
140
+ }));
141
+ }
142
+
143
+ export async function assertActiveSourceMatchesRecord(destination, record) {
144
+ if (!(await sourcePathExists(destination))) {
145
+ throw new SourceAcquisitionError(
146
+ 'source-collision',
147
+ `Registered source content is missing: ${record.sourceId}`,
148
+ 3
149
+ );
150
+ }
151
+ if (await checksumDirectory(destination) !== record.integrity?.value) {
152
+ throw new SourceAcquisitionError(
153
+ 'source-collision',
154
+ `Registered source content no longer matches its integrity: ${record.sourceId}`,
155
+ 3
156
+ );
157
+ }
158
+ }
159
+
160
+ export function classifySkillChanges(previousSkills, nextSkills) {
161
+ const previous = new Set(previousSkills);
162
+ const next = new Set(nextSkills);
163
+ return {
164
+ unchanged: previousSkills.filter((skillPath) => next.has(skillPath)),
165
+ added: nextSkills.filter((skillPath) => !previous.has(skillPath)),
166
+ removedOrRelocated: previousSkills.filter(
167
+ (skillPath) => !next.has(skillPath)
168
+ )
169
+ };
170
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Source upgrade transactions
3
+ *
4
+ * Two strategies for atomic source updates:
5
+ * - DIRECTORY_REPLACEMENT: For local, archive, and remote-file sources
6
+ * - GIT_FAST_FORWARD: For Git sources
7
+ *
8
+ * Both follow the same phase structure:
9
+ * - PREPARE: Stage content and registry entries
10
+ * - COMMIT: Make changes active
11
+ * - CLEANUP: Remove temporary artifacts (directory only)
12
+ * - ROLLBACK: Undo on failure
13
+ * - DISCARD: Clean up staging state
14
+ *
15
+ * See sourceUpgrade.js for the lifecycle orchestration.
16
+ */
17
+ import { execFile as execFileCallback } from 'node:child_process';
18
+ import {
19
+ lstat,
20
+ rename,
21
+ rm
22
+ } from 'node:fs/promises';
23
+ import path from 'node:path';
24
+ import { promisify } from 'node:util';
25
+ import { SourceAcquisitionError } from './sourceAcquisitionError.js';
26
+ import {
27
+ discardStagedSourceRecord,
28
+ publishStagedSourceRecord,
29
+ stageSourceRecord,
30
+ writeSourceRecord
31
+ } from './sourceRegistry.js';
32
+
33
+ const execFile = promisify(execFileCallback);
34
+
35
+ // === Directory Replacement Strategy ===
36
+ // For local, archive, and remote-file sources
37
+
38
+ export const defaultSourceUpdateFileOperations = { rename, rm };
39
+
40
+ export async function prepareDirectorySourceReplacement({
41
+ rootDir,
42
+ destination,
43
+ stagingRoot,
44
+ preparedContentRoot,
45
+ nextRecord,
46
+ fileOperations
47
+ }) {
48
+ const previousSource = path.join(stagingRoot, 'previous-source');
49
+ const previousRecord = path.join(stagingRoot, 'previous-record.json');
50
+ const stagedRecord = await stageSourceRecord(rootDir, nextRecord);
51
+ return {
52
+ destination,
53
+ fileOperations,
54
+ preparedContentRoot,
55
+ previousRecord,
56
+ previousSource,
57
+ stagedRecord
58
+ };
59
+ }
60
+
61
+ export async function commitDirectorySourceReplacement(state) {
62
+ await state.fileOperations.rename(state.destination, state.previousSource);
63
+ await state.fileOperations.rename(
64
+ state.preparedContentRoot,
65
+ state.destination
66
+ );
67
+ await state.fileOperations.rename(
68
+ state.stagedRecord.targetPath,
69
+ state.previousRecord
70
+ );
71
+ await state.fileOperations.rename(
72
+ state.stagedRecord.temporaryPath,
73
+ state.stagedRecord.targetPath
74
+ );
75
+ }
76
+
77
+ export async function cleanupDirectorySourceReplacement(state) {
78
+ await state.fileOperations.rm(
79
+ state.previousSource,
80
+ { recursive: true, force: true }
81
+ );
82
+ await state.fileOperations.rm(state.previousRecord, { force: true });
83
+ }
84
+
85
+ export async function rollbackDirectorySourceReplacement(state) {
86
+ if (await pathExists(state.previousRecord)) {
87
+ await restorePublishedPath({
88
+ fileOperations: state.fileOperations,
89
+ backup: state.previousRecord,
90
+ target: state.stagedRecord.targetPath,
91
+ removeOptions: { force: true }
92
+ });
93
+ }
94
+ if (await pathExists(state.previousSource)) {
95
+ await restorePublishedPath({
96
+ fileOperations: state.fileOperations,
97
+ backup: state.previousSource,
98
+ target: state.destination,
99
+ removeOptions: { recursive: true, force: true }
100
+ });
101
+ }
102
+ }
103
+
104
+ export async function discardDirectorySourceReplacement(state) {
105
+ await discardStagedSourceRecord(state.stagedRecord);
106
+ }
107
+
108
+ // === Git Fast-Forward Strategy ===
109
+ // For Git sources
110
+
111
+ export async function prepareGitSourceFastForward({
112
+ rootDir,
113
+ record,
114
+ nextRecord,
115
+ incomingCommit,
116
+ publishRecord = publishStagedSourceRecord
117
+ }) {
118
+ return {
119
+ destination: path.resolve(rootDir, record.installPath),
120
+ incomingCommit,
121
+ previousRecord: record,
122
+ publishRecord,
123
+ recordPublished: false,
124
+ rootDir,
125
+ sourceId: record.sourceId,
126
+ stagedRecord: await stageSourceRecord(rootDir, nextRecord)
127
+ };
128
+ }
129
+
130
+ export async function commitGitSourceFastForward(state) {
131
+ await state.publishRecord(state.stagedRecord);
132
+ state.recordPublished = true;
133
+ await execGit(state.destination, [
134
+ 'merge',
135
+ '--ff-only',
136
+ '--no-edit',
137
+ state.incomingCommit
138
+ ], state.sourceId, 'Could not fast-forward Git source');
139
+ const installedCommit = await readGitOutput(
140
+ state.destination,
141
+ ['rev-parse', 'HEAD']
142
+ );
143
+ if (installedCommit !== state.incomingCommit) {
144
+ throw gitUpdateError(
145
+ 'git-update',
146
+ `Git source did not advance to the planned commit: ${state.sourceId}`
147
+ );
148
+ }
149
+ }
150
+
151
+ export async function rollbackGitSourceFastForward(state) {
152
+ if (!state.recordPublished) return;
153
+ await writeSourceRecord(state.rootDir, state.previousRecord);
154
+ }
155
+
156
+ export async function discardGitSourceFastForward(state) {
157
+ await discardStagedSourceRecord(state.stagedRecord);
158
+ }
159
+
160
+ // === Private helpers ===
161
+
162
+ async function pathExists(targetPath) {
163
+ try {
164
+ await lstat(targetPath);
165
+ return true;
166
+ } catch (error) {
167
+ if (error.code === 'ENOENT') return false;
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ async function restorePublishedPath({
173
+ fileOperations,
174
+ backup,
175
+ target,
176
+ removeOptions
177
+ }) {
178
+ await fileOperations.rm(target, removeOptions);
179
+ await fileOperations.rename(backup, target);
180
+ }
181
+
182
+ async function readGitOutput(directory, args) {
183
+ const result = await execGit(
184
+ directory,
185
+ args,
186
+ 'source',
187
+ 'Could not inspect Git source'
188
+ );
189
+ return result.stdout.trim();
190
+ }
191
+
192
+ async function execGit(directory, args, sourceId, action) {
193
+ try {
194
+ return await execFile('git', ['-C', directory, ...args]);
195
+ } catch {
196
+ throw gitUpdateError('git-update', `${action}: ${sourceId}`);
197
+ }
198
+ }
199
+
200
+ function gitUpdateError(category, message, exitCode = 1) {
201
+ return new SourceAcquisitionError(category, message, exitCode);
202
+ }
@@ -0,0 +1,120 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, lstat, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { SourceAcquisitionError } from './sourceAcquisitionError.js';
5
+ import { discoverSourceSkillDirectories } from './sourceSkillDiscovery.js';
6
+
7
+ const KNOWN_FRONTMATTER_FIELDS = new Set([
8
+ 'allowed-tools',
9
+ 'compatibility',
10
+ 'description',
11
+ 'disable-model-invocation',
12
+ 'license',
13
+ 'metadata',
14
+ 'name'
15
+ ]);
16
+
17
+ export async function validateStagedSource(sourceRoot) {
18
+ const skillDirectories = await discoverSourceSkillDirectories(sourceRoot);
19
+ if (skillDirectories.length === 0) {
20
+ throw validationError('No scanner-visible SKILL.md was found');
21
+ }
22
+
23
+ const skills = [];
24
+ const warnings = [];
25
+ for (const skillDirectory of skillDirectories) {
26
+ const skillPath = path.join(skillDirectory, 'SKILL.md');
27
+ const relativeDirectory = toRelativeSkillPath(sourceRoot, skillDirectory);
28
+ const content = await readRegularSkillFile(skillPath, relativeDirectory);
29
+ skills.push(relativeDirectory);
30
+ warnings.push(...qualityWarnings(relativeDirectory, content));
31
+ }
32
+
33
+ return { skills: skills.sort(), warnings };
34
+ }
35
+
36
+ async function readRegularSkillFile(skillPath, relativeDirectory) {
37
+ const displayPath = relativeDirectory === '.' ? 'SKILL.md' : `${relativeDirectory}/SKILL.md`;
38
+ const stat = await lstat(skillPath);
39
+ if (!stat.isFile()) {
40
+ throw validationError(`${displayPath} must be a regular file`);
41
+ }
42
+
43
+ try {
44
+ await access(skillPath, constants.R_OK);
45
+ const content = await readFile(skillPath, 'utf8');
46
+ if (!content.trim()) throw validationError(`${displayPath} must not be empty`);
47
+ return content;
48
+ } catch (error) {
49
+ if (error instanceof SourceAcquisitionError) throw error;
50
+ throw validationError(`${displayPath} must be readable`);
51
+ }
52
+ }
53
+
54
+ function qualityWarnings(skillPath, content) {
55
+ const frontmatter = parseFrontmatter(content);
56
+ if (!frontmatter) {
57
+ return [
58
+ warning('missing-frontmatter', skillPath, 'SKILL.md has no YAML frontmatter'),
59
+ warning('missing-description', skillPath, 'SKILL.md frontmatter has no description')
60
+ ];
61
+ }
62
+ if (frontmatter.malformed) {
63
+ return [
64
+ warning('malformed-frontmatter', skillPath, 'SKILL.md frontmatter is not closed'),
65
+ warning('missing-description', skillPath, 'SKILL.md frontmatter has no description')
66
+ ];
67
+ }
68
+
69
+ const warnings = [];
70
+ if (!hasDescription(frontmatter.fields)) {
71
+ warnings.push(warning(
72
+ 'missing-description',
73
+ skillPath,
74
+ 'SKILL.md frontmatter has no description'
75
+ ));
76
+ }
77
+ for (const field of frontmatter.fields.keys()) {
78
+ if (!KNOWN_FRONTMATTER_FIELDS.has(field)) {
79
+ warnings.push(warning(
80
+ 'unknown-frontmatter-field',
81
+ skillPath,
82
+ `SKILL.md frontmatter contains unknown field: ${field}`
83
+ ));
84
+ }
85
+ }
86
+ return warnings;
87
+ }
88
+
89
+ function parseFrontmatter(content) {
90
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
91
+ if (lines[0] !== '---') return null;
92
+ const closingIndex = lines.indexOf('---', 1);
93
+ if (closingIndex < 0) return { malformed: true, fields: new Map() };
94
+
95
+ const fields = new Map();
96
+ for (const line of lines.slice(1, closingIndex)) {
97
+ const match = line.match(/^([A-Za-z0-9_-]+)\s*:(.*)$/);
98
+ if (match) fields.set(match[1], match[2].trim());
99
+ }
100
+ return { malformed: false, fields };
101
+ }
102
+
103
+ function hasDescription(fields) {
104
+ if (!fields.has('description')) return false;
105
+ const value = fields.get('description');
106
+ return Boolean(value && !['""', "''", 'null', '~'].includes(value));
107
+ }
108
+
109
+ function warning(category, skillPath, message) {
110
+ return { category, skillPath, message };
111
+ }
112
+
113
+ function validationError(message) {
114
+ return new SourceAcquisitionError('source-validation', message);
115
+ }
116
+
117
+ function toRelativeSkillPath(sourceRoot, skillDirectory) {
118
+ const relative = path.relative(sourceRoot, skillDirectory);
119
+ return relative ? relative.split(path.sep).join('/') : '.';
120
+ }
@@ -0,0 +1,37 @@
1
+ import {
2
+ mkdtemp,
3
+ rm,
4
+ rmdir
5
+ } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { SourceAcquisitionError } from './sourceAcquisitionError.js';
8
+ import { ensureSourceDirectory } from './sourceTree.js';
9
+
10
+ export async function createManagedSourceWorkspace(rootDir, prefix) {
11
+ const parent = await requireSafeDirectory(
12
+ rootDir,
13
+ '.skillcaddy',
14
+ 'staging'
15
+ );
16
+ return {
17
+ parent,
18
+ root: await mkdtemp(path.join(parent, prefix))
19
+ };
20
+ }
21
+
22
+ export async function removeManagedSourceWorkspace(workspace) {
23
+ await rm(workspace.root, { recursive: true, force: true });
24
+ try {
25
+ await rmdir(workspace.parent);
26
+ } catch (error) {
27
+ if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error;
28
+ }
29
+ }
30
+
31
+ async function requireSafeDirectory(rootDir, ...segments) {
32
+ try {
33
+ return await ensureSourceDirectory(rootDir, ...segments);
34
+ } catch (error) {
35
+ throw new SourceAcquisitionError('source-safety', error.message);
36
+ }
37
+ }