craftdesk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.claude/settings.local.json +17 -0
  2. package/.github/workflows/ci.yml +94 -0
  3. package/API.md +414 -0
  4. package/README.md +799 -0
  5. package/TESTING.md +263 -0
  6. package/bin/craftdesk.js +4 -0
  7. package/dist/commands/add.d.ts +3 -0
  8. package/dist/commands/add.d.ts.map +1 -0
  9. package/dist/commands/add.js +284 -0
  10. package/dist/commands/add.js.map +1 -0
  11. package/dist/commands/config.d.ts +3 -0
  12. package/dist/commands/config.d.ts.map +1 -0
  13. package/dist/commands/config.js +56 -0
  14. package/dist/commands/config.js.map +1 -0
  15. package/dist/commands/init.d.ts +3 -0
  16. package/dist/commands/init.d.ts.map +1 -0
  17. package/dist/commands/init.js +57 -0
  18. package/dist/commands/init.js.map +1 -0
  19. package/dist/commands/install.d.ts +3 -0
  20. package/dist/commands/install.d.ts.map +1 -0
  21. package/dist/commands/install.js +109 -0
  22. package/dist/commands/install.js.map +1 -0
  23. package/dist/commands/list.d.ts +3 -0
  24. package/dist/commands/list.d.ts.map +1 -0
  25. package/dist/commands/list.js +117 -0
  26. package/dist/commands/list.js.map +1 -0
  27. package/dist/commands/remove.d.ts +3 -0
  28. package/dist/commands/remove.d.ts.map +1 -0
  29. package/dist/commands/remove.js +74 -0
  30. package/dist/commands/remove.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +51 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/services/config-manager.d.ts +88 -0
  36. package/dist/services/config-manager.d.ts.map +1 -0
  37. package/dist/services/config-manager.js +143 -0
  38. package/dist/services/config-manager.js.map +1 -0
  39. package/dist/services/git-resolver.d.ts +139 -0
  40. package/dist/services/git-resolver.d.ts.map +1 -0
  41. package/dist/services/git-resolver.js +334 -0
  42. package/dist/services/git-resolver.js.map +1 -0
  43. package/dist/services/installer.d.ts +98 -0
  44. package/dist/services/installer.d.ts.map +1 -0
  45. package/dist/services/installer.js +274 -0
  46. package/dist/services/installer.js.map +1 -0
  47. package/dist/services/registry-client.d.ts +27 -0
  48. package/dist/services/registry-client.d.ts.map +1 -0
  49. package/dist/services/registry-client.js +134 -0
  50. package/dist/services/registry-client.js.map +1 -0
  51. package/dist/types/config.d.ts +17 -0
  52. package/dist/types/config.d.ts.map +1 -0
  53. package/dist/types/config.js +3 -0
  54. package/dist/types/config.js.map +1 -0
  55. package/dist/types/craftdesk-json.d.ts +42 -0
  56. package/dist/types/craftdesk-json.d.ts.map +1 -0
  57. package/dist/types/craftdesk-json.js +3 -0
  58. package/dist/types/craftdesk-json.js.map +1 -0
  59. package/dist/types/craftdesk-lock.d.ts +34 -0
  60. package/dist/types/craftdesk-lock.d.ts.map +1 -0
  61. package/dist/types/craftdesk-lock.js +3 -0
  62. package/dist/types/craftdesk-lock.js.map +1 -0
  63. package/dist/utils/file-system.d.ts +10 -0
  64. package/dist/utils/file-system.d.ts.map +1 -0
  65. package/dist/utils/file-system.js +60 -0
  66. package/dist/utils/file-system.js.map +1 -0
  67. package/dist/utils/logger.d.ts +18 -0
  68. package/dist/utils/logger.d.ts.map +1 -0
  69. package/dist/utils/logger.js +72 -0
  70. package/dist/utils/logger.js.map +1 -0
  71. package/docs/DEPENDENCY_RESOLUTION.md +237 -0
  72. package/examples/craftdesk-all-sources.json +79 -0
  73. package/examples/craftdesk-git-monorepo.json +41 -0
  74. package/examples/craftdesk-multi-registry.json +40 -0
  75. package/examples/craftdesk-with-registries.json +49 -0
  76. package/examples/simple-craftdesk.json +10 -0
  77. package/package.json +32 -25
  78. package/src/commands/add.ts +314 -0
  79. package/src/commands/init.ts +55 -0
  80. package/src/commands/install.ts +126 -0
  81. package/src/commands/list.ts +126 -0
  82. package/src/commands/remove.ts +82 -0
  83. package/src/index.ts +56 -0
  84. package/src/services/config-manager.ts +150 -0
  85. package/src/services/git-resolver.ts +370 -0
  86. package/src/services/installer.ts +314 -0
  87. package/src/services/registry-client.ts +163 -0
  88. package/src/types/craftdesk-json.ts +53 -0
  89. package/src/types/craftdesk-lock.ts +38 -0
  90. package/src/utils/file-system.ts +55 -0
  91. package/src/utils/logger.ts +82 -0
  92. package/tests/fixtures/craftdesk-git.json +17 -0
  93. package/tests/fixtures/craftdesk.json +15 -0
  94. package/tests/helpers/test-utils.ts +60 -0
  95. package/tests/integration/init-command.test.ts +70 -0
  96. package/tests/integration/list-command.test.ts +77 -0
  97. package/tests/unit/config-manager.test.ts +228 -0
  98. package/tests/unit/git-resolver.test.ts +303 -0
  99. package/tests/unit/installer.test.ts +105 -0
  100. package/tsconfig.json +20 -0
  101. package/vitest.config.ts +29 -0
  102. package/LICENSE +0 -21
  103. package/index.js +0 -4
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "my-project",
3
+ "version": "1.0.0",
4
+ "type": "skill",
5
+ "description": "A simple project with default registry",
6
+ "dependencies": {
7
+ "ruby-on-rails": "^7.0.0",
8
+ "postgres-expert": "^1.2.0"
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,38 +1,45 @@
1
1
  {
2
2
  "name": "craftdesk",
3
- "version": "0.1.0",
4
- "description": "Package management for Claude Skills and AI Agents - npm-style versioning, dependency resolution, and company-wide skill distribution",
5
- "main": "index.js",
3
+ "version": "0.2.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
- "test": "echo \"Error: no test specified\" && exit 1"
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
- "claude",
20
+ "craftdesk",
11
21
  "ai",
12
- "skills",
13
- "agents",
22
+ "cli",
14
23
  "package-manager",
15
- "claude-skills",
16
- "ai-agents",
17
- "skill-management",
18
- "craftdesk"
24
+ "skills",
25
+ "agents"
19
26
  ],
20
- "author": "CraftDesk Team",
27
+ "author": "CraftDesk",
21
28
  "license": "MIT",
22
- "repository": {
23
- "type": "git",
24
- "url": "https://github.com/craftdesk/craftdesk.git"
29
+ "dependencies": {
30
+ "axios": "^1.6.2",
31
+ "commander": "^11.1.0",
32
+ "fs-extra": "^11.2.0"
25
33
  },
26
- "bugs": {
27
- "url": "https://github.com/craftdesk/craftdesk/issues"
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
- "files": [
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)')
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)', '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
+ }