@launch77/cli 1.2.0 → 1.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 (215) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cli.js +8 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/infrastructure/git.d.ts +37 -0
  5. package/dist/infrastructure/git.d.ts.map +1 -0
  6. package/dist/infrastructure/git.js +82 -0
  7. package/dist/infrastructure/git.js.map +1 -0
  8. package/dist/infrastructure/github.d.ts +43 -0
  9. package/dist/infrastructure/github.d.ts.map +1 -0
  10. package/dist/infrastructure/github.js +89 -0
  11. package/dist/infrastructure/github.js.map +1 -0
  12. package/dist/infrastructure/template-generator.d.ts +1 -1
  13. package/dist/infrastructure/template-generator.d.ts.map +1 -1
  14. package/dist/infrastructure/template.d.ts +5 -0
  15. package/dist/infrastructure/template.d.ts.map +1 -1
  16. package/dist/infrastructure/template.js +11 -0
  17. package/dist/infrastructure/template.js.map +1 -1
  18. package/dist/modules/app/commands/create-app.js +1 -1
  19. package/dist/modules/app/commands/create-app.js.map +1 -1
  20. package/dist/modules/app/commands/delete-app.js +1 -1
  21. package/dist/modules/app/commands/delete-app.js.map +1 -1
  22. package/dist/modules/app/services/app-svc.d.ts +1 -1
  23. package/dist/modules/app/services/app-svc.d.ts.map +1 -1
  24. package/dist/modules/app/services/manifest-svc.d.ts +1 -1
  25. package/dist/modules/app/services/manifest-svc.d.ts.map +1 -1
  26. package/dist/modules/catalog/config/catalog-config.test.js +1 -1
  27. package/dist/modules/catalog/config/catalog-config.test.js.map +1 -1
  28. package/dist/modules/catalog/schemas/catalog-ui-components.schema.json +2 -18
  29. package/dist/modules/git/commands/git-connect.d.ts +3 -0
  30. package/dist/modules/git/commands/git-connect.d.ts.map +1 -0
  31. package/dist/modules/git/commands/git-connect.js +156 -0
  32. package/dist/modules/git/commands/git-connect.js.map +1 -0
  33. package/dist/modules/git/errors/git-errors.d.ts +21 -0
  34. package/dist/modules/git/errors/git-errors.d.ts.map +1 -0
  35. package/dist/modules/git/errors/git-errors.js +41 -0
  36. package/dist/modules/git/errors/git-errors.js.map +1 -0
  37. package/dist/modules/git/index.d.ts +5 -0
  38. package/dist/modules/git/index.d.ts.map +1 -0
  39. package/dist/modules/git/index.js +8 -0
  40. package/dist/modules/git/index.js.map +1 -0
  41. package/dist/modules/git/services/git-service.d.ts +24 -0
  42. package/dist/modules/git/services/git-service.d.ts.map +1 -0
  43. package/dist/modules/git/services/git-service.js +56 -0
  44. package/dist/modules/git/services/git-service.js.map +1 -0
  45. package/dist/modules/git/services/github-service.d.ts +27 -0
  46. package/dist/modules/git/services/github-service.d.ts.map +1 -0
  47. package/dist/modules/git/services/github-service.js +45 -0
  48. package/dist/modules/git/services/github-service.js.map +1 -0
  49. package/dist/modules/plugin/commands/plugin-create.d.ts +3 -0
  50. package/dist/modules/plugin/commands/plugin-create.d.ts.map +1 -0
  51. package/dist/modules/plugin/commands/plugin-create.js +59 -0
  52. package/dist/modules/plugin/commands/plugin-create.js.map +1 -0
  53. package/dist/modules/plugin/commands/plugin-install.d.ts.map +1 -1
  54. package/dist/modules/plugin/commands/plugin-install.js +9 -24
  55. package/dist/modules/plugin/commands/plugin-install.js.map +1 -1
  56. package/dist/modules/plugin/errors/plugin-errors.d.ts +24 -1
  57. package/dist/modules/plugin/errors/plugin-errors.d.ts.map +1 -1
  58. package/dist/modules/plugin/errors/plugin-errors.js +79 -6
  59. package/dist/modules/plugin/errors/plugin-errors.js.map +1 -1
  60. package/dist/modules/plugin/index.d.ts +4 -2
  61. package/dist/modules/plugin/index.d.ts.map +1 -1
  62. package/dist/modules/plugin/index.js +4 -2
  63. package/dist/modules/plugin/index.js.map +1 -1
  64. package/dist/modules/plugin/lib/plugin-registry.d.ts +6 -12
  65. package/dist/modules/plugin/lib/plugin-registry.d.ts.map +1 -1
  66. package/dist/modules/plugin/lib/plugin-registry.js +13 -30
  67. package/dist/modules/plugin/lib/plugin-registry.js.map +1 -1
  68. package/dist/modules/plugin/lib/plugin-resolver.d.ts +76 -0
  69. package/dist/modules/plugin/lib/plugin-resolver.d.ts.map +1 -0
  70. package/dist/modules/plugin/lib/plugin-resolver.js +128 -0
  71. package/dist/modules/plugin/lib/plugin-resolver.js.map +1 -0
  72. package/dist/modules/plugin/lib/plugin-resolver.test.d.ts +2 -0
  73. package/dist/modules/plugin/lib/plugin-resolver.test.d.ts.map +1 -0
  74. package/dist/modules/plugin/lib/plugin-resolver.test.js +175 -0
  75. package/dist/modules/plugin/lib/plugin-resolver.test.js.map +1 -0
  76. package/dist/modules/plugin/services/plugin-create-service.d.ts +16 -0
  77. package/dist/modules/plugin/services/plugin-create-service.d.ts.map +1 -0
  78. package/dist/modules/plugin/services/plugin-create-service.js +47 -0
  79. package/dist/modules/plugin/services/plugin-create-service.js.map +1 -0
  80. package/dist/modules/plugin/services/plugin-svc.d.ts +8 -3
  81. package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
  82. package/dist/modules/plugin/services/plugin-svc.js +96 -15
  83. package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
  84. package/dist/modules/release/commands/release-init.d.ts +3 -0
  85. package/dist/modules/release/commands/release-init.d.ts.map +1 -0
  86. package/dist/modules/release/commands/release-init.js +92 -0
  87. package/dist/modules/release/commands/release-init.js.map +1 -0
  88. package/dist/modules/release/errors/release-errors.d.ts +7 -0
  89. package/dist/modules/release/errors/release-errors.d.ts.map +1 -0
  90. package/dist/modules/release/errors/release-errors.js +13 -0
  91. package/dist/modules/release/errors/release-errors.js.map +1 -0
  92. package/dist/modules/release/index.d.ts +4 -0
  93. package/dist/modules/release/index.d.ts.map +1 -0
  94. package/dist/modules/release/index.js +7 -0
  95. package/dist/modules/release/index.js.map +1 -0
  96. package/dist/modules/release/services/release-service.d.ts +34 -0
  97. package/dist/modules/release/services/release-service.d.ts.map +1 -0
  98. package/dist/modules/release/services/release-service.js +154 -0
  99. package/dist/modules/release/services/release-service.js.map +1 -0
  100. package/dist/modules/workspace/commands/init-workspace.d.ts.map +1 -1
  101. package/dist/modules/workspace/commands/init-workspace.js +4 -5
  102. package/dist/modules/workspace/commands/init-workspace.js.map +1 -1
  103. package/dist/modules/workspace/services/workspace-service.d.ts +2 -1
  104. package/dist/modules/workspace/services/workspace-service.d.ts.map +1 -1
  105. package/dist/modules/workspace/services/workspace-service.js +27 -1
  106. package/dist/modules/workspace/services/workspace-service.js.map +1 -1
  107. package/dist/templates/plugin/README.md.hbs +39 -0
  108. package/dist/{plugins/theme/package.json → templates/plugin/package.json.hbs} +5 -3
  109. package/dist/templates/plugin/plugin.json.hbs +7 -0
  110. package/dist/templates/plugin/src/generator.ts.hbs +64 -0
  111. package/dist/templates/plugin/templates/src/.gitkeep +0 -0
  112. package/dist/templates/plugin/tsconfig.json +10 -0
  113. package/dist/{plugins/theme → templates/plugin}/tsup.config.ts +0 -1
  114. package/dist/templates/workspace/.github/workflows/ci.yml +102 -0
  115. package/dist/templates/workspace/package.json +16 -1
  116. package/dist/templates/workspace/turbo.json +5 -0
  117. package/dist/utils/launch77-context.d.ts +1 -1
  118. package/dist/utils/launch77-context.d.ts.map +1 -1
  119. package/dist/utils/launch77-context.js +25 -2
  120. package/dist/utils/launch77-context.js.map +1 -1
  121. package/dist/utils/launch77-validation.d.ts +1 -1
  122. package/dist/utils/launch77-validation.d.ts.map +1 -1
  123. package/dist/utils/string.d.ts +13 -0
  124. package/dist/utils/string.d.ts.map +1 -0
  125. package/dist/utils/string.js +18 -0
  126. package/dist/utils/string.js.map +1 -0
  127. package/package.json +7 -10
  128. package/src/cli.ts +10 -1
  129. package/src/infrastructure/git.ts +86 -0
  130. package/src/infrastructure/github.ts +111 -0
  131. package/src/infrastructure/template-generator.ts +1 -1
  132. package/src/infrastructure/template.ts +14 -0
  133. package/src/modules/app/commands/create-app.ts +1 -1
  134. package/src/modules/app/commands/delete-app.ts +1 -1
  135. package/src/modules/app/services/app-svc.ts +1 -1
  136. package/src/modules/app/services/manifest-svc.ts +1 -1
  137. package/src/modules/catalog/config/catalog-config.test.ts +1 -1
  138. package/src/modules/git/commands/git-connect.ts +183 -0
  139. package/src/modules/git/errors/git-errors.ts +44 -0
  140. package/src/modules/git/index.ts +9 -0
  141. package/src/modules/git/services/git-service.ts +63 -0
  142. package/src/modules/git/services/github-service.ts +52 -0
  143. package/src/modules/plugin/commands/plugin-create.ts +68 -0
  144. package/src/modules/plugin/commands/plugin-install.ts +9 -26
  145. package/src/modules/plugin/errors/plugin-errors.ts +87 -6
  146. package/src/modules/plugin/index.ts +4 -2
  147. package/src/modules/plugin/lib/plugin-registry.ts +14 -37
  148. package/src/modules/plugin/lib/plugin-resolver.test.ts +215 -0
  149. package/src/modules/plugin/lib/plugin-resolver.ts +160 -0
  150. package/src/modules/plugin/services/plugin-create-service.ts +69 -0
  151. package/src/modules/plugin/services/plugin-svc.ts +108 -15
  152. package/src/modules/release/commands/release-init.ts +102 -0
  153. package/src/modules/release/errors/release-errors.ts +13 -0
  154. package/src/modules/release/index.ts +8 -0
  155. package/src/modules/release/services/release-service.ts +170 -0
  156. package/src/modules/workspace/commands/init-workspace.ts +4 -6
  157. package/src/modules/workspace/services/workspace-service.ts +30 -1
  158. package/src/utils/launch77-context.ts +29 -3
  159. package/src/utils/launch77-validation.ts +1 -1
  160. package/src/utils/string.ts +17 -0
  161. package/templates/plugin/README.md.hbs +39 -0
  162. package/templates/plugin/package.json.hbs +34 -0
  163. package/templates/plugin/plugin.json.hbs +7 -0
  164. package/templates/plugin/src/generator.ts.hbs +64 -0
  165. package/templates/plugin/templates/src/.gitkeep +0 -0
  166. package/templates/plugin/tsconfig.json +10 -0
  167. package/templates/plugin/tsup.config.ts +9 -0
  168. package/templates/workspace/.github/workflows/ci.yml +102 -0
  169. package/templates/workspace/package.json +5 -0
  170. package/templates/workspace/turbo.json +5 -0
  171. package/tests/integration/cli.test.ts +25 -0
  172. package/tests/integration/setup.ts +20 -0
  173. package/vitest.config.ts +9 -0
  174. package/vitest.integration.config.ts +9 -0
  175. package/dist/app-templates/webapp/.env.ci +0 -6
  176. package/dist/app-templates/webapp/.env.example +0 -9
  177. package/dist/app-templates/webapp/.eslintrc.json +0 -6
  178. package/dist/app-templates/webapp/README.md.hbs +0 -80
  179. package/dist/app-templates/webapp/app/about/page.tsx.hbs +0 -41
  180. package/dist/app-templates/webapp/app/dashboard/page.tsx.hbs +0 -51
  181. package/dist/app-templates/webapp/app/globals.css +0 -31
  182. package/dist/app-templates/webapp/app/layout.tsx.hbs +0 -26
  183. package/dist/app-templates/webapp/app/page.tsx.hbs +0 -30
  184. package/dist/app-templates/webapp/next.config.js +0 -99
  185. package/dist/app-templates/webapp/package.json.hbs +0 -30
  186. package/dist/app-templates/webapp/postcss.config.js +0 -6
  187. package/dist/app-templates/webapp/tailwind.config.ts +0 -24
  188. package/dist/app-templates/webapp/tsconfig.json +0 -29
  189. package/dist/app-templates/webapp/vercel.json.hbs +0 -7
  190. package/dist/modules/catalog/schemas/schemas/catalog-ui-components.schema.json +0 -145
  191. package/dist/plugins/theme/plugin.json +0 -9
  192. package/dist/plugins/theme/src/generator.ts +0 -92
  193. package/dist/plugins/theme/src/utils/config-modifier.ts +0 -142
  194. package/dist/plugins/theme/src/utils/css-modifier.ts +0 -89
  195. package/dist/plugins/theme/templates/app/theme-test/page.tsx +0 -156
  196. package/dist/plugins/theme/templates/src/modules/theme/README.md +0 -209
  197. package/dist/plugins/theme/templates/src/modules/theme/config/brand.css +0 -23
  198. package/dist/plugins/theme/tsconfig.json +0 -14
  199. package/dist/templates/templates/startup/apps/.gitkeep +0 -8
  200. package/dist/templates/templates/workspace/.launch77/workspace.json +0 -3
  201. package/dist/templates/templates/workspace/README.md +0 -62
  202. package/dist/templates/templates/workspace/app-templates/.gitkeep +0 -1
  203. package/dist/templates/templates/workspace/apps/.gitkeep +0 -1
  204. package/dist/templates/templates/workspace/libraries/.gitkeep +0 -1
  205. package/dist/templates/templates/workspace/package.json +0 -31
  206. package/dist/templates/templates/workspace/plugins/.gitkeep +0 -1
  207. package/dist/templates/templates/workspace/tsconfig.json +0 -22
  208. package/dist/templates/templates/workspace/turbo.json +0 -25
  209. package/launch77-cli-1.2.0.tgz +0 -0
  210. package/src/modules/plugin/lib/launch77-workspace.code-workspace +0 -14
  211. /package/dist/templates/{templates/workspace → workspace}/.eslintignore +0 -0
  212. /package/dist/templates/{templates/workspace → workspace}/.eslintrc.js +0 -0
  213. /package/dist/templates/{templates/workspace → workspace}/.husky/pre-push +0 -0
  214. /package/dist/templates/{templates/workspace → workspace}/.lintstagedrc.json +0 -0
  215. /package/dist/templates/{templates/workspace → workspace}/.prettierrc +0 -0
