@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,160 @@
1
+ import * as path from 'path'
2
+
3
+ import fs from 'fs-extra'
4
+ import { parsePluginName, isValidNpmPackageName } from '@launch77/plugin-runtime'
5
+
6
+ import type { ValidationResult } from '@launch77/plugin-runtime'
7
+
8
+ /**
9
+ * Plugin resolution result
10
+ */
11
+ export interface PluginResolution {
12
+ /** The source of the plugin */
13
+ source: 'local' | 'npm'
14
+ /** The resolved name/package to use */
15
+ resolvedName: string
16
+ /** The local path if source is 'local' */
17
+ localPath?: string
18
+ /** The npm package name if source is 'npm' */
19
+ npmPackage?: string
20
+ }
21
+
22
+ /**
23
+ * Validate plugin input name
24
+ *
25
+ * Accepts:
26
+ * - Unscoped names (e.g., "release", "my-plugin")
27
+ * - Scoped npm packages (e.g., "@ibm/plugin-name")
28
+ *
29
+ * Rejects:
30
+ * - Invalid formats
31
+ * - Empty strings
32
+ * - Names with invalid characters
33
+ *
34
+ * @param name - The plugin name to validate
35
+ * @returns ValidationResult with isValid and optional error message
36
+ *
37
+ * @example
38
+ * validatePluginInput('release') // { isValid: true }
39
+ * validatePluginInput('@ibm/analytics') // { isValid: true }
40
+ * validatePluginInput('@invalid') // { isValid: false, error: '...' }
41
+ */
42
+ export function validatePluginInput(name: string): ValidationResult {
43
+ if (!name || name.trim().length === 0) {
44
+ return {
45
+ isValid: false,
46
+ error: 'Plugin name cannot be empty',
47
+ }
48
+ }
49
+
50
+ const trimmedName = name.trim()
51
+
52
+ // Parse the name to determine type and validate
53
+ const parsed = parsePluginName(trimmedName)
54
+
55
+ if (!parsed.isValid) {
56
+ return {
57
+ isValid: false,
58
+ error: parsed.error,
59
+ }
60
+ }
61
+
62
+ // If it's scoped, it must be a valid npm package name
63
+ if (parsed.type === 'scoped') {
64
+ return isValidNpmPackageName(trimmedName)
65
+ }
66
+
67
+ // Unscoped names are valid for both local and npm
68
+ return { isValid: true }
69
+ }
70
+
71
+ /**
72
+ * Convert an unscoped plugin name to an npm package name
73
+ *
74
+ * Rules:
75
+ * - Unscoped names: prefix with @launch77-shared/plugin-
76
+ * - Scoped names: use as-is
77
+ *
78
+ * @param name - The plugin name (must be validated first)
79
+ * @returns The npm package name
80
+ *
81
+ * @example
82
+ * toNpmPackageName('release') // '@launch77-shared/plugin-release'
83
+ * toNpmPackageName('@ibm/analytics') // '@ibm/analytics'
84
+ */
85
+ export function toNpmPackageName(name: string): string {
86
+ const trimmedName = name.trim()
87
+
88
+ // If already scoped, use as-is
89
+ if (trimmedName.startsWith('@')) {
90
+ return trimmedName
91
+ }
92
+
93
+ // Otherwise, convert to @launch77-shared/plugin-<name>
94
+ return `@launch77-shared/plugin-${trimmedName}`
95
+ }
96
+
97
+ /**
98
+ * Resolve plugin location from name
99
+ *
100
+ * Resolution order:
101
+ * 1. Check local workspace plugins directory
102
+ * 2. Resolve to npm package name
103
+ *
104
+ * @param name - The plugin name to resolve
105
+ * @param workspaceRoot - The workspace root directory
106
+ * @returns PluginResolution with source and resolved location
107
+ *
108
+ * @example
109
+ * // Local plugin found
110
+ * await resolvePluginLocation('my-plugin', '/workspace')
111
+ * // { source: 'local', resolvedName: 'my-plugin', localPath: '/workspace/plugins/my-plugin' }
112
+ *
113
+ * // Not found locally, resolve to npm
114
+ * await resolvePluginLocation('release', '/workspace')
115
+ * // { source: 'npm', resolvedName: 'release', npmPackage: '@launch77-shared/plugin-release' }
116
+ *
117
+ * // Scoped package always resolves to npm
118
+ * await resolvePluginLocation('@ibm/analytics', '/workspace')
119
+ * // { source: 'npm', resolvedName: '@ibm/analytics', npmPackage: '@ibm/analytics' }
120
+ */
121
+ export async function resolvePluginLocation(name: string, workspaceRoot: string): Promise<PluginResolution> {
122
+ const trimmedName = name.trim()
123
+ const parsed = parsePluginName(trimmedName)
124
+
125
+ // If scoped, always use npm (local plugins are never scoped)
126
+ if (parsed.type === 'scoped') {
127
+ return {
128
+ source: 'npm',
129
+ resolvedName: trimmedName,
130
+ npmPackage: trimmedName,
131
+ }
132
+ }
133
+
134
+ // Check local workspace plugins directory
135
+ const localPath = path.join(workspaceRoot, 'plugins', trimmedName)
136
+ const localExists = await fs.pathExists(localPath)
137
+
138
+ if (localExists) {
139
+ // Verify it's a valid plugin (has plugin.json and dist/generator.js)
140
+ const hasPluginJson = await fs.pathExists(path.join(localPath, 'plugin.json'))
141
+ const hasGenerator = await fs.pathExists(path.join(localPath, 'dist/generator.js'))
142
+
143
+ if (hasPluginJson && hasGenerator) {
144
+ return {
145
+ source: 'local',
146
+ resolvedName: trimmedName,
147
+ localPath,
148
+ }
149
+ }
150
+ }
151
+
152
+ // Not found locally, resolve to npm package
153
+ const npmPackage = toNpmPackageName(trimmedName)
154
+
155
+ return {
156
+ source: 'npm',
157
+ resolvedName: trimmedName,
158
+ npmPackage,
159
+ }
160
+ }
@@ -0,0 +1,69 @@
1
+ import * as path from 'path'
2
+
3
+ import fs from 'fs-extra'
4
+
5
+ import { validatePluginName } from '@launch77/plugin-runtime'
6
+ import { processTemplate, getPluginTemplatePath } from '../../../infrastructure/template.js'
7
+ import { toPascalCase } from '../../../utils/string.js'
8
+
9
+ import type { Launch77Context } from '@launch77/plugin-runtime'
10
+
11
+ export interface CreatePluginRequest {
12
+ pluginName: string
13
+ description?: string
14
+ }
15
+
16
+ export interface CreatePluginResult {
17
+ pluginName: string
18
+ pluginPath: string
19
+ }
20
+
21
+ export class PluginCreateService {
22
+ /**
23
+ * Create a new plugin from template
24
+ */
25
+ async createPlugin(request: CreatePluginRequest, context: Launch77Context): Promise<CreatePluginResult> {
26
+ const { pluginName, description } = request
27
+
28
+ // 1. Validate plugin name
29
+ const nameValidation = validatePluginName(pluginName)
30
+ if (!nameValidation.isValid) {
31
+ throw new Error(nameValidation.error || 'Invalid plugin name')
32
+ }
33
+
34
+ // 2. Validate workspace context
35
+ if (!context.isValid) {
36
+ throw new Error('Must be run from within a Launch77 workspace')
37
+ }
38
+
39
+ // 3. Determine plugin path
40
+ const pluginPath = path.join(context.workspaceRoot, 'plugins', pluginName)
41
+
42
+ // 4. Check if plugin already exists
43
+ if (await fs.pathExists(pluginPath)) {
44
+ throw new Error(`Plugin '${pluginName}' already exists at ${pluginPath}`)
45
+ }
46
+
47
+ // 5. Get plugin template path
48
+ const templatePath = getPluginTemplatePath('plugin')
49
+
50
+ // 6. Check if template exists
51
+ if (!(await fs.pathExists(templatePath))) {
52
+ throw new Error(`Plugin template not found at ${templatePath}`)
53
+ }
54
+
55
+ // 7. Process template with context
56
+ await processTemplate(templatePath, pluginPath, {
57
+ appName: pluginName, // Required by TemplateContext but not used for plugin templates
58
+ pluginName,
59
+ pluginNamePascal: toPascalCase(pluginName),
60
+ workspaceName: context.workspaceName,
61
+ description: description || `Launch77 plugin for ${pluginName}`,
62
+ })
63
+
64
+ return {
65
+ pluginName,
66
+ pluginPath,
67
+ }
68
+ }
69
+ }
@@ -1,34 +1,90 @@
1
1
  import * as path from 'path'
