craftdesk 0.1.0 → 0.3.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/.claude/settings.local.json +19 -0
- package/.github/workflows/ci.yml +94 -0
- package/API.md +414 -0
- package/README.md +778 -0
- package/TESTING.md +263 -0
- package/bin/craftdesk.js +4 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +284 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +57 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +109 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +117 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +74 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/services/config-manager.d.ts +88 -0
- package/dist/services/config-manager.d.ts.map +1 -0
- package/dist/services/config-manager.js +143 -0
- package/dist/services/config-manager.js.map +1 -0
- package/dist/services/git-resolver.d.ts +139 -0
- package/dist/services/git-resolver.d.ts.map +1 -0
- package/dist/services/git-resolver.js +340 -0
- package/dist/services/git-resolver.js.map +1 -0
- package/dist/services/installer.d.ts +98 -0
- package/dist/services/installer.d.ts.map +1 -0
- package/dist/services/installer.js +274 -0
- package/dist/services/installer.js.map +1 -0
- package/dist/services/registry-client.d.ts +27 -0
- package/dist/services/registry-client.d.ts.map +1 -0
- package/dist/services/registry-client.js +134 -0
- package/dist/services/registry-client.js.map +1 -0
- package/dist/types/config.d.ts +17 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/craftdesk-json.d.ts +42 -0
- package/dist/types/craftdesk-json.d.ts.map +1 -0
- package/dist/types/craftdesk-json.js +3 -0
- package/dist/types/craftdesk-json.js.map +1 -0
- package/dist/types/craftdesk-lock.d.ts +34 -0
- package/dist/types/craftdesk-lock.d.ts.map +1 -0
- package/dist/types/craftdesk-lock.js +3 -0
- package/dist/types/craftdesk-lock.js.map +1 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.d.ts.map +1 -0
- package/dist/utils/file-system.js +60 -0
- package/dist/utils/file-system.js.map +1 -0
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/logger.js.map +1 -0
- package/docs/DEPENDENCY_RESOLUTION.md +237 -0
- package/examples/craftdesk-all-sources.json +79 -0
- package/examples/craftdesk-git-monorepo.json +41 -0
- package/examples/craftdesk-multi-registry.json +40 -0
- package/examples/craftdesk-with-registries.json +49 -0
- package/examples/simple-craftdesk.json +10 -0
- package/package.json +32 -25
- package/src/commands/add.ts +314 -0
- package/src/commands/init.ts +55 -0
- package/src/commands/install.ts +126 -0
- package/src/commands/list.ts +126 -0
- package/src/commands/remove.ts +82 -0
- package/src/index.ts +56 -0
- package/src/services/config-manager.ts +150 -0
- package/src/services/git-resolver.ts +373 -0
- package/src/services/installer.ts +314 -0
- package/src/services/registry-client.ts +163 -0
- package/src/types/craftdesk-json.ts +53 -0
- package/src/types/craftdesk-lock.ts +38 -0
- package/src/utils/file-system.ts +55 -0
- package/src/utils/logger.ts +82 -0
- package/tests/fixtures/craftdesk-git.json +17 -0
- package/tests/fixtures/craftdesk.json +15 -0
- package/tests/helpers/test-utils.ts +60 -0
- package/tests/integration/init-command.test.ts +70 -0
- package/tests/integration/list-command.test.ts +77 -0
- package/tests/unit/config-manager.test.ts +228 -0
- package/tests/unit/git-resolver.test.ts +303 -0
- package/tests/unit/installer.test.ts +105 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +29 -0
- package/LICENSE +0 -21
- package/index.js +0 -4
package/package.json
CHANGED
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "craftdesk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Package
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Package manager for Claude Code skills and agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"craftdesk": "./bin/craftdesk.js"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
|
-
"
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"test:ui": "vitest --ui",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"test:watch": "vitest watch",
|
|
17
|
+
"prepare": "npm run build"
|
|
8
18
|
},
|
|
9
19
|
"keywords": [
|
|
10
|
-
"
|
|
20
|
+
"craftdesk",
|
|
11
21
|
"ai",
|
|
12
|
-
"
|
|
13
|
-
"agents",
|
|
22
|
+
"cli",
|
|
14
23
|
"package-manager",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"skill-management",
|
|
18
|
-
"craftdesk"
|
|
24
|
+
"skills",
|
|
25
|
+
"agents"
|
|
19
26
|
],
|
|
20
|
-
"author": "CraftDesk
|
|
27
|
+
"author": "CraftDesk",
|
|
21
28
|
"license": "MIT",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"axios": "^1.6.2",
|
|
31
|
+
"commander": "^11.1.0",
|
|
32
|
+
"fs-extra": "^11.2.0"
|
|
25
33
|
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.10.5",
|
|
36
|
+
"@types/fs-extra": "^11.0.4",
|
|
37
|
+
"typescript": "^5.3.3",
|
|
38
|
+
"vitest": "^1.0.4",
|
|
39
|
+
"@vitest/ui": "^1.0.4",
|
|
40
|
+
"@vitest/coverage-v8": "^1.0.4"
|
|
28
41
|
},
|
|
29
|
-
"homepage": "https://craftdesk.dev",
|
|
30
42
|
"engines": {
|
|
31
43
|
"node": ">=18.0.0"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
"index.js",
|
|
35
|
-
"README.md",
|
|
36
|
-
"LICENSE"
|
|
37
|
-
]
|
|
38
|
-
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readCraftDeskJson, writeCraftDeskJson, readCraftDeskLock, writeCraftDeskLock } from '../utils/file-system';
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
|
+
import { registryClient } from '../services/registry-client';
|
|
6
|
+
import { installer } from '../services/installer';
|
|
7
|
+
import { gitResolver } from '../services/git-resolver';
|
|
8
|
+
|
|
9
|
+
export function createAddCommand(): Command {
|
|
10
|
+
return new Command('add')
|
|
11
|
+
.description('Add a new dependency and install it')
|
|
12
|
+
.argument('<craft>', 'Craft name (e.g., ruby-on-rails or @acme/patterns)')
|
|
13
|
+
.option('-D, --save-dev', 'Save as devDependency')
|
|
14
|
+
.option('-O, --save-optional', 'Save as optionalDependency')
|
|
15
|
+
.option('-E, --save-exact', 'Save exact version')
|
|
16
|
+
.option('-t, --type <type>', 'Specify craft type (skill, agent, command, hook, plugin)')
|
|
17
|
+
.action(async (craftArg: string, options) => {
|
|
18
|
+
await addCommand(craftArg, options);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function addCommand(craftArg: string, options: any): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
// Read craftdesk.json
|
|
25
|
+
const craftDeskJson = await readCraftDeskJson();
|
|
26
|
+
if (!craftDeskJson) {
|
|
27
|
+
logger.error('No craftdesk.json found in current directory');
|
|
28
|
+
logger.info('Run "craftdesk init" to create one');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse craft name and version
|
|
33
|
+
let craftName: string;
|
|
34
|
+
let versionConstraint: string = '*';
|
|
35
|
+
|
|
36
|
+
if (craftArg.includes('@') && !craftArg.startsWith('@')) {
|
|
37
|
+
// Format: craft@version
|
|
38
|
+
const parts = craftArg.split('@');
|
|
39
|
+
craftName = parts[0];
|
|
40
|
+
versionConstraint = parts[1];
|
|
41
|
+
} else if (craftArg.startsWith('@')) {
|
|
42
|
+
// Scoped craft: @scope/craft or @scope/craft@version
|
|
43
|
+
const afterScope = craftArg.substring(craftArg.indexOf('/') + 1);
|
|
44
|
+
if (afterScope.includes('@')) {
|
|
45
|
+
const parts = craftArg.split('@');
|
|
46
|
+
craftName = `${parts[0]}@${parts[1]}`;
|
|
47
|
+
versionConstraint = parts[2];
|
|
48
|
+
} else {
|
|
49
|
+
craftName = craftArg;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
craftName = craftArg;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger.info(`Adding ${craftName}...`);
|
|
56
|
+
|
|
57
|
+
// Check if this is a git dependency
|
|
58
|
+
let depValue: string | any;
|
|
59
|
+
let lockEntry: any;
|
|
60
|
+
let displayInfo: string;
|
|
61
|
+
|
|
62
|
+
// Convert GitHub web URLs to git URLs automatically
|
|
63
|
+
const normalizedArg = normalizeGitHubUrl(craftArg);
|
|
64
|
+
|
|
65
|
+
if (normalizedArg.startsWith('git+') || normalizedArg.includes('.git')) {
|
|
66
|
+
// Handle git dependency
|
|
67
|
+
logger.startSpinner('Analyzing git dependency...');
|
|
68
|
+
|
|
69
|
+
const gitInfo = parseGitUrl(normalizedArg);
|
|
70
|
+
|
|
71
|
+
// Resolve the git dependency to get its craftdesk.json and dependencies
|
|
72
|
+
const resolvedGit = await gitResolver.resolveGitDependency({
|
|
73
|
+
url: gitInfo.url,
|
|
74
|
+
branch: gitInfo.branch,
|
|
75
|
+
tag: gitInfo.tag,
|
|
76
|
+
commit: gitInfo.commit,
|
|
77
|
+
path: gitInfo.path,
|
|
78
|
+
file: gitInfo.file
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
depValue = {
|
|
82
|
+
git: gitInfo.url,
|
|
83
|
+
...(gitInfo.branch && { branch: gitInfo.branch }),
|
|
84
|
+
...(gitInfo.tag && { tag: gitInfo.tag }),
|
|
85
|
+
...(gitInfo.path && { path: gitInfo.path }),
|
|
86
|
+
...(gitInfo.file && { file: gitInfo.file })
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Use resolved information for the lock entry
|
|
90
|
+
const craftJson = resolvedGit.craftDeskJson;
|
|
91
|
+
|
|
92
|
+
// Determine type: explicit option > craftdesk.json > inferred > default
|
|
93
|
+
const craftType = options.type || craftJson?.type || 'skill';
|
|
94
|
+
|
|
95
|
+
lockEntry = {
|
|
96
|
+
version: craftJson?.version || gitInfo.tag || gitInfo.branch || 'HEAD',
|
|
97
|
+
resolved: gitInfo.url,
|
|
98
|
+
integrity: resolvedGit.resolvedCommit || 'git',
|
|
99
|
+
type: craftType,
|
|
100
|
+
author: craftJson?.author || 'git',
|
|
101
|
+
git: gitInfo.url,
|
|
102
|
+
...(gitInfo.branch && { branch: gitInfo.branch }),
|
|
103
|
+
...(gitInfo.tag && { tag: gitInfo.tag }),
|
|
104
|
+
...(resolvedGit.resolvedCommit && { commit: resolvedGit.resolvedCommit }),
|
|
105
|
+
...(gitInfo.path && { path: gitInfo.path }),
|
|
106
|
+
...(gitInfo.file && { file: gitInfo.file }),
|
|
107
|
+
dependencies: craftJson?.dependencies || {}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
craftName = craftJson?.name || gitInfo.name || path.basename(gitInfo.url, '.git');
|
|
111
|
+
displayInfo = `${craftName}@${craftJson?.version || 'git'} from ${gitInfo.url}`;
|
|
112
|
+
logger.succeedSpinner(`Resolved git dependency: ${displayInfo}`);
|
|
113
|
+
} else {
|
|
114
|
+
// Regular registry dependency
|
|
115
|
+
logger.startSpinner('Fetching craft information...');
|
|
116
|
+
|
|
117
|
+
// Check if the dependency has custom registry info in craftdesk.json
|
|
118
|
+
let registryOverride: string | undefined;
|
|
119
|
+
const existingDep = craftDeskJson.dependencies?.[craftName];
|
|
120
|
+
if (existingDep && typeof existingDep === 'object' && 'registry' in existingDep) {
|
|
121
|
+
registryOverride = existingDep.registry;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get craft info from registry
|
|
125
|
+
const craftInfo = await registryClient.getCraftInfo(craftName, undefined, registryOverride);
|
|
126
|
+
if (!craftInfo) {
|
|
127
|
+
logger.failSpinner(`Craft '${craftName}' not found`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.succeedSpinner(`Found ${craftInfo.name}@${craftInfo.version}`);
|
|
132
|
+
|
|
133
|
+
const version = options.saveExact ? craftInfo.version : versionConstraint;
|
|
134
|
+
depValue = version;
|
|
135
|
+
|
|
136
|
+
lockEntry = {
|
|
137
|
+
version: craftInfo.version,
|
|
138
|
+
resolved: craftInfo.download_url || `http://localhost:3000/api/v1/crafts/${craftInfo.author}/${craftInfo.name}/versions/${craftInfo.version}/download`,
|
|
139
|
+
integrity: craftInfo.integrity || 'sha256-placeholder',
|
|
140
|
+
type: craftInfo.type,
|
|
141
|
+
author: craftInfo.author,
|
|
142
|
+
dependencies: craftInfo.dependencies || {}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
displayInfo = `${craftInfo.name}@${craftInfo.version}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Determine which dependency field to use
|
|
149
|
+
let depField: string;
|
|
150
|
+
if (options.saveDev) {
|
|
151
|
+
depField = 'devDependencies';
|
|
152
|
+
} else if (options.saveOptional) {
|
|
153
|
+
depField = 'optionalDependencies';
|
|
154
|
+
} else {
|
|
155
|
+
depField = 'dependencies';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Initialize field if it doesn't exist
|
|
159
|
+
if (!craftDeskJson[depField]) {
|
|
160
|
+
craftDeskJson[depField] = {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add to craftdesk.json
|
|
164
|
+
craftDeskJson[depField][craftName] = depValue;
|
|
165
|
+
|
|
166
|
+
// Save craftdesk.json
|
|
167
|
+
await writeCraftDeskJson(craftDeskJson);
|
|
168
|
+
|
|
169
|
+
const displayVersion = typeof depValue === 'string' ? depValue : (depValue.tag || depValue.branch || 'git');
|
|
170
|
+
logger.success(`Added ${craftName}@${displayVersion} to ${depField}`);
|
|
171
|
+
|
|
172
|
+
// Install the craft
|
|
173
|
+
logger.startSpinner(`Installing ${craftName}...`);
|
|
174
|
+
|
|
175
|
+
await installer.installCraft(craftName, lockEntry);
|
|
176
|
+
|
|
177
|
+
logger.succeedSpinner(`Installed ${displayInfo}`);
|
|
178
|
+
|
|
179
|
+
// Update craftdesk.lock
|
|
180
|
+
let lockfile = await readCraftDeskLock();
|
|
181
|
+
if (!lockfile) {
|
|
182
|
+
lockfile = {
|
|
183
|
+
version: '1.0.0',
|
|
184
|
+
lockfileVersion: 1,
|
|
185
|
+
generatedAt: new Date().toISOString(),
|
|
186
|
+
crafts: {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add or update the craft in the lockfile
|
|
191
|
+
lockfile.crafts[craftName] = lockEntry;
|
|
192
|
+
await writeCraftDeskLock(lockfile);
|
|
193
|
+
|
|
194
|
+
logger.success('Craft added successfully!');
|
|
195
|
+
} catch (error: any) {
|
|
196
|
+
logger.failSpinner();
|
|
197
|
+
logger.error(`Failed to add craft: ${error.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Converts GitHub web URLs to git URL format
|
|
204
|
+
*
|
|
205
|
+
* Supported GitHub URL formats:
|
|
206
|
+
* - https://github.com/user/repo/tree/branch/path/to/dir
|
|
207
|
+
* - https://github.com/user/repo/blob/branch/path/to/file.md
|
|
208
|
+
* - https://github.com/user/repo (assumes main branch)
|
|
209
|
+
*
|
|
210
|
+
* @param url - The GitHub URL (web or git format)
|
|
211
|
+
* @returns Normalized git URL with #branch#path: or #branch#file: syntax
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
function normalizeGitHubUrl(url: string): string {
|
|
215
|
+
// Already in git+ format, return as-is
|
|
216
|
+
if (url.startsWith('git+')) {
|
|
217
|
+
return url;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Already a .git URL, return as-is
|
|
221
|
+
if (url.endsWith('.git') || url.includes('.git#')) {
|
|
222
|
+
return url;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if it's a GitHub web URL
|
|
226
|
+
const githubTreeMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)$/);
|
|
227
|
+
const githubBlobMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
228
|
+
const githubRepoMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/);
|
|
229
|
+
|
|
230
|
+
if (githubTreeMatch) {
|
|
231
|
+
// Tree URL: directory reference
|
|
232
|
+
const [, owner, repo, branch, path] = githubTreeMatch;
|
|
233
|
+
return `git+https://github.com/${owner}/${repo}.git#${branch}#path:${path}`;
|
|
234
|
+
} else if (githubBlobMatch) {
|
|
235
|
+
// Blob URL: file reference
|
|
236
|
+
const [, owner, repo, branch, file] = githubBlobMatch;
|
|
237
|
+
return `git+https://github.com/${owner}/${repo}.git#${branch}#file:${file}`;
|
|
238
|
+
} else if (githubRepoMatch) {
|
|
239
|
+
// Repo URL without tree/blob: clone entire repo
|
|
240
|
+
const [, owner, repo] = githubRepoMatch;
|
|
241
|
+
return `git+https://github.com/${owner}/${repo}.git`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Not a recognized GitHub URL, return as-is
|
|
245
|
+
return url;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Parses a git URL with optional ref and subdirectory/file specifiers
|
|
250
|
+
*
|
|
251
|
+
* Supported formats:
|
|
252
|
+
* - git+https://github.com/user/repo.git#branch
|
|
253
|
+
* - git+https://github.com/user/repo.git#v1.0.0
|
|
254
|
+
* - git+https://github.com/user/repo.git#commit-hash
|
|
255
|
+
* - git+https://github.com/user/repo.git#path:subfolder/skill
|
|
256
|
+
* - git+https://github.com/user/repo.git#file:skill.md
|
|
257
|
+
* - git+https://github.com/user/repo.git#main#file:rspec-agent.md
|
|
258
|
+
* - https://github.com/user/repo.git
|
|
259
|
+
*
|
|
260
|
+
* @param urlString - The git URL to parse
|
|
261
|
+
* @returns Parsed git dependency information
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
function parseGitUrl(urlString: string): any {
|
|
265
|
+
// Remove optional git+ prefix (npm-style format)
|
|
266
|
+
let url = urlString.replace(/^git\+/, '');
|
|
267
|
+
|
|
268
|
+
const result: any = {};
|
|
269
|
+
|
|
270
|
+
// Extract direct file path (e.g., #file:rspec-agent.md)
|
|
271
|
+
if (url.includes('#file:')) {
|
|
272
|
+
const [baseUrl, filePart] = url.split('#file:');
|
|
273
|
+
url = baseUrl;
|
|
274
|
+
result.file = filePart;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Extract subdirectory path for monorepos (e.g., #path:crafts/auth)
|
|
278
|
+
if (url.includes('#path:')) {
|
|
279
|
+
const [baseUrl, pathPart] = url.split('#path:');
|
|
280
|
+
url = baseUrl;
|
|
281
|
+
result.path = pathPart;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Extract git reference (branch/tag/commit)
|
|
285
|
+
// Must be parsed after path and file to avoid conflicts
|
|
286
|
+
if (url.includes('#') && !url.includes('#path:') && !url.includes('#file:')) {
|
|
287
|
+
const [baseUrl, ref] = url.split('#');
|
|
288
|
+
url = baseUrl;
|
|
289
|
+
|
|
290
|
+
// Heuristic to determine ref type:
|
|
291
|
+
// - Tags typically start with 'v' or match semver pattern (1.0.0)
|
|
292
|
+
if (ref.startsWith('v') || /^\d+\.\d+/.test(ref)) {
|
|
293
|
+
result.tag = ref;
|
|
294
|
+
}
|
|
295
|
+
// - Commit hashes are 40-character hex strings
|
|
296
|
+
else if (ref.length === 40 && /^[a-f0-9]+$/.test(ref)) {
|
|
297
|
+
result.commit = ref;
|
|
298
|
+
}
|
|
299
|
+
// - Otherwise, assume it's a branch name
|
|
300
|
+
else {
|
|
301
|
+
result.branch = ref;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
result.url = url;
|
|
306
|
+
|
|
307
|
+
// Extract repository name from URL for display purposes
|
|
308
|
+
const match = url.match(/\/([^/]+?)(\.git)?$/);
|
|
309
|
+
if (match) {
|
|
310
|
+
result.name = match[1];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CraftDeskJson } from '../types/craftdesk-json';
|
|
4
|
+
import { writeCraftDeskJson, exists } from '../utils/file-system';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
export function createInitCommand(): Command {
|
|
8
|
+
return new Command('init')
|
|
9
|
+
.description('Initialize a new craftdesk.json file')
|
|
10
|
+
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
11
|
+
.option('-n, --name <name>', 'Project name')
|
|
12
|
+
.option('--project-version <version>', 'Project version', '1.0.0')
|
|
13
|
+
.option('-t, --type <type>', 'Project type (skill, agent, command, hook, plugin)', 'skill')
|
|
14
|
+
.option('-d, --description <desc>', 'Project description')
|
|
15
|
+
.option('-a, --author <author>', 'Author name')
|
|
16
|
+
.option('-l, --license <license>', 'License', 'MIT')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
await initCommand(options);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function initCommand(options: any): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
// Check if craftdesk.json already exists
|
|
25
|
+
if (await exists('craftdesk.json')) {
|
|
26
|
+
logger.error('craftdesk.json already exists in this directory');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const config: CraftDeskJson = {
|
|
31
|
+
name: options.name || path.basename(process.cwd()),
|
|
32
|
+
version: options.projectVersion,
|
|
33
|
+
type: options.type,
|
|
34
|
+
description: options.description || undefined,
|
|
35
|
+
author: options.author || undefined,
|
|
36
|
+
license: options.license || undefined,
|
|
37
|
+
dependencies: {},
|
|
38
|
+
registries: {
|
|
39
|
+
default: {
|
|
40
|
+
url: 'https://craftdesk.ai'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Write craftdesk.json
|
|
46
|
+
await writeCraftDeskJson(config);
|
|
47
|
+
|
|
48
|
+
logger.success('Created craftdesk.json');
|
|
49
|
+
logger.info('Run "craftdesk install" to install dependencies');
|
|
50
|
+
logger.info('Run "craftdesk add <craft>" to add dependencies');
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
logger.error(`Failed to initialize: ${error.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readCraftDeskJson, readCraftDeskLock, writeCraftDeskLock } from '../utils/file-system';
|
|
3
|
+
import { logger } from '../utils/logger';
|
|
4
|
+
import { registryClient } from '../services/registry-client';
|
|
5
|
+
import { gitResolver } from '../services/git-resolver';
|
|
6
|
+
import { installer } from '../services/installer';
|
|
7
|
+
import { CraftDeskLock } from '../types/craftdesk-lock';
|
|
8
|
+
|
|
9
|
+
export function createInstallCommand(): Command {
|
|
10
|
+
return new Command('install')
|
|
11
|
+
.description('Install all dependencies from craftdesk.json')
|
|
12
|
+
.option('--no-lockfile', 'Ignore craftdesk.lock and re-resolve dependencies')
|
|
13
|
+
.option('--production', 'Skip devDependencies')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
await installCommand(options);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function installCommand(options: any): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
// Read craftdesk.json
|
|
22
|
+
const craftDeskJson = await readCraftDeskJson();
|
|
23
|
+
if (!craftDeskJson) {
|
|
24
|
+
logger.error('No craftdesk.json found in current directory');
|
|
25
|
+
logger.info('Run "craftdesk init" to create one');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.info('Installing dependencies from craftdesk.json...');
|
|
30
|
+
|
|
31
|
+
let lockfile: CraftDeskLock | null = null;
|
|
32
|
+
|
|
33
|
+
// Check for existing lockfile
|
|
34
|
+
if (!options.noLockfile) {
|
|
35
|
+
lockfile = await readCraftDeskLock();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (lockfile) {
|
|
39
|
+
// Install from lockfile
|
|
40
|
+
logger.info('Installing from craftdesk.lock...');
|
|
41
|
+
logger.startSpinner('Installing crafts...');
|
|
42
|
+
|
|
43
|
+
await installer.installFromLockfile(lockfile);
|
|
44
|
+
|
|
45
|
+
logger.success('Installation complete!');
|
|
46
|
+
} else {
|
|
47
|
+
// Resolve dependencies
|
|
48
|
+
logger.info('No lockfile found. Resolving dependencies...');
|
|
49
|
+
logger.startSpinner('Resolving dependencies...');
|
|
50
|
+
|
|
51
|
+
const rawDependencies = craftDeskJson.dependencies || {};
|
|
52
|
+
|
|
53
|
+
if (Object.keys(rawDependencies).length === 0) {
|
|
54
|
+
logger.stopSpinner();
|
|
55
|
+
logger.info('No dependencies to install');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Collect all dependencies (including devDependencies if not --production)
|
|
60
|
+
const allDependencies: Record<string, any> = { ...rawDependencies };
|
|
61
|
+
|
|
62
|
+
if (!options.production && craftDeskJson.devDependencies) {
|
|
63
|
+
Object.assign(allDependencies, craftDeskJson.devDependencies);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Use git resolver which handles both git AND registry dependencies
|
|
67
|
+
const resolution = await gitResolver.resolveAllDependencies(allDependencies);
|
|
68
|
+
|
|
69
|
+
if (!resolution) {
|
|
70
|
+
logger.failSpinner('Failed to resolve dependencies');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.succeedSpinner('Dependencies resolved');
|
|
75
|
+
|
|
76
|
+
// For registry dependencies that need resolution, fetch their info
|
|
77
|
+
logger.startSpinner('Fetching registry crafts...');
|
|
78
|
+
|
|
79
|
+
for (const [name, entry] of Object.entries(resolution.resolved)) {
|
|
80
|
+
if (entry.needsResolution) {
|
|
81
|
+
const craftInfo = await registryClient.getCraftInfo(name, entry.version, entry.registry);
|
|
82
|
+
if (craftInfo) {
|
|
83
|
+
resolution.resolved[name] = {
|
|
84
|
+
version: craftInfo.version,
|
|
85
|
+
resolved: craftInfo.download_url || `http://localhost:3000/api/v1/crafts/${craftInfo.author}/${craftInfo.name}/versions/${craftInfo.version}/download`,
|
|
86
|
+
integrity: craftInfo.integrity || 'sha256-placeholder',
|
|
87
|
+
type: craftInfo.type,
|
|
88
|
+
author: craftInfo.author,
|
|
89
|
+
dependencies: craftInfo.dependencies || {}
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
logger.failSpinner(`Failed to resolve craft: ${name}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.succeedSpinner('Registry crafts fetched');
|
|
99
|
+
|
|
100
|
+
// Install resolved crafts
|
|
101
|
+
logger.startSpinner('Installing crafts...');
|
|
102
|
+
|
|
103
|
+
const newLockfile = resolution.lockfile;
|
|
104
|
+
await installer.installFromLockfile(newLockfile);
|
|
105
|
+
|
|
106
|
+
// Save lockfile
|
|
107
|
+
await writeCraftDeskLock(newLockfile);
|
|
108
|
+
logger.success('Created craftdesk.lock');
|
|
109
|
+
|
|
110
|
+
logger.success('Installation complete!');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Show summary
|
|
114
|
+
const installedCrafts = await installer.listInstalled();
|
|
115
|
+
if (installedCrafts.length > 0) {
|
|
116
|
+
logger.log('\nInstalled crafts:');
|
|
117
|
+
for (const craft of installedCrafts) {
|
|
118
|
+
logger.log(` • ${craft.name}@${craft.version} (${craft.type})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (error: any) {
|
|
122
|
+
logger.failSpinner();
|
|
123
|
+
logger.error(`Installation failed: ${error.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|