craftdesk 0.3.0 → 0.4.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 (175) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +943 -46
  3. package/dist/commands/add.d.ts +5 -0
  4. package/dist/commands/add.d.ts.map +1 -1
  5. package/dist/commands/add.js +302 -6
  6. package/dist/commands/add.js.map +1 -1
  7. package/dist/commands/convert.d.ts +8 -0
  8. package/dist/commands/convert.d.ts.map +1 -0
  9. package/dist/commands/convert.js +146 -0
  10. package/dist/commands/convert.js.map +1 -0
  11. package/dist/commands/info.d.ts +8 -0
  12. package/dist/commands/info.d.ts.map +1 -0
  13. package/dist/commands/info.js +66 -0
  14. package/dist/commands/info.js.map +1 -0
  15. package/dist/commands/init.d.ts +5 -0
  16. package/dist/commands/init.d.ts.map +1 -1
  17. package/dist/commands/init.js +8 -2
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/install.d.ts +5 -0
  20. package/dist/commands/install.d.ts.map +1 -1
  21. package/dist/commands/install.js +24 -4
  22. package/dist/commands/install.js.map +1 -1
  23. package/dist/commands/list.d.ts +5 -0
  24. package/dist/commands/list.d.ts.map +1 -1
  25. package/dist/commands/list.js +72 -7
  26. package/dist/commands/list.js.map +1 -1
  27. package/dist/commands/login.d.ts +8 -0
  28. package/dist/commands/login.d.ts.map +1 -0
  29. package/dist/commands/login.js +158 -0
  30. package/dist/commands/login.js.map +1 -0
  31. package/dist/commands/logout.d.ts +8 -0
  32. package/dist/commands/logout.d.ts.map +1 -0
  33. package/dist/commands/logout.js +51 -0
  34. package/dist/commands/logout.js.map +1 -0
  35. package/dist/commands/outdated.d.ts +23 -0
  36. package/dist/commands/outdated.d.ts.map +1 -0
  37. package/dist/commands/outdated.js +268 -0
  38. package/dist/commands/outdated.js.map +1 -0
  39. package/dist/commands/publish.d.ts +8 -0
  40. package/dist/commands/publish.d.ts.map +1 -0
  41. package/dist/commands/publish.js +198 -0
  42. package/dist/commands/publish.js.map +1 -0
  43. package/dist/commands/remove.d.ts +5 -0
  44. package/dist/commands/remove.d.ts.map +1 -1
  45. package/dist/commands/remove.js +51 -10
  46. package/dist/commands/remove.js.map +1 -1
  47. package/dist/commands/search.d.ts +8 -0
  48. package/dist/commands/search.d.ts.map +1 -0
  49. package/dist/commands/search.js +60 -0
  50. package/dist/commands/search.js.map +1 -0
  51. package/dist/commands/update.d.ts +8 -0
  52. package/dist/commands/update.d.ts.map +1 -0
  53. package/dist/commands/update.js +369 -0
  54. package/dist/commands/update.js.map +1 -0
  55. package/dist/commands/whoami.d.ts +8 -0
  56. package/dist/commands/whoami.d.ts.map +1 -0
  57. package/dist/commands/whoami.js +58 -0
  58. package/dist/commands/whoami.js.map +1 -0
  59. package/dist/converters/base-converter.d.ts +89 -0
  60. package/dist/converters/base-converter.d.ts.map +1 -0
  61. package/dist/converters/base-converter.js +249 -0
  62. package/dist/converters/base-converter.js.map +1 -0
  63. package/dist/converters/continue-converter.d.ts +53 -0
  64. package/dist/converters/continue-converter.d.ts.map +1 -0
  65. package/dist/converters/continue-converter.js +243 -0
  66. package/dist/converters/continue-converter.js.map +1 -0
  67. package/dist/converters/cursor-converter.d.ts +70 -0
  68. package/dist/converters/cursor-converter.d.ts.map +1 -0
  69. package/dist/converters/cursor-converter.js +272 -0
  70. package/dist/converters/cursor-converter.js.map +1 -0
  71. package/dist/index.js +34 -3
  72. package/dist/index.js.map +1 -1
  73. package/dist/services/config-manager.d.ts +128 -10
  74. package/dist/services/config-manager.d.ts.map +1 -1
  75. package/dist/services/config-manager.js +189 -18
  76. package/dist/services/config-manager.js.map +1 -1
  77. package/dist/services/converter.d.ts +53 -0
  78. package/dist/services/converter.d.ts.map +1 -0
  79. package/dist/services/converter.js +185 -0
  80. package/dist/services/converter.js.map +1 -0
  81. package/dist/services/craft-wrapper.d.ts +108 -0
  82. package/dist/services/craft-wrapper.d.ts.map +1 -0
  83. package/dist/services/craft-wrapper.js +268 -0
  84. package/dist/services/craft-wrapper.js.map +1 -0
  85. package/dist/services/git-resolver.d.ts +9 -3
  86. package/dist/services/git-resolver.d.ts.map +1 -1
  87. package/dist/services/git-resolver.js +29 -10
  88. package/dist/services/git-resolver.js.map +1 -1
  89. package/dist/services/installer.d.ts +51 -1
  90. package/dist/services/installer.d.ts.map +1 -1
  91. package/dist/services/installer.js +245 -14
  92. package/dist/services/installer.js.map +1 -1
  93. package/dist/services/plugin-resolver.d.ts +80 -0
  94. package/dist/services/plugin-resolver.d.ts.map +1 -0
  95. package/dist/services/plugin-resolver.js +182 -0
  96. package/dist/services/plugin-resolver.js.map +1 -0
  97. package/dist/services/registry-client.d.ts +132 -5
  98. package/dist/services/registry-client.d.ts.map +1 -1
  99. package/dist/services/registry-client.js +239 -34
  100. package/dist/services/registry-client.js.map +1 -1
  101. package/dist/services/settings-manager.d.ts +151 -0
  102. package/dist/services/settings-manager.d.ts.map +1 -0
  103. package/dist/services/settings-manager.js +333 -0
  104. package/dist/services/settings-manager.js.map +1 -0
  105. package/dist/types/claude-settings.d.ts +155 -0
  106. package/dist/types/claude-settings.d.ts.map +1 -0
  107. package/dist/types/claude-settings.js +20 -0
  108. package/dist/types/claude-settings.js.map +1 -0
  109. package/dist/types/converter.d.ts +98 -0
  110. package/dist/types/converter.d.ts.map +1 -0
  111. package/dist/types/converter.js +6 -0
  112. package/dist/types/converter.js.map +1 -0
  113. package/dist/types/craftdesk-json.d.ts +63 -3
  114. package/dist/types/craftdesk-json.d.ts.map +1 -1
  115. package/dist/types/craftdesk-lock.d.ts +62 -1
  116. package/dist/types/craftdesk-lock.d.ts.map +1 -1
  117. package/dist/utils/crypto.d.ts +23 -0
  118. package/dist/utils/crypto.d.ts.map +1 -0
  119. package/dist/utils/crypto.js +52 -0
  120. package/dist/utils/crypto.js.map +1 -0
  121. package/dist/utils/file-system.d.ts +43 -0
  122. package/dist/utils/file-system.d.ts.map +1 -1
  123. package/dist/utils/file-system.js +45 -2
  124. package/dist/utils/file-system.js.map +1 -1
  125. package/dist/utils/logger.d.ts +75 -0
  126. package/dist/utils/logger.d.ts.map +1 -1
  127. package/dist/utils/logger.js +76 -1
  128. package/dist/utils/logger.js.map +1 -1
  129. package/dist/utils/version-utils.d.ts +63 -0
  130. package/dist/utils/version-utils.d.ts.map +1 -0
  131. package/dist/utils/version-utils.js +130 -0
  132. package/dist/utils/version-utils.js.map +1 -0
  133. package/package.json +36 -9
  134. package/.claude/settings.local.json +0 -19
  135. package/.github/workflows/ci.yml +0 -94
  136. package/API.md +0 -414
  137. package/TESTING.md +0 -263
  138. package/dist/commands/config.d.ts +0 -3
  139. package/dist/commands/config.d.ts.map +0 -1
  140. package/dist/commands/config.js +0 -56
  141. package/dist/commands/config.js.map +0 -1
  142. package/dist/types/config.d.ts +0 -17
  143. package/dist/types/config.d.ts.map +0 -1
  144. package/dist/types/config.js +0 -3
  145. package/dist/types/config.js.map +0 -1
  146. package/docs/DEPENDENCY_RESOLUTION.md +0 -237
  147. package/examples/craftdesk-all-sources.json +0 -79
  148. package/examples/craftdesk-git-monorepo.json +0 -41
  149. package/examples/craftdesk-multi-registry.json +0 -40
  150. package/examples/craftdesk-with-registries.json +0 -49
  151. package/examples/simple-craftdesk.json +0 -10
  152. package/src/commands/add.ts +0 -314
  153. package/src/commands/init.ts +0 -55
  154. package/src/commands/install.ts +0 -126
  155. package/src/commands/list.ts +0 -126
  156. package/src/commands/remove.ts +0 -82
  157. package/src/index.ts +0 -56
  158. package/src/services/config-manager.ts +0 -150
  159. package/src/services/git-resolver.ts +0 -373
  160. package/src/services/installer.ts +0 -314
  161. package/src/services/registry-client.ts +0 -163
  162. package/src/types/craftdesk-json.ts +0 -53
  163. package/src/types/craftdesk-lock.ts +0 -38
  164. package/src/utils/file-system.ts +0 -55
  165. package/src/utils/logger.ts +0 -82
  166. package/tests/fixtures/craftdesk-git.json +0 -17
  167. package/tests/fixtures/craftdesk.json +0 -15
  168. package/tests/helpers/test-utils.ts +0 -60
  169. package/tests/integration/init-command.test.ts +0 -70
  170. package/tests/integration/list-command.test.ts +0 -77
  171. package/tests/unit/config-manager.test.ts +0 -228
  172. package/tests/unit/git-resolver.test.ts +0 -303
  173. package/tests/unit/installer.test.ts +0 -105
  174. package/tsconfig.json +0 -20
  175. package/vitest.config.ts +0 -29