@@ -0,0 +1,68 @@
1
+ /* eslint-disable no-console */
2
+ import * as path from 'path'
3
+
4
+ import chalk from 'chalk'
5
+ import { Command } from 'commander'
6
+ import ora from 'ora'
7
+
8
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
9
+ import { PluginCreateService } from '../services/plugin-create-service.js'
10
+
11
+ export function pluginCreateCommand(): Command {
12
+ const command = new Command('plugin:create')
13
+ .argument('<plugin-name>', 'Name of the plugin to create (e.g., analytics)')
14
+ .option('-d, --description <description>', 'Plugin description')
15
+ .description('Create a new plugin from template')
16
+ .action(async (pluginName: string, options) => {
17
+ try {
18
+ console.log(chalk.blue(`\n🔌 Creating plugin: ${pluginName}\n`))
19
+
20
+ // Detect context
21
+ const context = await detectLaunch77Context(process.cwd())
22
+
23
+ // Create plugin service
24
+ const pluginCreateService = new PluginCreateService()
25
+
26
+ // Create plugin
27
+ const spinner = ora('Generating plugin structure...').start()
28
+ let result
29
+ try {
30
+ result = await pluginCreateService.createPlugin(
31
+ {
32
+ pluginName,
33
+ description: options.description,
34
+ },
35
+ context
36
+ )
37
+ spinner.succeed('Plugin structure generated')
38
+ } catch (error) {
39
+ spinner.fail('Failed to generate plugin structure')
40
+ throw error
41
+ }
42
+
43
+ // Success message
44
+ const relativePath = path.relative(process.cwd(), result.pluginPath)
45
+ const displayPath = relativePath.startsWith('..') ? result.pluginPath : relativePath
46
+ console.log(chalk.green(`\n✅ Plugin created successfully at ${displayPath}`))
47
+
48
+ // Next steps
49
+ console.log(chalk.white('\n' + '─'.repeat(60) + '\n'))
50
+ console.log(chalk.cyan('📋 Next Steps:\n'))
51
+ const cdPath = path.relative(process.cwd(), result.pluginPath)
52
+ console.log(chalk.gray('1. Navigate to your plugin directory:'))
53
+ console.log(chalk.cyan(` cd ${cdPath}\n`))
54
+ console.log(chalk.gray('2. Implement your plugin logic in src/generator.ts\n'))
55
+ console.log(chalk.gray('3. Add any template files to templates/\n'))
56
+ console.log(chalk.gray('4. Build your plugin:'))
57
+ console.log(chalk.cyan(' npm run build\n'))
58
+ console.log(chalk.gray('5. Test your plugin:'))
59
+ console.log(chalk.cyan(` launch77 plugin:install ${pluginName}\n`))
60
+ } catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error)
62
+ console.error(chalk.red('Error:'), message)
63
+ process.exit(1)
64
+ }
65
+ })
66
+
67
+ return command
68
+ }
@@ -1,48 +1,31 @@
1
1
  /* eslint-disable no-console */