2
2
 
3
+ import chalk from 'chalk'
3
4
  import { execa } from 'execa'
5
+ import { readPluginMetadata } from '@launch77/plugin-runtime'
4
6
 
5
- import { PluginInstallationError, PluginNotFoundError, InvalidPluginContextError, createInvalidContextError } from '../errors/plugin-errors.js'
6
- import { pluginExists, getPluginPath } from '../lib/plugin-registry.js'
7
+ import { PluginInstallationError, InvalidPluginContextError, createInvalidContextError, MissingPluginTargetsError, createInvalidTargetError, NpmInstallationError, PluginResolutionError } from '../errors/plugin-errors.js'
8
+ import { validatePluginInput, resolvePluginLocation } from '../lib/plugin-resolver.js'
7
9
 
8
- import type { Launch77Context } from '../../../utils/launch77-context.js'
10
+ import type { Launch77Context, Launch77LocationType } from '@launch77/plugin-runtime'
9
11
  import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js'
10
12
 
13
+ /**
14
+ * Map location type to target string
15
+ */
16
+ function locationTypeToTarget(locationType: Launch77LocationType): string | null {
17
+ switch (locationType) {
18
+ case 'workspace-app':
19
+ return 'app'
20
+ case 'workspace-library':
21
+ return 'library'
22
+ case 'workspace-plugin':
23
+ return 'plugin'
24
+ case 'workspace-app-template':
25
+ return 'app-template'
26
+ default:
27
+ return null
28
+ }
29
+ }
30
+
11
31
  export class PluginService {
12
32
  /**
13
- * Install a plugin to the current app
33
+ * Install a plugin to the current package
14
34
  */
15
- async installPlugin(request: InstallPluginRequest, context: Launch77Context): Promise<InstallPluginResult> {
35
+ async installPlugin(request: InstallPluginRequest, context: Launch77Context, logger: (message: string) => void = console.log): Promise<InstallPluginResult> {
16
36
  const { pluginName } = request
17
37
 
18
- // Validate context - must be in app directory
19
- if (context.locationType !== 'workspace-app') throw createInvalidContextError(context.locationType)
20
- if (!context.appName) throw new InvalidPluginContextError('Could not determine app name. This is a bug. Please report it.')
38
+ // Must be in a package directory (app, library, plugin, or app-template)
39
+ const currentTarget = locationTypeToTarget(context.locationType)
40
+ if (!currentTarget) throw createInvalidContextError(context.locationType)
41
+ if (!context.appName) throw new InvalidPluginContextError('Could not determine package name. This is a bug. Please report it.')
42
+
43
+ // Step 1: Validate plugin input
44
+ logger(chalk.blue(`\nšŸ” Resolving plugin "${pluginName}"...`))
45
+ logger(` ā”œā”€ Validating plugin name...`)
46
+
47
+ const validation = validatePluginInput(pluginName)
48
+ if (!validation.isValid) {
49
+ throw new PluginResolutionError(pluginName, validation.error || 'Invalid plugin name')
50
+ }
51
+ logger(` │ └─ ${chalk.green('āœ“')} Valid plugin name`)
52
+
53
+ // Step 2: Resolve plugin location
54
+ logger(` ā”œā”€ Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`)
55
+ const resolution = await resolvePluginLocation(pluginName, context.workspaceRoot)
21
56
 
22
- // Get app directory path
23
- const appPath = path.join(context.appsDir, context.appName)
57
+ let pluginPath: string
58
+
59
+ if (resolution.source === 'local') {
60
+ logger(` │ └─ ${chalk.green('āœ“')} Found local plugin`)
61
+ pluginPath = resolution.localPath!
62
+ } else {
63
+ logger(` │ └─ ${chalk.dim('Not found locally')}`)
64
+ logger(` ā”œā”€ Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`)
65
+
66
+ // Download npm plugin
67
+ pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, context.workspaceRoot, logger)
68
+ }
69
+
70
+ logger(` └─ ${chalk.green('āœ“')} Plugin resolved\n`)
71
+
72
+ // Step 3: Read plugin metadata and validate targets
73
+ const metadata = await readPluginMetadata(pluginPath)
74
+
75
+ if (!metadata.targets || metadata.targets.length === 0) {
76
+ throw new MissingPluginTargetsError(pluginName)
77
+ }
24
78
 
25
- // Check if plugin exists
26
- if (!(await pluginExists(pluginName))) throw new PluginNotFoundError(pluginName)
79
+ if (!metadata.targets.includes(currentTarget)) {
80
+ throw createInvalidTargetError(pluginName, currentTarget, metadata.targets)
81
+ }
27
82
 
28
- const pluginPath = getPluginPath(pluginName)
83
+ // Step 4: Get package directory path
84
+ const packagePath = this.getPackagePath(context)
29
85
 
30
- // Run generator
31
- await this.runGenerator(pluginPath, appPath, context)
86
+ // Step 5: Run generator
87
+ await this.runGenerator(pluginPath, packagePath, context)
32
88
 
33
89
  return {
34
90
  pluginName,
@@ -38,6 +94,43 @@ export class PluginService {
38
94
  }
39
95
  }
40
96
 
97
+ /**
98
+ * Download and install an npm plugin package
99
+ */
100
+ private async downloadNpmPlugin(npmPackage: string, workspaceRoot: string, logger: (message: string) => void): Promise<string> {
101
+ logger(` └─ Installing from npm: ${chalk.cyan(npmPackage)}...`)
102
+
103
+ try {
104
+ // Install the npm package to the workspace
105
+ await execa('npm', ['install', npmPackage, '--save-dev'], {
106
+ cwd: workspaceRoot,
107
+ stdio: 'pipe', // Capture output for clean logging
108
+ })
109
+
110
+ // Return path to installed plugin in node_modules
111
+ const pluginPath = path.join(workspaceRoot, 'node_modules', npmPackage)
112
+ return pluginPath
113
+ } catch (error) {
114
+ throw new NpmInstallationError(npmPackage, error instanceof Error ? error : undefined)
115
+ }
116
+ }
117
+
118
+ private getPackagePath(context: Launch77Context): string {
119
+ // Determine the base directory based on location type
120
+ switch (context.locationType) {
121
+ case 'workspace-app':
122
+ return path.join(context.appsDir, context.appName!)
123
+ case 'workspace-library':
124
+ return path.join(context.workspaceRoot, 'libraries', context.appName!)
125
+ case 'workspace-plugin':
126
+ return path.join(context.workspaceRoot, 'plugins', context.appName!)
127
+ case 'workspace-app-template':
128
+ return path.join(context.workspaceRoot, 'app-templates', context.appName!)
129
+ default:
130
+ throw new InvalidPluginContextError(`Cannot install plugin from ${context.locationType}`)
131
+ }
132
+ }
133
+
41
134
  private async runGenerator(pluginPath: string, appPath: string, context: Launch77Context): Promise<void> {
42
135
  try {
43
136
  const generatorPath = path.join(pluginPath, 'dist/generator.js')
@@ -0,0 +1,102 @@
1
+ import chalk from 'chalk'
2
+ import { Command } from 'commander'
3
+ import ora from 'ora'
4
+
5
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
6
+ import { GitService, GitHubService, GitHubCLINotInstalledError, GitHubNotAuthenticatedError, NotInWorkspaceError, GitHubNotConnectedError } from '../../git/index.js'
7
+ import { ReleaseService } from '../services/release-service.js'
8
+ import { ChangesetNotInitializedError } from '../errors/release-errors.js'
9
+
10
+ export function releaseInitCommand(): Command {
11
+ return new Command('release:init').description('Initialize complete release workflow setup').action(async () => {
12
+ console.log(chalk.blue('\nšŸš€ Initializing release workflow...\n'))
13
+
14
+ const cwd = process.cwd()
15
+ const gitService = new GitService()
16
+ const githubService = new GitHubService()
17
+ const releaseService = new ReleaseService(githubService)
18
+
19
+ try {
20
+ // 1. Verify we're in a workspace root
21
+ const context = await detectLaunch77Context(cwd)
22
+ if (!context.isValid || context.locationType !== 'workspace-root') {
23
+ throw new NotInWorkspaceError(cwd)
24
+ }
25
+
26
+ // 2. Verify prerequisites (GitHub CLI installed and authenticated)
27
+ const spinner = ora('Checking prerequisites...').start()
28
+ await githubService.verifyPrerequisites()
29
+ spinner.succeed('Prerequisites verified')
30
+
31
+ // 3. Ensure connected to GitHub
32
+ try {
33
+ await gitService.ensureConnectedToGitHub(context.workspaceRoot)
34
+ } catch (error) {
35
+ if (error instanceof GitHubNotConnectedError) {
36
+ console.error(chalk.red(`\nāŒ ${error.message}\n`))
37
+ console.log(chalk.gray(` Connect to GitHub first: ${chalk.cyan('launch77 git:connect')}\n`))
38
+ process.exit(1)
39
+ }
40
+ throw error
41
+ }
42
+
43
+ // 4. Get repository information
44
+ const repoSpinner = ora('Detecting repository...').start()
45
+ const { owner, repo } = await githubService.getCurrentRepository(context.workspaceRoot)
46
+ repoSpinner.succeed(`Repository: ${owner}/${repo}`)
47
+
48
+ // 5. Fix changeset config access setting
49
+ console.log(chalk.cyan('\nšŸ“ Configuring changesets...\n'))
50
+ try {
51
+ const changesetSpinner = ora('Checking changeset configuration...').start()
52
+ const wasChanged = await releaseService.fixChangesetAccess(context.workspaceRoot)
53
+
54
+ if (wasChanged) {
55
+ changesetSpinner.succeed('Updated .changeset/config.json access to "public"')
56
+ } else {
57
+ changesetSpinner.succeed('Changeset already configured correctly')
58
+ }
59
+ } catch (error) {
60
+ if (error instanceof ChangesetNotInitializedError) {
61
+ console.error(chalk.red(`\nāŒ ${error.message}\n`))
62
+ console.log(chalk.gray(' Changesets should be initialized during workspace creation.'))
63
+ console.log(chalk.gray(` Run manually: ${chalk.cyan('npx changeset init')}\n`))
64
+ process.exit(1)
65
+ }
66
+ throw error
67
+ }
68
+
69
+ // 6. Setup RELEASE_TOKEN
70
+ releaseService.explainReleaseToken()
71
+ const token = await releaseService.promptForReleaseToken()
72
+ await releaseService.setupReleaseToken(owner, repo, token)
73
+
74
+ // 7. Guide npm Trusted Publishing setup
75
+ releaseService.explainNpmTrustedPublishing()
76
+
77
+ // 8. Show success summary
78
+ releaseService.showSuccessSummary()
79
+ } catch (error) {
80
+ if (error instanceof GitHubCLINotInstalledError) {
81
+ console.error(chalk.red(`\nāŒ ${error.message}\n`))
82
+ console.log(chalk.gray(` Install with: ${chalk.cyan('brew install gh')}\n`))
83
+ console.log(chalk.gray(` Or visit: ${chalk.cyan('https://cli.github.com/')}\n`))
84
+ process.exit(1)
85
+ }
86
+
87
+ if (error instanceof GitHubNotAuthenticatedError) {
88
+ console.error(chalk.red(`\nāŒ ${error.message}\n`))
89
+ console.log(chalk.gray(` Authenticate with: ${chalk.cyan('gh auth login')}\n`))
90
+ process.exit(1)
91
+ }
92
+
93
+ if (error instanceof NotInWorkspaceError) {
94
+ console.error(chalk.red(`\nāŒ ${error.message}\n`))
95
+ console.log(chalk.gray(` This command must be run from a Launch77 workspace root directory\n`))
96
+ process.exit(1)
97
+ }
98
+
99
+ throw error
100
+ }
101
+ })
102
+ }
@@ -0,0 +1,13 @@
1
+ export class InvalidReleaseTokenError extends Error {
2
+ constructor(message: string) {
3
+ super(message)
4
+ this.name = 'InvalidReleaseTokenError'
5
+ }
6
+ }
7
+
8
+ export class ChangesetNotInitializedError extends Error {
9
+ constructor() {
10
+ super('Changesets are not initialized in this workspace')
11
+ this.name = 'ChangesetNotInitializedError'
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ // Services
2
+ export { ReleaseService } from './services/release-service.js'
3
+
4
+ // Errors
5
+ export { InvalidReleaseTokenError, ChangesetNotInitializedError } from './errors/release-errors.js'
6
+
7
+ // Commands
8
+ export { releaseInitCommand } from './commands/release-init.js'
@@ -0,0 +1,170 @@
1
+ import chalk from 'chalk'
2
+ import { password, confirm } from '@inquirer/prompts'
3
+ import ora from 'ora'
4
+ import fs from 'fs/promises'
5
+ import path from 'path'
6
+
7
+ import { GitHubService } from '../../git/index.js'
8
+ import { InvalidReleaseTokenError, ChangesetNotInitializedError } from '../errors/release-errors.js'
9
+
10
+ export class ReleaseService {
11
+ constructor(private githubService: GitHubService = new GitHubService()) {}
12
+
13
+ /**
14
+ * Validate GitHub Personal Access Token format
15
+ */
16
+ validateReleaseToken(token: string): void {
17
+ if (!token || token.trim().length === 0) {
18
+ throw new InvalidReleaseTokenError('Token cannot be empty')
19
+ }
20
+
21
+ // GitHub PATs start with specific prefixes
22
+ if (!token.startsWith('ghp_') && !token.startsWith('github_pat_')) {
23
+ throw new InvalidReleaseTokenError('Invalid token format. GitHub PATs should start with "ghp_" or "github_pat_"')
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Explain what RELEASE_TOKEN is and why it's needed
29
+ */
30
+ explainReleaseToken(): void {
31
+ console.log(chalk.cyan('\nšŸ“‹ About RELEASE_TOKEN:\n'))
32
+ console.log(chalk.white('The RELEASE_TOKEN is a GitHub Personal Access Token (PAT) that allows'))
33
+ console.log(chalk.white('the Changesets action to create Pull Requests for version updates.\n'))
34
+ console.log(chalk.white('Why is this needed?'))
35
+ console.log(chalk.gray(' • The default GITHUB_TOKEN has limited permissions'))
36
+ console.log(chalk.gray(' • Creating PRs that trigger CI requires a PAT'))
37
+ console.log(chalk.gray(' • This enables automated release workflows\n'))
38
+ console.log(chalk.white('Required permissions:'))
39
+ console.log(chalk.gray(' • Contents: Read and write'))
40
+ console.log(chalk.gray(' • Pull requests: Read and write\n'))
41
+
42
+ // Provide link to create PAT
43
+ const tokenUrl = 'https://github.com/settings/personal-access-tokens/new'
44
+ console.log(chalk.cyan('šŸ”— Create your token:'))
45
+ console.log(chalk.gray(` ${chalk.cyan(tokenUrl)}`))
46
+ console.log(chalk.gray(` Name: Launch77 Release Token`))
47
+ console.log(chalk.gray(` Permissions: Contents (Read and write), Pull requests (Read and write)\n`))
48
+ }
49
+
50
+ /**
51
+ * Prompt user for RELEASE_TOKEN
52
+ */
53
+ async promptForReleaseToken(): Promise<string> {
54
+ return password({
55
+ message: 'Paste your Personal Access Token (PAT):',
56
+ mask: '*',
57
+ validate: (value) => {
58
+ try {
59
+ this.validateReleaseToken(value)
60
+ return true
61
+ } catch (error) {
62
+ if (error instanceof InvalidReleaseTokenError) {
63
+ return error.message
64
+ }
65
+ return 'Invalid token'
66
+ }
67
+ },
68
+ })
69
+ }
70
+
71
+ /**
72
+ * Setup RELEASE_TOKEN secret in GitHub repository
73
+ */
74
+ async setupReleaseToken(owner: string, repo: string, token: string): Promise<void> {
75
+ this.validateReleaseToken(token)
76
+
77
+ // Confirm before setting
78
+ console.log(chalk.yellow('\n⚠ Note: This will overwrite any existing RELEASE_TOKEN secret\n'))
79
+ const shouldContinue = await confirm({
80
+ message: 'Continue and set RELEASE_TOKEN?',
81
+ default: true,
82
+ })
83
+
84
+ if (!shouldContinue) {
85
+ console.log(chalk.green('\nāœ… No changes made.\n'))
86
+ process.exit(0)
87
+ }
88
+
89
+ // Set the secret
90
+ const setSpinner = ora('Setting RELEASE_TOKEN secret...').start()
91
+ try {
92
+ await this.githubService.setRepositorySecret(owner, repo, 'RELEASE_TOKEN', token)
93
+ setSpinner.succeed('RELEASE_TOKEN configured successfully!')
94
+ } catch (error) {
95
+ setSpinner.fail('Failed to set RELEASE_TOKEN')
96
+ throw error
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Fix changeset config to set access to "public"
102
+ */
103
+ async fixChangesetAccess(workspaceRoot: string): Promise<boolean> {
104
+ const configPath = path.join(workspaceRoot, '.changeset', 'config.json')
105
+
106
+ try {
107
+ // Check if config exists
108
+ const configContent = await fs.readFile(configPath, 'utf-8')
109
+ const config = JSON.parse(configContent)
110
+
111
+ // Check if already public
112
+ if (config.access === 'public') {
113
+ return false // No change needed
114
+ }
115
+
116
+ // Update to public
117
+ config.access = 'public'
118
+
119
+ // Write back
120
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
121
+
122
+ return true // Changed
123
+ } catch (error) {
124
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
125
+ throw new ChangesetNotInitializedError()
126
+ }
127
+ throw error
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Explain npm Trusted Publishing (OIDC)
133
+ */
134
+ explainNpmTrustedPublishing(): void {
135
+ console.log(chalk.cyan('\nšŸ“¦ Setting up npm publishing with Trusted Publishers (OIDC):\n'))
136
+ console.log(chalk.white('Trusted Publishing uses OpenID Connect (OIDC) for secure, token-free publishing.'))
137
+ console.log(chalk.white('This is the recommended approach (no tokens to manage or expire).\n'))
138
+
139
+ console.log(chalk.white('Steps to configure:'))
140
+ console.log(chalk.gray(' 1. Visit: https://www.npmjs.com/settings/~/publishers'))
141
+ console.log(chalk.gray(' 2. Click "Add a trusted publisher"'))
142
+ console.log(chalk.gray(' 3. Select "GitHub Actions"'))
143
+ console.log(chalk.gray(' 4. Enter your repository information:'))
144
+ console.log(chalk.gray(' - Repository owner (your GitHub username or org)'))
145
+ console.log(chalk.gray(' - Repository name'))
146
+ console.log(chalk.gray(' - Workflow file: .github/workflows/ci.yml'))
147
+ console.log(chalk.gray(' 5. Save the configuration\n'))
148
+
149
+ console.log(chalk.white('Your GitHub workflow already has the required permission:'))
150
+ console.log(chalk.gray(' āœ“ id-token: write\n'))
151
+
152
+ console.log(chalk.cyan('šŸ“š Learn more:'))
153
+ console.log(chalk.gray(' https://docs.npmjs.com/trusted-publishers/\n'))
154
+ }
155
+
156
+ /**
157
+ * Show success summary for release setup
158
+ */
159
+ showSuccessSummary(): void {
160
+ console.log(chalk.green('\nāœ… Release automation is ready!\n'))
161
+ console.log(chalk.white('What happens now:'))
162
+ console.log(chalk.gray(' • When you push to main, CI runs as usual'))
163
+ console.log(chalk.gray(' • Changesets detects version changes'))
164
+ console.log(chalk.gray(' • A "Version Packages" PR is created automatically'))
165
+ console.log(chalk.gray(' • Merge the PR to publish your packages\n'))
166
+
167
+ console.log(chalk.cyan('šŸ“š Learn more:'))
168
+ console.log(chalk.gray(` ${chalk.cyan('https://github.com/changesets/changesets')}\n`))
169
+ }
170
+ }