@@ -1,82 +0,0 @@
1
- import { Command } from 'commander';
2
- import { readCraftDeskJson, writeCraftDeskJson, readCraftDeskLock, writeCraftDeskLock } from '../utils/file-system';
3
- import { logger } from '../utils/logger';
4
- import { installer } from '../services/installer';
5
-
6
- export function createRemoveCommand(): Command {
7
- return new Command('remove')
8
- .description('Remove a dependency')
9
- .argument('<craft>', 'Craft name to remove')
10
- .action(async (craftName: string) => {
11
- await removeCommand(craftName);
12
- });
13
- }
14
-
15
- async function removeCommand(craftName: string): Promise<void> {
16
- try {
17
- // Read craftdesk.json
18
- const craftDeskJson = await readCraftDeskJson();
19
- if (!craftDeskJson) {
20
- logger.error('No craftdesk.json found in current directory');
21
- process.exit(1);
22
- }
23
-
24
- // Check if craft exists in any dependency field
25
- let found = false;
26
- let foundInField: string | null = null;
27
-
28
- const depFields = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
29
-
30
- for (const field of depFields) {
31
- if (craftDeskJson[field] && craftDeskJson[field][craftName]) {
32
- found = true;
33
- foundInField = field;
34
- delete craftDeskJson[field][craftName];
35
-
36
- // Remove empty objects
37
- if (Object.keys(craftDeskJson[field]).length === 0) {
38
- delete craftDeskJson[field];
39
- }
40
- break;
41
- }
42
- }
43
-
44
- if (!found) {
45
- logger.error(`Craft '${craftName}' is not in craftdesk.json`);
46
- process.exit(1);
47
- }
48
-
49
- // Save updated craftdesk.json
50
- await writeCraftDeskJson(craftDeskJson);
51
- logger.success(`Removed ${craftName} from ${foundInField}`);
52
-
53
- // Read lockfile to get craft type
54
- const lockfile = await readCraftDeskLock();
55
- if (lockfile && lockfile.crafts[craftName]) {
56
- const craftEntry = lockfile.crafts[craftName];
57
-
58
- // Remove from file system
59
- await installer.removeCraft(craftName, craftEntry.type);
60
-
61
- // Remove from lockfile
62
- delete lockfile.crafts[craftName];
63
-
64
- // TODO: Clean up dependency tree in lockfile
65
-
66
- // Save updated lockfile
67
- await writeCraftDeskLock(lockfile);
68
- logger.success('Updated craftdesk.lock');
69
- } else {
70
- // Try to remove from file system anyway (best effort)
71
- const typesToTry = ['skill', 'agent', 'command', 'hook', 'plugin'];
72
- for (const type of typesToTry) {
73
- await installer.removeCraft(craftName, type);
74
- }
75
- }
76
-
77
- logger.success('Craft removed successfully!');
78
- } catch (error: any) {
79
- logger.error(`Failed to remove craft: ${error.message}`);
80
- process.exit(1);
81
- }
82
- }
package/src/index.ts DELETED
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import { createInitCommand } from './commands/init';
5
- import { createInstallCommand } from './commands/install';
6
- import { createAddCommand } from './commands/add';
7
- import { createRemoveCommand } from './commands/remove';
8
- import { createListCommand } from './commands/list';
9
- import { logger } from './utils/logger';
10
-
11
- const program = new Command();
12
-
13
- program
14
- .name('craftdesk')
15
- .description('CLI for managing CraftDesk AI capabilities')
16
- .version('0.3.0', '-v, --version')
17
- .option('-d, --debug', 'Enable debug output', () => {
18
- process.env.DEBUG = 'true';
19
- });
20
-
21
- // Add commands
22
- program.addCommand(createInitCommand());
23
- program.addCommand(createInstallCommand());
24
- program.addCommand(createAddCommand());
25
- program.addCommand(createRemoveCommand());
26
- program.addCommand(createListCommand());
27
-
28
- // Add aliases
29
- program.command('i', { hidden: true }).action(() => {
30
- program.parse(['', '', 'install', ...process.argv.slice(3)]);
31
- });
32
-
33
- // Error handling
34
- program.on('command:*', () => {
35
- logger.error(`Invalid command: ${program.args.join(' ')}`);
36
- logger.info('Run "craftdesk --help" for a list of available commands');
37
- process.exit(1);
38
- });
39
-
40
- // Show help if no command provided
41
- if (process.argv.length === 2) {
42
- logger.log('🚀 CraftDesk CLI');
43
- logger.log('');
44
- logger.log('Dependency management for AI capabilities');
45
- logger.log('');
46
- logger.log('Getting started:');
47
- logger.log(' craftdesk init Create a new craftdesk.json file');
48
- logger.log(' craftdesk install Install all dependencies');
49
- logger.log(' craftdesk add <pkg> Add a new dependency');
50
- logger.log('');
51
- logger.log('Run "craftdesk --help" for full command list');
52
- process.exit(0);
53
- }
54
-
55
- // Parse command line arguments
56
- program.parse(process.argv);
@@ -1,150 +0,0 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import { CraftDeskJson } from '../types/craftdesk-json';
4
- import { logger } from '../utils/logger';
5
-
6
- /**
7
- * Manages configuration for the CraftDesk CLI
8
- *
9
- * Handles reading craftdesk.json, resolving registry URLs for crafts,
10
- * managing authentication tokens, and determining installation paths.
11
- *
12
- * @example
13
- * ```typescript
14
- * const configManager = new ConfigManager();
15
- * const registry = await configManager.getRegistryForCraft('@company/auth');
16
- * const token = await configManager.getAuthToken('company-private');
17
- * ```
18
- */
19
- export class ConfigManager {
20
- private craftDeskJson: CraftDeskJson | null = null;
21
-
22
- /**
23
- * Gets the appropriate registry URL for a given craft
24
- *
25
- * Resolution order:
26
- * 1. Scoped registry matching the craft scope (e.g., @company/craft -> company registry)
27
- * 2. Default registry from craftdesk.json
28
- * 3. Global fallback (https://craftdesk.ai)
29
- *
30
- * @param craftName - The name of the craft to resolve
31
- * @returns The registry URL to use for this craft
32
- *
33
- * @example
34
- * ```typescript
35
- * // Scoped craft with configured registry
36
- * const url = await configManager.getRegistryForCraft('@company/auth');
37
- * // Returns: 'https://company.internal'
38
- *
39
- * // Regular craft
40
- * const url = await configManager.getRegistryForCraft('ruby-on-rails');
41
- * // Returns: 'https://craftdesk.ai' (or default from config)
42
- * ```
43
- */
44
- async getRegistryForCraft(craftName: string): Promise<string> {
45
- const craftDesk = await this.getCraftDeskJson();
46
-
47
- if (!craftDesk) {
48
- // Default registry if no craftdesk.json exists
49
- return 'https://craftdesk.ai';
50
- }
51
-
52
- // Check if it's a scoped craft and has a configured registry
53
- if (craftName.startsWith('@') && craftDesk.registries) {
54
- const scope = craftName.split('/')[0];
55
-
56
- // Look for a registry with matching scope
57
- for (const [name, registry] of Object.entries(craftDesk.registries)) {
58
- if (registry.scope === scope) {
59
- return registry.url;
60
- }
61
- }
62
- }
63
-
64
- // Check if there's a default registry configured
65
- if (craftDesk.registries?.default) {
66
- return craftDesk.registries.default.url;
67
- }
68
-
69
- // Final fallback
70
- return 'https://craftdesk.ai';
71
- }
72
-
73
- /**
74
- * Retrieves authentication token for a registry from environment variables
75
- *
76
- * Looks for tokens in environment variables with the pattern:
77
- * CRAFTDESK_AUTH_{REGISTRY_NAME_UPPERCASE}
78
- *
79
- * Hyphens in registry names are converted to underscores.
80
- *
81
- * @param registryName - The name of the registry from craftdesk.json
82
- * @returns The auth token if found, null otherwise
83
- *
84
- * @example
85
- * ```typescript
86
- * // For registry named 'company-private', looks for CRAFTDESK_AUTH_COMPANY_PRIVATE
87
- * const token = await configManager.getAuthToken('company-private');
88
- *
89
- * // Usage in shell:
90
- * // export CRAFTDESK_AUTH_COMPANY_PRIVATE=token_abc123
91
- * ```
92
- */
93
- async getAuthToken(registryName: string): Promise<string | null> {
94
- const craftDesk = await this.getCraftDeskJson();
95
-
96
- if (!craftDesk?.registries?.[registryName]) {
97
- return null;
98
- }
99
-
100
- // Auth tokens should be in environment variables for security
101
- // e.g., CRAFTDESK_AUTH_COMPANY_PRIVATE
102
- const envVar = `CRAFTDESK_AUTH_${registryName.toUpperCase().replace(/-/g, '_')}`;
103
- return process.env[envVar] || null;
104
- }
105
-
106
- /**
107
- * Returns the installation path for crafts
108
- *
109
- * All crafts are installed to the .claude directory in the project root.
110
- *
111
- * @returns The relative path to the installation directory
112
- */
113
- getInstallPath(): string {
114
- // Always use .claude directory in project root
115
- return '.claude';
116
- }
117
-
118
- /**
119
- * Reads and caches the craftdesk.json file from the current working directory
120
- *
121
- * The file is cached after the first read for performance. Returns null if
122
- * the file doesn't exist or contains invalid JSON.
123
- *
124
- * @returns The parsed craftdesk.json content, or null if not found/invalid
125
- *
126
- * @example
127
- * ```typescript
128
- * const config = await configManager.getCraftDeskJson();
129
- * if (config) {
130
- * console.log(`Project: ${config.name}@${config.version}`);
131
- * }
132
- * ```
133
- */
134
- async getCraftDeskJson(): Promise<CraftDeskJson | null> {
135
- if (this.craftDeskJson) {
136
- return this.craftDeskJson;
137
- }
138
-
139
- try {
140
- const craftDeskPath = path.join(process.cwd(), 'craftdesk.json');
141
- const content = await fs.readFile(craftDeskPath, 'utf-8');
142
- this.craftDeskJson = JSON.parse(content);
143
- return this.craftDeskJson;
144
- } catch (error) {
145
- return null;
146
- }
147
- }
148
- }
149
-
150
- export const configManager = new ConfigManager();
@@ -1,373 +0,0 @@
1
- import path from 'path';
2
- import fs from 'fs-extra';
3
- import { execSync } from 'child_process';
4
- import { logger } from '../utils/logger';
5
- import { CraftDeskJson, DependencyConfig } from '../types/craftdesk-json';
6
-
7
- /**
8
- * Information about a git dependency
9
- */
10
- export interface GitDependencyInfo {
11
- /** Git repository URL */
12
- url: string;
13
- /** Optional branch name */
14
- branch?: string;
15
- /** Optional tag name */
16
- tag?: string;
17
- /** Optional commit hash */
18
- commit?: string;
19
- /** Optional subdirectory path for monorepos */
20
- path?: string;
21
- /** Optional direct file path (e.g., skill.md, agent.md) */
22
- file?: string;
23
- /** Parsed craftdesk.json from the repository */
24
- craftDeskJson?: CraftDeskJson;
25
- /** Resolved full commit hash */
26
- resolvedCommit?: string;
27
- }
28
-
29
- /**
30
- * Resolves git dependencies and extracts their metadata
31
- *
32
- * Handles cloning git repositories, reading craftdesk.json, inferring craft types
33
- * when metadata is missing, and supporting monorepo structures with subdirectory paths.
34
- *
35
- * @example
36
- * ```typescript
37
- * const resolver = new GitResolver();
38
- *
39
- * // Resolve a git dependency
40
- * const info = await resolver.resolveGitDependency({
41
- * url: 'https://github.com/company/auth.git',
42
- * branch: 'main'
43
- * });
44
- *
45
- * console.log(info.craftDeskJson.name); // 'auth-skill'
46
- * console.log(info.resolvedCommit); // Full commit hash
47
- * ```
48
- */
49
- export class GitResolver {
50
- private tempDir: string;
51
-
52
- constructor() {
53
- this.tempDir = path.join(process.cwd(), '.craftdesk', 'tmp');
54
- }
55
-
56
- /**
57
- * Resolves a git dependency by cloning and reading its craftdesk.json
58
- *
59
- * Process:
60
- * 1. Clones the repository (shallow clone for efficiency)
61
- * 2. Checks out specified branch/tag/commit
62
- * 3. Reads craftdesk.json from repository, subdirectory, or infers from direct file
63
- * 4. Generates minimal metadata if craftdesk.json is missing
64
- * 5. Infers craft type from marker files, direct file extension, or directory structure
65
- *
66
- * @param gitInfo - Git dependency information
67
- * @returns Enhanced git info with craftdesk.json and resolved commit
68
- *
69
- * @example
70
- * ```typescript
71
- * // With craftdesk.json in repository
72
- * const info = await resolver.resolveGitDependency({
73
- * url: 'https://github.com/user/skill.git',
74
- * tag: 'v1.0.0'
75
- * });
76
- *
77
- * // Monorepo with subdirectory
78
- * const info = await resolver.resolveGitDependency({
79
- * url: 'https://github.com/company/monorepo.git',
80
- * tag: 'v3.0.0',
81
- * path: 'crafts/auth'
82
- * });
83
- *
84
- * // Direct file reference
85
- * const info = await resolver.resolveGitDependency({
86
- * url: 'https://github.com/user/repo.git',
87
- * branch: 'main',
88
- * file: 'rspec-dry-agent.md'
89
- * });
90
- * ```
91
- */
92
- async resolveGitDependency(gitInfo: GitDependencyInfo): Promise<GitDependencyInfo> {
93
- const repoTempDir = path.join(this.tempDir, `git-${Date.now()}`);
94
-
95
- try {
96
- await fs.ensureDir(repoTempDir);
97
-
98
- // Clone the repository
99
- logger.debug(`Cloning ${gitInfo.url} to analyze dependencies...`);
100
-
101
- let cloneCmd = `git clone --depth 1`;
102
- if (gitInfo.branch) {
103
- cloneCmd += ` -b ${gitInfo.branch}`;
104
- } else if (gitInfo.tag) {
105
- cloneCmd += ` -b ${gitInfo.tag}`;
106
- }
107
- cloneCmd += ` ${gitInfo.url} ${repoTempDir}`;
108
-
109
- execSync(cloneCmd, { stdio: 'pipe' });
110
-
111
- // If specific commit, checkout that commit
112
- if (gitInfo.commit) {
113
- execSync(`cd ${repoTempDir} && git fetch --unshallow && git checkout ${gitInfo.commit}`, { stdio: 'pipe' });
114
- }
115
-
116
- // Get the actual commit hash for lockfile
117
- const resolvedCommit = execSync(`cd ${repoTempDir} && git rev-parse HEAD`, { encoding: 'utf8' }).trim();
118
- gitInfo.resolvedCommit = resolvedCommit;
119
-
120
- // Handle direct file reference
121
- if (gitInfo.file) {
122
- const filePath = path.join(repoTempDir, gitInfo.file);
123
-
124
- if (await fs.pathExists(filePath)) {
125
- logger.debug(`Found direct file reference: ${gitInfo.file}`);
126
-
127
- // Try to find craftdesk.json to get type and metadata
128
- let craftType: 'skill' | 'agent' | 'command' | 'hook' | 'plugin' | undefined;
129
- let craftName: string | undefined;
130
- let craftVersion: string | undefined;
131
- let craftDeps: Record<string, any> | undefined;
132
-
133
- // Check for craftdesk.json in same directory as file
134
- const fileDir = path.dirname(gitInfo.file);
135
- const fileDirCraftPath = path.join(repoTempDir, fileDir, 'craftdesk.json');
136
-
137
- // Check for craftdesk.json in repo root
138
- const rootCraftPath = path.join(repoTempDir, 'craftdesk.json');
139
-
140
- if (await fs.pathExists(fileDirCraftPath)) {
141
- const content = await fs.readFile(fileDirCraftPath, 'utf-8');
142
- const craftJson = JSON.parse(content);
143
- logger.debug(`Found craftdesk.json in file directory: ${craftJson.name}@${craftJson.version}`);
144
- craftType = craftJson.type;
145
- craftName = craftJson.name;
146
- craftVersion = craftJson.version;
147
- craftDeps = craftJson.dependencies;
148
- } else if (await fs.pathExists(rootCraftPath)) {
149
- const content = await fs.readFile(rootCraftPath, 'utf-8');
150
- const craftJson = JSON.parse(content);
151
- logger.debug(`Found craftdesk.json in repo root: ${craftJson.name}@${craftJson.version}`);
152
- craftType = craftJson.type;
153
- craftName = craftJson.name;
154
- craftVersion = craftJson.version;
155
- craftDeps = craftJson.dependencies;
156
- }
157
-
158
- // If no craftdesk.json found, infer from filename
159
- if (!craftType) {
160
- craftType = this.inferCraftTypeFromFilename(gitInfo.file);
161
- }
162
-
163
- // Create minimal craftdesk.json from file
164
- gitInfo.craftDeskJson = {
165
- name: craftName || path.basename(gitInfo.file, path.extname(gitInfo.file)),
166
- version: craftVersion || gitInfo.tag || gitInfo.branch || resolvedCommit.substring(0, 7),
167
- type: craftType,
168
- description: `Git file dependency from ${gitInfo.url}/${gitInfo.file}`,
169
- dependencies: craftDeps || {}
170
- };
171
- } else {
172
- throw new Error(`File not found: ${gitInfo.file} in ${gitInfo.url}`);
173
- }
174
- } else {
175
- // Look for craftdesk.json in the specified path or root
176
- const craftPath = gitInfo.path
177
- ? path.join(repoTempDir, gitInfo.path, 'craftdesk.json')
178
- : path.join(repoTempDir, 'craftdesk.json');
179
-
180
- if (await fs.pathExists(craftPath)) {
181
- const content = await fs.readFile(craftPath, 'utf-8');
182
- gitInfo.craftDeskJson = JSON.parse(content);
183
- logger.debug(`Found craftdesk.json in git repository: ${gitInfo.craftDeskJson!.name}@${gitInfo.craftDeskJson!.version}`);
184
- } else {
185
- // No craftdesk.json found - create a minimal one
186
- logger.warn(`No craftdesk.json found in ${gitInfo.url}${gitInfo.path ? `#path:${gitInfo.path}` : ''}`);
187
-
188
- // Try to infer the type from directory structure
189
- const type = await this.inferCraftType(repoTempDir, gitInfo.path);
190
-
191
- gitInfo.craftDeskJson = {
192
- name: path.basename(gitInfo.url, '.git'),
193
- version: gitInfo.tag || gitInfo.branch || resolvedCommit.substring(0, 7),
194
- type: type,
195
- description: `Git dependency from ${gitInfo.url}`,
196
- dependencies: {}
197
- };
198
- }
199
- }
200
-
201
- return gitInfo;
202
-
203
- } finally {
204
- // Clean up temp directory
205
- await fs.remove(repoTempDir);
206
- }
207
- }
208
-
209
- /**
210
- * Infers the craft type from a filename
211
- *
212
- * Type inference strategy:
213
- * 1. Check filename for type indicators: agent, skill, command, hook
214
- * 2. Check file extension patterns
215
- * 3. Default to 'skill' if no indicators found
216
- *
217
- * @param filename - The filename to analyze
218
- * @returns The inferred craft type
219
- * @private
220
- */
221
- private inferCraftTypeFromFilename(filename: string): 'skill' | 'agent' | 'command' | 'hook' | 'plugin' {
222
- const lower = filename.toLowerCase();
223
-
224
- // Check filename for type indicators
225
- if (lower.includes('agent')) return 'agent';
226
- if (lower.includes('command')) return 'command';
227
- if (lower.includes('hook')) return 'hook';
228
- if (lower.includes('plugin')) return 'plugin';
229
- if (lower.includes('skill')) return 'skill';
230
-
231
- // Default to skill
232
- return 'skill';
233
- }
234
-
235
- /**
236
- * Infers the craft type from the repository structure
237
- *
238
- * Type inference strategy:
239
- * 1. Check for marker files: SKILL.md, AGENT.md, COMMAND.md, HOOK.md
240
- * 2. Check directory name for type indicators
241
- * 3. Default to 'skill' if no indicators found
242
- *
243
- * @param repoPath - Path to the cloned repository
244
- * @param subPath - Optional subdirectory to check
245
- * @returns The inferred craft type
246
- * @private
247
- */
248
- private async inferCraftType(repoPath: string, subPath?: string): Promise<'skill' | 'agent' | 'command' | 'hook' | 'plugin'> {
249
- const checkPath = subPath ? path.join(repoPath, subPath) : repoPath;
250
-
251
- // Check for type-indicating files
252
- const files = await fs.readdir(checkPath);
253
-
254
- // Look for SKILL.md, AGENT.md, COMMAND.md, HOOK.md, PLUGIN.md
255
- if (files.includes('SKILL.md')) return 'skill';
256
- if (files.includes('AGENT.md')) return 'agent';
257
- if (files.includes('COMMAND.md')) return 'command';
258
- if (files.includes('HOOK.md')) return 'hook';
259
- if (files.includes('PLUGIN.md')) return 'plugin';
260
-
261
- // Check directory name hints
262
- const dirName = subPath ? path.basename(subPath) : path.basename(repoPath);
263
- if (dirName.includes('skill')) return 'skill';
264
- if (dirName.includes('agent')) return 'agent';
265
- if (dirName.includes('command')) return 'command';
266
- if (dirName.includes('hook')) return 'hook';
267
- if (dirName.includes('plugin')) return 'plugin';
268
-
269
- // Default to skill
270
- return 'skill';
271
- }
272
-
273
- /**
274
- * Resolves all dependencies including transitive dependencies from git sources
275
- *
276
- * Uses a breadth-first traversal algorithm to resolve dependencies recursively.
277
- * Tracks visited packages to avoid circular dependencies.
278
- *
279
- * @param dependencies - Map of dependency names to version strings or config objects
280
- * @returns Object containing resolved dependencies and generated lockfile
281
- *
282
- * @example
283
- * ```typescript
284
- * const result = await resolver.resolveAllDependencies({
285
- * 'ruby-on-rails': '^7.0.0',
286
- * 'custom-auth': {
287
- * git: 'https://github.com/company/auth.git',
288
- * branch: 'main'
289
- * }
290
- * });
291
- *
292
- * console.log(result.resolved); // All dependencies including transitive
293
- * console.log(result.lockfile); // Generated lockfile structure
294
- * ```
295
- */
296
- async resolveAllDependencies(dependencies: Record<string, string | DependencyConfig>): Promise<{
297
- resolved: Record<string, any>;
298
- lockfile: any;
299
- }> {
300
- const resolved: Record<string, any> = {};
301
- // Queue of dependencies to resolve (breadth-first traversal)
302
- const toResolve: Array<[string, string | DependencyConfig]> = Object.entries(dependencies);
303
- // Track visited packages to prevent infinite loops from circular dependencies
304
- const visited = new Set<string>();
305
-
306
- // Process queue until all dependencies are resolved
307
- while (toResolve.length > 0) {
308
- const [name, dep] = toResolve.shift()!;
309
-
310
- // Skip if already resolved (handles duplicates and circular deps)
311
- if (visited.has(name)) continue;
312
- visited.add(name);
313
-
314
- // Determine if this is a git or registry dependency
315
- if (typeof dep === 'object' && dep.git) {
316
- // Git dependency - clone repository and extract metadata
317
- const gitInfo = await this.resolveGitDependency({
318
- url: dep.git,
319
- branch: dep.branch,
320
- tag: dep.tag,
321
- commit: dep.commit,
322
- path: dep.path,
323
- file: dep.file
324
- });
325
-
326
- resolved[name] = {
327
- version: gitInfo.craftDeskJson?.version || '0.0.0',
328
- resolved: gitInfo.url,
329
- integrity: gitInfo.resolvedCommit || 'git',
330
- type: gitInfo.craftDeskJson?.type || 'skill',
331
- author: gitInfo.craftDeskJson?.author || 'git',
332
- git: gitInfo.url,
333
- ...(gitInfo.branch && { branch: gitInfo.branch }),
334
- ...(gitInfo.tag && { tag: gitInfo.tag }),
335
- ...(gitInfo.commit && { commit: gitInfo.resolvedCommit }),
336
- ...(gitInfo.path && { path: gitInfo.path }),
337
- ...(gitInfo.file && { file: gitInfo.file }),
338
- dependencies: gitInfo.craftDeskJson?.dependencies || {}
339
- };
340
-
341
- // Add transitive dependencies to the resolution queue
342
- // This enables recursive dependency resolution
343
- if (gitInfo.craftDeskJson?.dependencies) {
344
- for (const [depName, depVersion] of Object.entries(gitInfo.craftDeskJson.dependencies)) {
345
- // Only add if not already visited (avoids redundant resolution)
346
- if (!visited.has(depName)) {
347
- toResolve.push([depName, depVersion]);
348
- }
349
- }
350
- }
351
- } else {
352
- // Registry dependency - mark for resolution by registry client
353
- // The actual version resolution happens via the registry API
354
- resolved[name] = {
355
- needsResolution: true,
356
- version: typeof dep === 'string' ? dep : dep.version,
357
- registry: typeof dep === 'object' ? dep.registry : undefined
358
- };
359
- }
360
- }
361
-
362
- return {
363
- resolved,
364
- lockfile: {
365
- version: '1.0.0',
366
- lockfileVersion: 1,
367
- crafts: resolved
368
- }
369
- };
370
- }
371
- }
372
-
373
- export const gitResolver = new GitResolver();