2
2
  import chalk from 'chalk'
3
3
  import { Command } from 'commander'
4
- import ora from 'ora'
5
4
 
6
- import { detectLaunch77Context } from '../../../utils/launch77-context.js'
5
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
7
6
  import { PluginService } from '../services/plugin-svc.js'
8
7
 
9
8
  export function pluginInstallCommand(): Command {
10
9
  const command = new Command('plugin:install')
11
- .argument('<plugin-name>', 'Name of the plugin to install (e.g., database)')
12
- .description('Install a plugin to the current app')
10
+ .argument('<plugin-name>', 'Name of the plugin to install (e.g., release, @org/plugin-name)')
11
+ .description('Install a plugin to the current package')
13
12
  .action(async (pluginName: string) => {
14
13
  try {
15
- console.log(chalk.blue(`\n🔌 Installing ${pluginName} plugin\n`))
16
-
17
14
  // Detect context
18
15
  const context = await detectLaunch77Context(process.cwd())
19
16
 
20
17
  // Create plugin service
21
18
  const pluginService = new PluginService()
22
19
 
23
- // Install files
24
- const filesSpinner = ora('Installing plugin files...').start()
25
- try {
26
- await pluginService.installPlugin({ pluginName }, context)
27
- filesSpinner.succeed('Plugin files installed')
28
- } catch (error) {
29
- filesSpinner.fail('Failed to install plugin')
30
- throw error
31
- }
32
-
33
- // Package.json update spinner
34
- const pkgSpinner = ora('Updating package.json...').start()
35
- pkgSpinner.succeed('package.json updated')
36
-
37
- // Dependencies spinner
38
- const installSpinner = ora('Installing dependencies...').start()
39
- installSpinner.succeed('Dependencies installed')
20
+ // Install plugin with transparent logging
21
+ // The service handles all logging internally
22
+ await pluginService.installPlugin({ pluginName }, context, console.log)
40
23
 
41
- // Success message (post-install hook handles custom messages)
42
- console.log(chalk.green(`\n✅ Plugin '${pluginName}' installed successfully!\n`))
24
+ // Success message
25
+ console.log(chalk.green(`✅ Plugin '${pluginName}' installed successfully!\n`))
43
26
  } catch (error) {
44
27
  const message = error instanceof Error ? error.message : String(error)
45
- console.error(chalk.red('Error:'), message)
28
+ console.error(chalk.red('\nError:'), message)
46
29
  process.exit(1)
47
30
  }
48
31
  })
@@ -16,20 +16,71 @@ export class InvalidPluginContextError extends Error {
16
16
 
17
17
  /**
18
18
  * Factory function to create standardized InvalidPluginContextError
19
- * for when plugin:install is run outside of an app directory
19
+ * for when plugin:install is run outside of a package directory
20
20
  */
21
21
  export function createInvalidContextError(currentLocation: string): InvalidPluginContextError {
22
22
  return new InvalidPluginContextError(
23
- `plugin:install must be run from within an app directory.
23
+ `plugin:install must be run from within a package directory.
24
24
 
25
25
  Current location: ${currentLocation}
26
- Expected: apps/<app-name>/
26
+ Expected: apps/<name>/, libraries/<name>/, plugins/<name>/, or app-templates/<name>/
27
27
 
28
- Navigate to an app directory:
28
+ Navigate to a package directory:
29
29
  cd apps/<app-name>/
30
+ cd libraries/<lib-name>/
31
+ cd plugins/<plugin-name>/
32
+ cd app-templates/<template-name>/`
33
+ )
34
+ }
35
+
36
+ /**
37
+ * Error when plugin.json is missing the required 'targets' field
38
+ */
39
+ export class MissingPluginTargetsError extends Error {
40
+ constructor(pluginName: string) {
41
+ super(`Plugin '${pluginName}' is missing the required 'targets' field in plugin.json.
42
+
43
+ The plugin.json file must include a 'targets' array specifying which package types
44
+ the plugin can be installed into.
45
+
46
+ Example plugin.json:
47
+ {
48
+ "name": "${pluginName}",
49
+ "version": "1.0.0",
50
+ "targets": ["app", "library", "plugin", "app-template"],
51
+ "pluginDependencies": {},
52
+ "libraryDependencies": {}
53
+ }`)
54
+ this.name = 'MissingPluginTargetsError'
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Factory function to create error when plugin targets don't match current location
60
+ */
61
+ export function createInvalidTargetError(pluginName: string, currentTarget: string, allowedTargets: string[]): InvalidPluginContextError {
62
+ const targetLocations = allowedTargets.map((target) => {
63
+ switch (target) {
64
+ case 'app':
65
+ return 'apps/<name>/'
66
+ case 'library':
67
+ return 'libraries/<name>/'
68
+ case 'plugin':
69
+ return 'plugins/<name>/'
70
+ case 'app-template':
71
+ return 'app-templates/<name>/'
72
+ default:
73
+ return target
74
+ }
75
+ })
76
+
77
+ return new InvalidPluginContextError(
78
+ `Plugin '${pluginName}' cannot be installed in a '${currentTarget}' package.
79
+
80
+ This plugin can only be installed in: ${allowedTargets.join(', ')}
30
81
 
31
- Or create an app first:
32
- launch77 app:create api <app-name>`
82
+ Allowed locations:
83
+ ${targetLocations.map((loc) => ` ${loc}`).join('\n')}`
33
84
  )
34
85
  }
35
86
 
@@ -42,3 +93,33 @@ export class PluginInstallationError extends Error {
42
93
  this.name = 'PluginInstallationError'
43
94
  }
44
95
  }
96
+
97
+ /**
98
+ * Error when plugin resolution fails
99
+ */
100
+ export class PluginResolutionError extends Error {
101
+ constructor(pluginName: string, reason: string) {
102
+ super(`Failed to resolve plugin '${pluginName}': ${reason}`)
103
+ this.name = 'PluginResolutionError'
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Error when npm package installation fails
109
+ */
110
+ export class NpmInstallationError extends Error {
111
+ constructor(
112
+ packageName: string,
113
+ public readonly cause?: Error
114
+ ) {
115
+ super(`Failed to install npm package '${packageName}'.
116
+
117
+ Please check:
118
+ - Your internet connection
119
+ - npm registry access (https://registry.npmjs.org)
120
+ - Package exists: https://www.npmjs.com/package/${packageName}
121
+
122
+ ${cause ? `\nOriginal error: ${cause.message}` : ''}`)
123
+ this.name = 'NpmInstallationError'
124
+ }
125
+ }
@@ -1,14 +1,16 @@
1
1
  // Services
2
2
  export { PluginService } from './services/plugin-svc.js'
3
+ export { PluginCreateService } from './services/plugin-create-service.js'
3
4
 
4
5
  // Types
5
6
  export type { InstallPluginRequest, InstallPluginResult, PluginMetadata, HookResult } from './types/plugin-types.js'
6
7
 
7
8
  // Errors
8
- export { PluginNotFoundError, InvalidPluginContextError, PluginInstallationError } from './errors/plugin-errors.js'
9
+ export { PluginNotFoundError, InvalidPluginContextError, PluginInstallationError, PluginResolutionError, NpmInstallationError } from './errors/plugin-errors.js'
9
10
 
10
11
  // Commands
11
12
  export { pluginInstallCommand } from './commands/plugin-install.js'
13
+ export { pluginCreateCommand } from './commands/plugin-create.js'
12
14
 
13
15
  // Utilities
14
- export { getPluginPath, pluginExists, listAvailablePlugins } from './lib/plugin-registry.js'
16
+ export { listAvailablePlugins } from './lib/plugin-registry.js'
@@ -1,55 +1,32 @@
1
1
  import * as path from 'path'
2
- import { fileURLToPath } from 'url'
3
2
 
4
3
  import fs from 'fs-extra'
5
4
 
6
5
  /**
7
- * Get the path to a plugin directory
8
- * Plugins are bundled with the CLI at dist/plugins/
6
+ * List all available plugins in the workspace
7
+ * Scans the workspace plugins/ directory for valid plugins
8
+ *
9
+ * @param workspaceRoot - The root directory of the Launch77 workspace
10
+ * @returns Array of plugin names found in the workspace
9
11
  */
10
- export function getPluginPath(pluginName: string): string {
11
- const __filename = fileURLToPath(import.meta.url)
12
- const __dirname = path.dirname(__filename)
12
+ export async function listAvailablePlugins(workspaceRoot: string): Promise<string[]> {
13
+ const pluginsDir = path.join(workspaceRoot, 'plugins')
13
14
 
14
- // From dist/modules/plugin/lib/ to dist/plugins/
15
- const baseDir = path.join(__dirname, '../../../plugins')
16
-
17
- return path.join(baseDir, pluginName)
18
- }
19
-
20
- /**
21
- * Check if a plugin exists
22
- * A plugin must have: dist/generator.js and plugin.json
23
- */
24
- export async function pluginExists(pluginName: string): Promise<boolean> {
25
- const pluginPath = getPluginPath(pluginName)
26
-
27
- const hasGenerator = await fs.pathExists(path.join(pluginPath, 'dist/generator.js'))
28
- const hasPluginJson = await fs.pathExists(path.join(pluginPath, 'plugin.json'))
29
-
30
- return hasGenerator && hasPluginJson
31
- }
32
-
33
- /**
34
- * List all available plugins
35
- */
36
- export async function listAvailablePlugins(): Promise<string[]> {
37
- const __filename = fileURLToPath(import.meta.url)
38
- const __dirname = path.dirname(__filename)
39
-
40
- const baseDir = path.join(__dirname, '../../../plugins')
41
-
42
- if (!(await fs.pathExists(baseDir))) {
15
+ if (!(await fs.pathExists(pluginsDir))) {
43
16
  return []
44
17
  }
45
18
 
46
- const items = await fs.readdir(baseDir, { withFileTypes: true })
19
+ const items = await fs.readdir(pluginsDir, { withFileTypes: true })
47
20
  const plugins: string[] = []
48
21
 
49
22
  for (const item of items) {
50
23
  if (item.isDirectory()) {
51
24
  // Verify it's a valid plugin
52
- if (await pluginExists(item.name)) {
25
+ const pluginPath = path.join(pluginsDir, item.name)
26
+ const hasPluginJson = await fs.pathExists(path.join(pluginPath, 'plugin.json'))
27
+ const hasGenerator = await fs.pathExists(path.join(pluginPath, 'dist/generator.js'))
28
+
29
+ if (hasPluginJson && hasGenerator) {
53
30
  plugins.push(item.name)
54
31
  }
55
32
  }
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import * as path from 'path'
3
+ import * as os from 'os'
4
+ import fs from 'fs-extra'
5
+
6
+ import { validatePluginInput, toNpmPackageName, resolvePluginLocation } from './plugin-resolver.js'
7
+
8
+ describe('Plugin Resolver', () => {
9
+ describe('validatePluginInput', () => {
10
+ it('should accept valid unscoped plugin names', () => {
11
+ expect(validatePluginInput('release')).toEqual({ isValid: true })
12
+ expect(validatePluginInput('my-plugin')).toEqual({ isValid: true })
13
+ expect(validatePluginInput('analytics-v2')).toEqual({ isValid: true })
14
+ })
15
+
16
+ it('should accept valid scoped npm packages', () => {
17
+ expect(validatePluginInput('@ibm/plugin-name')).toEqual({ isValid: true })
18
+ expect(validatePluginInput('@launch77-shared/plugin-release')).toEqual({ isValid: true })
19
+ expect(validatePluginInput('@org/analytics')).toEqual({ isValid: true })
20
+ })
21
+
22
+ it('should reject empty names', () => {
23
+ const result = validatePluginInput('')
24
+ expect(result.isValid).toBe(false)
25
+ expect(result.error).toBeDefined()
26
+ })
27
+
28
+ it('should reject whitespace-only names', () => {
29
+ const result = validatePluginInput(' ')
30
+ expect(result.isValid).toBe(false)
31
+ expect(result.error).toBeDefined()
32
+ })
33
+
34
+ it('should reject invalid scoped packages', () => {
35
+ const result1 = validatePluginInput('@invalid')
36
+ expect(result1.isValid).toBe(false)
37
+ expect(result1.error).toBeDefined()
38
+
39
+ const result2 = validatePluginInput('@/package')
40
+ expect(result2.isValid).toBe(false)
41
+ expect(result2.error).toBeDefined()
42
+
43
+ const result3 = validatePluginInput('@org/')
44
+ expect(result3.isValid).toBe(false)
45
+ expect(result3.error).toBeDefined()
46
+ })
47
+
48
+ it('should reject names with uppercase letters', () => {
49
+ const result = validatePluginInput('MyPlugin')
50
+ expect(result.isValid).toBe(false)
51
+ expect(result.error).toContain('lowercase')
52
+ })
53
+
54
+ it('should reject names starting with numbers', () => {
55
+ const result = validatePluginInput('123plugin')
56
+ expect(result.isValid).toBe(false)
57
+ expect(result.error).toBeDefined()
58
+ })
59
+
60
+ it('should reject names with special characters', () => {
61
+ const result1 = validatePluginInput('plugin_name')
62
+ expect(result1.isValid).toBe(false)
63
+
64
+ const result2 = validatePluginInput('plugin.name')
65
+ expect(result2.isValid).toBe(false)
66
+
67
+ const result3 = validatePluginInput('plugin name')
68
+ expect(result3.isValid).toBe(false)
69
+ })
70
+
71
+ it('should trim whitespace before validation', () => {
72
+ expect(validatePluginInput(' release ')).toEqual({ isValid: true })
73
+ expect(validatePluginInput(' @ibm/analytics ')).toEqual({ isValid: true })
74
+ })
75
+ })
76
+
77
+ describe('toNpmPackageName', () => {
78
+ it('should prefix unscoped names with @launch77-shared/plugin-', () => {
79
+ expect(toNpmPackageName('release')).toBe('@launch77-shared/plugin-release')
80
+ expect(toNpmPackageName('my-plugin')).toBe('@launch77-shared/plugin-my-plugin')
81
+ expect(toNpmPackageName('analytics-v2')).toBe('@launch77-shared/plugin-analytics-v2')
82
+ })
83
+
84
+ it('should return scoped packages as-is', () => {
85
+ expect(toNpmPackageName('@ibm/analytics')).toBe('@ibm/analytics')
86
+ expect(toNpmPackageName('@launch77-shared/plugin-release')).toBe('@launch77-shared/plugin-release')
87
+ expect(toNpmPackageName('@org/package')).toBe('@org/package')
88
+ })
89
+
90
+ it('should trim whitespace', () => {
91
+ expect(toNpmPackageName(' release ')).toBe('@launch77-shared/plugin-release')
92
+ expect(toNpmPackageName(' @ibm/analytics ')).toBe('@ibm/analytics')
93
+ })
94
+ })
95
+
96
+ describe('resolvePluginLocation', () => {
97
+ let tempDir: string
98
+
99
+ beforeEach(async () => {
100
+ // Create a temporary workspace directory
101
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-resolver-test-'))
102
+ await fs.ensureDir(path.join(tempDir, 'plugins'))
103
+ })
104
+
105
+ afterEach(async () => {
106
+ // Clean up
107
+ await fs.remove(tempDir)
108
+ })
109
+
110
+ it('should resolve local plugins first', async () => {
111
+ // Create a valid local plugin
112
+ const pluginPath = path.join(tempDir, 'plugins', 'my-plugin')
113
+ await fs.ensureDir(pluginPath)
114
+ await fs.ensureDir(path.join(pluginPath, 'dist'))
115
+ await fs.writeFile(path.join(pluginPath, 'plugin.json'), JSON.stringify({ name: 'my-plugin', version: '1.0.0' }))
116
+ await fs.writeFile(path.join(pluginPath, 'dist/generator.js'), 'console.log("test")')
117
+
118
+ const result = await resolvePluginLocation('my-plugin', tempDir)
119
+
120
+ expect(result).toEqual({
121
+ source: 'local',
122
+ resolvedName: 'my-plugin',
123
+ localPath: pluginPath,
124
+ })
125
+ })
126
+
127
+ it('should resolve to npm if local plugin does not exist', async () => {
128
+ const result = await resolvePluginLocation('release', tempDir)
129
+
130
+ expect(result).toEqual({
131
+ source: 'npm',
132
+ resolvedName: 'release',
133
+ npmPackage: '@launch77-shared/plugin-release',
134
+ })
135
+ })
136
+
137
+ it('should resolve to npm if local plugin is incomplete (missing plugin.json)', async () => {
138
+ // Create incomplete local plugin (no plugin.json)
139
+ const pluginPath = path.join(tempDir, 'plugins', 'incomplete')
140
+ await fs.ensureDir(pluginPath)
141
+ await fs.ensureDir(path.join(pluginPath, 'dist'))
142
+ await fs.writeFile(path.join(pluginPath, 'dist/generator.js'), 'console.log("test")')
143
+
144
+ const result = await resolvePluginLocation('incomplete', tempDir)
145
+
146
+ expect(result).toEqual({
147
+ source: 'npm',
148
+ resolvedName: 'incomplete',
149
+ npmPackage: '@launch77-shared/plugin-incomplete',
150
+ })
151
+ })
152
+
153
+ it('should resolve to npm if local plugin is incomplete (missing generator.js)', async () => {
154
+ // Create incomplete local plugin (no generator.js)
155
+ const pluginPath = path.join(tempDir, 'plugins', 'incomplete')
156
+ await fs.ensureDir(pluginPath)
157
+ await fs.writeFile(path.join(pluginPath, 'plugin.json'), JSON.stringify({ name: 'incomplete', version: '1.0.0' }))
158
+
159
+ const result = await resolvePluginLocation('incomplete', tempDir)
160
+
161
+ expect(result).toEqual({
162
+ source: 'npm',
163
+ resolvedName: 'incomplete',
164
+ npmPackage: '@launch77-shared/plugin-incomplete',
165
+ })
166
+ })
167
+
168
+ it('should always resolve scoped packages to npm', async () => {
169
+ // Even if a directory exists locally, scoped names go to npm
170
+ const pluginPath = path.join(tempDir, 'plugins', '@ibm')
171
+ await fs.ensureDir(pluginPath)
172
+
173
+ const result = await resolvePluginLocation('@ibm/analytics', tempDir)
174
+
175
+ expect(result).toEqual({
176
+ source: 'npm',
177
+ resolvedName: '@ibm/analytics',
178
+ npmPackage: '@ibm/analytics',
179
+ })
180
+ })
181
+
182
+ it('should resolve scoped @launch77-shared packages to npm', async () => {
183
+ const result = await resolvePluginLocation('@launch77-shared/plugin-release', tempDir)
184
+
185
+ expect(result).toEqual({
186
+ source: 'npm',
187
+ resolvedName: '@launch77-shared/plugin-release',
188
+ npmPackage: '@launch77-shared/plugin-release',
189
+ })
190
+ })
191
+
192
+ it('should handle plugins directory not existing', async () => {
193
+ // Remove plugins directory
194
+ await fs.remove(path.join(tempDir, 'plugins'))
195
+
196
+ const result = await resolvePluginLocation('release', tempDir)
197
+
198
+ expect(result).toEqual({
199
+ source: 'npm',
200
+ resolvedName: 'release',
201
+ npmPackage: '@launch77-shared/plugin-release',
202
+ })
203
+ })
204
+
205
+ it('should trim whitespace from plugin names', async () => {
206
+ const result = await resolvePluginLocation(' release ', tempDir)
207
+
208
+ expect(result).toEqual({
209
+ source: 'npm',
210
+ resolvedName: 'release',
211
+ npmPackage: '@launch77-shared/plugin-release',
212
+ })
213
+ })
214
+ })
215
+ })