@launch77/cli 1.4.3 → 1.5.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 (152) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cli.js +1 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/infrastructure/npm-package.d.ts +42 -0
  5. package/dist/infrastructure/npm-package.d.ts.map +1 -0
  6. package/dist/infrastructure/npm-package.js +46 -0
  7. package/dist/infrastructure/npm-package.js.map +1 -0
  8. package/dist/infrastructure/package-resolver.d.ts +117 -0
  9. package/dist/infrastructure/package-resolver.d.ts.map +1 -0
  10. package/dist/infrastructure/package-resolver.js +170 -0
  11. package/dist/infrastructure/package-resolver.js.map +1 -0
  12. package/dist/infrastructure/package-resolver.test.d.ts +2 -0
  13. package/dist/infrastructure/package-resolver.test.d.ts.map +1 -0
  14. package/dist/infrastructure/package-resolver.test.js +251 -0
  15. package/dist/infrastructure/package-resolver.test.js.map +1 -0
  16. package/dist/infrastructure/template.d.ts +0 -1
  17. package/dist/infrastructure/template.d.ts.map +1 -1
  18. package/dist/infrastructure/template.js.map +1 -1
  19. package/dist/modules/app/commands/create-app.d.ts.map +1 -1
  20. package/dist/modules/app/commands/create-app.js +14 -15
  21. package/dist/modules/app/commands/create-app.js.map +1 -1
  22. package/dist/modules/app/commands/delete-app.js +1 -1
  23. package/dist/modules/app/commands/delete-app.js.map +1 -1
  24. package/dist/modules/app/commands/validate-manifest.d.ts.map +1 -1
  25. package/dist/modules/app/commands/validate-manifest.js +0 -1
  26. package/dist/modules/app/commands/validate-manifest.js.map +1 -1
  27. package/dist/modules/app/index.d.ts +1 -2
  28. package/dist/modules/app/index.d.ts.map +1 -1
  29. package/dist/modules/app/index.js +0 -1
  30. package/dist/modules/app/index.js.map +1 -1
  31. package/dist/modules/app/lib/app-template-resolver.d.ts +14 -0
  32. package/dist/modules/app/lib/app-template-resolver.d.ts.map +1 -0
  33. package/dist/modules/app/lib/app-template-resolver.js +51 -0
  34. package/dist/modules/app/lib/app-template-resolver.js.map +1 -0
  35. package/dist/modules/app/lib/manifest-schema.d.ts +0 -7
  36. package/dist/modules/app/lib/manifest-schema.d.ts.map +1 -1
  37. package/dist/modules/app/lib/manifest-schema.js +0 -2
  38. package/dist/modules/app/lib/manifest-schema.js.map +1 -1
  39. package/dist/modules/app/services/app-svc.d.ts +2 -1
  40. package/dist/modules/app/services/app-svc.d.ts.map +1 -1
  41. package/dist/modules/app/services/app-svc.js +77 -12
  42. package/dist/modules/app/services/app-svc.js.map +1 -1
  43. package/dist/modules/app/services/manifest-svc.d.ts +2 -2
  44. package/dist/modules/app/services/manifest-svc.d.ts.map +1 -1
  45. package/dist/modules/app/services/manifest-svc.js +9 -50
  46. package/dist/modules/app/services/manifest-svc.js.map +1 -1
  47. package/dist/modules/app/types/app-types.d.ts +2 -5
  48. package/dist/modules/app/types/app-types.d.ts.map +1 -1
  49. package/dist/modules/app/types/app-types.js +1 -4
  50. package/dist/modules/app/types/app-types.js.map +1 -1
  51. package/dist/modules/catalog/commands/scan.js +3 -3
  52. package/dist/modules/catalog/commands/scan.js.map +1 -1
  53. package/dist/modules/catalog/services/catalog-svc.d.ts +2 -2
  54. package/dist/modules/catalog/services/catalog-svc.d.ts.map +1 -1
  55. package/dist/modules/catalog/services/catalog-svc.js +2 -2
  56. package/dist/modules/catalog/services/catalog-svc.js.map +1 -1
  57. package/dist/modules/deploy/commands/deploy-init-action.d.ts.map +1 -1
  58. package/dist/modules/deploy/commands/deploy-init-action.js +21 -20
  59. package/dist/modules/deploy/commands/deploy-init-action.js.map +1 -1
  60. package/dist/modules/deploy/commands/deploy-logs-action.d.ts.map +1 -1
  61. package/dist/modules/deploy/commands/deploy-logs-action.js +16 -13
  62. package/dist/modules/deploy/commands/deploy-logs-action.js.map +1 -1
  63. package/dist/modules/deploy/commands/deploy-status-action.d.ts.map +1 -1
  64. package/dist/modules/deploy/commands/deploy-status-action.js +16 -13
  65. package/dist/modules/deploy/commands/deploy-status-action.js.map +1 -1
  66. package/dist/modules/plugin/lib/plugin-resolver.d.ts +10 -72
  67. package/dist/modules/plugin/lib/plugin-resolver.d.ts.map +1 -1
  68. package/dist/modules/plugin/lib/plugin-resolver.js +23 -115
  69. package/dist/modules/plugin/lib/plugin-resolver.js.map +1 -1
  70. package/dist/modules/plugin/lib/plugin-resolver.test.js +45 -39
  71. package/dist/modules/plugin/lib/plugin-resolver.test.js.map +1 -1
  72. package/dist/modules/plugin/services/plugin-svc.d.ts +2 -0
  73. package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
  74. package/dist/modules/plugin/services/plugin-svc.js +24 -19
  75. package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
  76. package/dist/modules/plugin/services/plugin-svc.test.js +12 -6
  77. package/dist/modules/plugin/services/plugin-svc.test.js.map +1 -1
  78. package/dist/templates/plugin/package.json.hbs +1 -1
  79. package/dist/templates/plugin/plugin.json.hbs +1 -3
  80. package/dist/utils/validation.d.ts +3 -7
  81. package/dist/utils/validation.d.ts.map +1 -1
  82. package/dist/utils/validation.js +7 -31
  83. package/dist/utils/validation.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/cli.ts +1 -4
  86. package/src/infrastructure/npm-package.ts +73 -0
  87. package/src/infrastructure/package-resolver.test.ts +313 -0
  88. package/src/infrastructure/package-resolver.ts +223 -0
  89. package/src/infrastructure/template.ts +0 -1
  90. package/src/modules/app/commands/create-app.ts +14 -18
  91. package/src/modules/app/commands/delete-app.ts +1 -1
  92. package/src/modules/app/commands/validate-manifest.ts +0 -1
  93. package/src/modules/app/index.ts +1 -2
  94. package/src/modules/app/lib/app-template-resolver.ts +56 -0
  95. package/src/modules/app/lib/manifest-schema.ts +0 -5
  96. package/src/modules/app/services/app-svc.ts +89 -13
  97. package/src/modules/app/services/manifest-svc.ts +8 -57
  98. package/src/modules/app/types/app-types.ts +2 -9
  99. package/src/modules/catalog/commands/scan.ts +3 -3
  100. package/src/modules/catalog/services/catalog-svc.ts +4 -4
  101. package/src/modules/deploy/commands/deploy-init-action.ts +23 -20
  102. package/src/modules/deploy/commands/deploy-logs-action.ts +19 -13
  103. package/src/modules/deploy/commands/deploy-status-action.ts +19 -13
  104. package/src/modules/plugin/lib/plugin-resolver.test.ts +48 -39
  105. package/src/modules/plugin/lib/plugin-resolver.ts +22 -141
  106. package/src/modules/plugin/services/plugin-svc.test.ts +12 -6
  107. package/src/modules/plugin/services/plugin-svc.ts +27 -18
  108. package/src/utils/validation.ts +10 -38
  109. package/templates/plugin/package.json.hbs +1 -1
  110. package/templates/plugin/plugin.json.hbs +1 -3
  111. package/dist/modules/app/commands/generate-manifest.d.ts +0 -3
  112. package/dist/modules/app/commands/generate-manifest.d.ts.map +0 -1
  113. package/dist/modules/app/commands/generate-manifest.js +0 -62
  114. package/dist/modules/app/commands/generate-manifest.js.map +0 -1
  115. package/dist/modules/startup/commands/create-startup.d.ts +0 -3
  116. package/dist/modules/startup/commands/create-startup.d.ts.map +0 -1
  117. package/dist/modules/startup/commands/create-startup.js +0 -43
  118. package/dist/modules/startup/commands/create-startup.js.map +0 -1
  119. package/dist/modules/startup/errors/startup-errors.d.ts +0 -13
  120. package/dist/modules/startup/errors/startup-errors.d.ts.map +0 -1
  121. package/dist/modules/startup/errors/startup-errors.js +0 -25
  122. package/dist/modules/startup/errors/startup-errors.js.map +0 -1
  123. package/dist/modules/startup/index.d.ts +0 -5
  124. package/dist/modules/startup/index.d.ts.map +0 -1
  125. package/dist/modules/startup/index.js +0 -7
  126. package/dist/modules/startup/index.js.map +0 -1
  127. package/dist/modules/startup/services/startup-service.d.ts +0 -7
  128. package/dist/modules/startup/services/startup-service.d.ts.map +0 -1
  129. package/dist/modules/startup/services/startup-service.js +0 -43
  130. package/dist/modules/startup/services/startup-service.js.map +0 -1
  131. package/dist/modules/startup/types/startup-types.d.ts +0 -8
  132. package/dist/modules/startup/types/startup-types.d.ts.map +0 -1
  133. package/dist/modules/startup/types/startup-types.js +0 -2
  134. package/dist/modules/startup/types/startup-types.js.map +0 -1
  135. package/dist/modules/startup/utils/startup-validators.d.ts +0 -8
  136. package/dist/modules/startup/utils/startup-validators.d.ts.map +0 -1
  137. package/dist/modules/startup/utils/startup-validators.js +0 -17
  138. package/dist/modules/startup/utils/startup-validators.js.map +0 -1
  139. package/dist/templates/startup/apps/.gitkeep +0 -8
  140. package/dist/utils/monorepo.d.ts +0 -19
  141. package/dist/utils/monorepo.d.ts.map +0 -1
  142. package/dist/utils/monorepo.js +0 -100
  143. package/dist/utils/monorepo.js.map +0 -1
  144. package/src/modules/app/commands/generate-manifest.ts +0 -75
  145. package/src/modules/startup/commands/create-startup.ts +0 -53
  146. package/src/modules/startup/errors/startup-errors.ts +0 -23
  147. package/src/modules/startup/index.ts +0 -11
  148. package/src/modules/startup/services/startup-service.ts +0 -57
  149. package/src/modules/startup/types/startup-types.ts +0 -8
  150. package/src/modules/startup/utils/startup-validators.ts +0 -19
  151. package/src/utils/monorepo.ts +0 -137
  152. package/templates/startup/apps/.gitkeep +0 -8
@@ -1,99 +1,106 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import * as path from 'path'
3
1
  import * as os from 'os'
2
+ import * as path from 'path'
3
+
4
4
  import fs from 'fs-extra'
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
5
6
 
6
- import { validatePluginInput, toNpmPackageName, resolvePluginLocation } from './plugin-resolver.js'
7
+ import { PluginResolver } from './plugin-resolver.js'
7
8
 
8
9
  describe('Plugin Resolver', () => {
9
- describe('validatePluginInput', () => {
10
+ let resolver: PluginResolver
11
+
12
+ beforeEach(() => {
13
+ resolver = new PluginResolver()
14
+ })
15
+
16
+ describe('validateInput', () => {
10
17
  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 })
18
+ expect(resolver.validateInput('release')).toEqual({ isValid: true })
19
+ expect(resolver.validateInput('my-plugin')).toEqual({ isValid: true })
20
+ expect(resolver.validateInput('analytics-v2')).toEqual({ isValid: true })
14
21
  })
15
22
 
16
23
  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 })
24
+ expect(resolver.validateInput('@ibm/plugin-name')).toEqual({ isValid: true })
25
+ expect(resolver.validateInput('@launch77-shared/plugin-release')).toEqual({ isValid: true })
26
+ expect(resolver.validateInput('@org/analytics')).toEqual({ isValid: true })
20
27
  })
21
28
 
22
29
  it('should reject empty names', () => {
23
- const result = validatePluginInput('')
30
+ const result = resolver.validateInput('')
24
31
  expect(result.isValid).toBe(false)
25
32
  expect(result.error).toBeDefined()
26
33
  })
27
34
 
28
35
  it('should reject whitespace-only names', () => {
29
- const result = validatePluginInput(' ')
36
+ const result = resolver.validateInput(' ')
30
37
  expect(result.isValid).toBe(false)
31
38
  expect(result.error).toBeDefined()
32
39
  })
33
40
 
34
41
  it('should reject invalid scoped packages', () => {
35
- const result1 = validatePluginInput('@invalid')
42
+ const result1 = resolver.validateInput('@invalid')
36
43
  expect(result1.isValid).toBe(false)
37
44
  expect(result1.error).toBeDefined()
38
45
 
39
- const result2 = validatePluginInput('@/package')
46
+ const result2 = resolver.validateInput('@/package')
40
47
  expect(result2.isValid).toBe(false)
41
48
  expect(result2.error).toBeDefined()
42
49
 
43
- const result3 = validatePluginInput('@org/')
50
+ const result3 = resolver.validateInput('@org/')
44
51
  expect(result3.isValid).toBe(false)
45
52
  expect(result3.error).toBeDefined()
46
53
  })
47
54
 
48
55
  it('should reject names with uppercase letters', () => {
49
- const result = validatePluginInput('MyPlugin')
56
+ const result = resolver.validateInput('MyPlugin')
50
57
  expect(result.isValid).toBe(false)
51
58
  expect(result.error).toContain('lowercase')
52
59
  })
53
60
 
54
61
  it('should reject names starting with numbers', () => {
55
- const result = validatePluginInput('123plugin')
62
+ const result = resolver.validateInput('123plugin')
56
63
  expect(result.isValid).toBe(false)
57
64
  expect(result.error).toBeDefined()
58
65
  })
59
66
 
60
67
  it('should reject names with special characters', () => {
61
- const result1 = validatePluginInput('plugin_name')
68
+ const result1 = resolver.validateInput('plugin_name')
62
69
  expect(result1.isValid).toBe(false)
63
70
 
64
- const result2 = validatePluginInput('plugin.name')
71
+ const result2 = resolver.validateInput('plugin.name')
65
72
  expect(result2.isValid).toBe(false)
66
73
 
67
- const result3 = validatePluginInput('plugin name')
74
+ const result3 = resolver.validateInput('plugin name')
68
75
  expect(result3.isValid).toBe(false)
69
76
  })
70
77
 
71
78
  it('should trim whitespace before validation', () => {
72
- expect(validatePluginInput(' release ')).toEqual({ isValid: true })
73
- expect(validatePluginInput(' @ibm/analytics ')).toEqual({ isValid: true })
79
+ expect(resolver.validateInput(' release ')).toEqual({ isValid: true })
80
+ expect(resolver.validateInput(' @ibm/analytics ')).toEqual({ isValid: true })
74
81
  })
75
82
  })
76
83
 
77
84
  describe('toNpmPackageName', () => {
78
85
  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')
86
+ expect(resolver.toNpmPackageName('release')).toBe('@launch77-shared/plugin-release')
87
+ expect(resolver.toNpmPackageName('my-plugin')).toBe('@launch77-shared/plugin-my-plugin')
88
+ expect(resolver.toNpmPackageName('analytics-v2')).toBe('@launch77-shared/plugin-analytics-v2')
82
89
  })
83
90
 
84
91
  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')
92
+ expect(resolver.toNpmPackageName('@ibm/analytics')).toBe('@ibm/analytics')
93
+ expect(resolver.toNpmPackageName('@launch77-shared/plugin-release')).toBe('@launch77-shared/plugin-release')
94
+ expect(resolver.toNpmPackageName('@org/package')).toBe('@org/package')
88
95
  })
89
96
 
90
97
  it('should trim whitespace', () => {
91
- expect(toNpmPackageName(' release ')).toBe('@launch77-shared/plugin-release')
92
- expect(toNpmPackageName(' @ibm/analytics ')).toBe('@ibm/analytics')
98
+ expect(resolver.toNpmPackageName(' release ')).toBe('@launch77-shared/plugin-release')
99
+ expect(resolver.toNpmPackageName(' @ibm/analytics ')).toBe('@ibm/analytics')
93
100
  })
94
101
  })
95
102
 
96
- describe('resolvePluginLocation', () => {
103
+ describe('resolveLocation', () => {
97
104
  let tempDir: string
98
105
 
99
106
  beforeEach(async () => {
@@ -113,19 +120,21 @@ describe('Plugin Resolver', () => {
113
120
  await fs.ensureDir(pluginPath)
114
121
  await fs.ensureDir(path.join(pluginPath, 'dist'))
115
122
  await fs.writeFile(path.join(pluginPath, 'plugin.json'), JSON.stringify({ name: 'my-plugin', version: '1.0.0' }))
123
+ await fs.writeFile(path.join(pluginPath, 'package.json'), JSON.stringify({ name: 'my-plugin', version: '0.0.1' }))
116
124
  await fs.writeFile(path.join(pluginPath, 'dist/generator.js'), 'console.log("test")')
117
125
 
118
- const result = await resolvePluginLocation('my-plugin', tempDir)
126
+ const result = await resolver.resolveLocation('my-plugin', tempDir)
119
127
 
120
128
  expect(result).toEqual({
121
129
  source: 'local',
122
130
  resolvedName: 'my-plugin',
123
131
  localPath: pluginPath,
132
+ version: '0.0.1',
124
133
  })
125
134
  })
126
135
 
127
136
  it('should resolve to npm if local plugin does not exist', async () => {
128
- const result = await resolvePluginLocation('release', tempDir)
137
+ const result = await resolver.resolveLocation('release', tempDir)
129
138
 
130
139
  expect(result).toEqual({
131
140
  source: 'npm',
@@ -141,7 +150,7 @@ describe('Plugin Resolver', () => {
141
150
  await fs.ensureDir(path.join(pluginPath, 'dist'))
142
151
  await fs.writeFile(path.join(pluginPath, 'dist/generator.js'), 'console.log("test")')
143
152
 
144
- const result = await resolvePluginLocation('incomplete', tempDir)
153
+ const result = await resolver.resolveLocation('incomplete', tempDir)
145
154
 
146
155
  expect(result).toEqual({
147
156
  source: 'npm',
@@ -156,7 +165,7 @@ describe('Plugin Resolver', () => {
156
165
  await fs.ensureDir(pluginPath)
157
166
  await fs.writeFile(path.join(pluginPath, 'plugin.json'), JSON.stringify({ name: 'incomplete', version: '1.0.0' }))
158
167
 
159
- const result = await resolvePluginLocation('incomplete', tempDir)
168
+ const result = await resolver.resolveLocation('incomplete', tempDir)
160
169
 
161
170
  expect(result).toEqual({
162
171
  source: 'npm',
@@ -170,7 +179,7 @@ describe('Plugin Resolver', () => {
170
179
  const pluginPath = path.join(tempDir, 'plugins', '@ibm')
171
180
  await fs.ensureDir(pluginPath)
172
181
 
173
- const result = await resolvePluginLocation('@ibm/analytics', tempDir)
182
+ const result = await resolver.resolveLocation('@ibm/analytics', tempDir)
174
183
 
175
184
  expect(result).toEqual({
176
185
  source: 'npm',
@@ -180,7 +189,7 @@ describe('Plugin Resolver', () => {
180
189
  })
181
190
 
182
191
  it('should resolve scoped @launch77-shared packages to npm', async () => {
183
- const result = await resolvePluginLocation('@launch77-shared/plugin-release', tempDir)
192
+ const result = await resolver.resolveLocation('@launch77-shared/plugin-release', tempDir)
184
193
 
185
194
  expect(result).toEqual({
186
195
  source: 'npm',
@@ -193,7 +202,7 @@ describe('Plugin Resolver', () => {
193
202
  // Remove plugins directory
194
203
  await fs.remove(path.join(tempDir, 'plugins'))
195
204
 
196
- const result = await resolvePluginLocation('release', tempDir)
205
+ const result = await resolver.resolveLocation('release', tempDir)
197
206
 
198
207
  expect(result).toEqual({
199
208
  source: 'npm',
@@ -203,7 +212,7 @@ describe('Plugin Resolver', () => {
203
212
  })
204
213
 
205
214
  it('should trim whitespace from plugin names', async () => {
206
- const result = await resolvePluginLocation(' release ', tempDir)
215
+ const result = await resolver.resolveLocation(' release ', tempDir)
207
216
 
208
217
  expect(result).toEqual({
209
218
  source: 'npm',
@@ -1,160 +1,41 @@
1
1
  import * as path from 'path'
2
2
 
3
3
  import fs from 'fs-extra'
4
- import { parsePluginName, isValidNpmPackageName } from '@launch77/plugin-runtime'
5
4
 
6
- import type { ValidationResult } from '@launch77/plugin-runtime'
5
+ import { PackageResolver } from '../../../infrastructure/package-resolver.js'
7
6
 
8
7
  /**
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
8
+ * Plugin resolver implementation
77
9
  *
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'
10
+ * Resolves plugins from:
11
+ * - Local workspace plugins/ directory
12
+ * - npm packages with @launch77-shared/plugin- prefix
84
13
  */
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
14
+ export class PluginResolver extends PackageResolver {
15
+ protected getFolderName(): string {
16
+ return 'plugins'
91
17
  }
92
18
 
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
- }
19
+ protected getPackagePrefix(): string {
20
+ return '@launch77-shared/plugin-'
132
21
  }
133
22
 
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)
23
+ protected async verify(localPath: string): Promise<boolean> {
24
+ // Verify it's a valid plugin (has plugin.json, dist/generator.js, and package.json with version)
140
25
  const hasPluginJson = await fs.pathExists(path.join(localPath, 'plugin.json'))
141
26
  const hasGenerator = await fs.pathExists(path.join(localPath, 'dist/generator.js'))
27
+ const hasPackageJson = await fs.pathExists(path.join(localPath, 'package.json'))
142
28
 
143
- if (hasPluginJson && hasGenerator) {
144
- return {
145
- source: 'local',
146
- resolvedName: trimmedName,
147
- localPath,
148
- }
29
+ if (!hasPluginJson || !hasGenerator || !hasPackageJson) {
30
+ return false
149
31
  }
150
- }
151
32
 
152
- // Not found locally, resolve to npm package
153
- const npmPackage = toNpmPackageName(trimmedName)
154
-
155
- return {
156
- source: 'npm',
157
- resolvedName: trimmedName,
158
- npmPackage,
33
+ // Verify package.json has a version field
34
+ try {
35
+ const packageJson = await fs.readJson(path.join(localPath, 'package.json'))
36
+ return !!packageJson.version
37
+ } catch {
38
+ return false
39
+ }
159
40
  }
160
41
  }
@@ -159,9 +159,9 @@ describe('PluginService', () => {
159
159
 
160
160
  const result = await (service as any).validatePluginTargets(pluginDir, 'test-plugin', 'app')
161
161
  expect(result).toEqual({
162
- name: 'test-plugin',
163
- version: '1.0.0',
164
162
  targets: ['app', 'library'],
163
+ pluginDependencies: undefined,
164
+ libraryDependencies: undefined,
165
165
  })
166
166
  })
167
167
 
@@ -175,8 +175,11 @@ describe('PluginService', () => {
175
175
  })
176
176
 
177
177
  const result = await (service as any).validatePluginTargets(pluginDir, 'multi-target-plugin', 'library')
178
- expect(result.targets).toContain('library')
179
- expect(result.targets).toHaveLength(4)
178
+ expect(result).toEqual({
179
+ targets: ['app', 'library', 'plugin', 'app-template'],
180
+ pluginDependencies: undefined,
181
+ libraryDependencies: undefined,
182
+ })
180
183
  })
181
184
 
182
185
  test('should handle plugin.json with optional fields (pluginDependencies, libraryDependencies)', async () => {
@@ -191,8 +194,11 @@ describe('PluginService', () => {
191
194
  })
192
195
 
193
196
  const result = await (service as any).validatePluginTargets(pluginDir, 'full-plugin', 'app')
194
- expect(result.pluginDependencies).toEqual({ 'other-plugin': '^1.0.0' })
195
- expect(result.libraryDependencies).toEqual({ react: '^18.0.0' })
197
+ expect(result).toEqual({
198
+ targets: ['app'],
199
+ pluginDependencies: { 'other-plugin': '^1.0.0' },
200
+ libraryDependencies: { react: '^18.0.0' },
201
+ })
196
202
  })
197
203
 
198
204
  // Error scenarios - test exact error messages
@@ -5,8 +5,9 @@ import chalk from 'chalk'
5
5
  import { execa } from 'execa'
6
6
  import { readPluginMetadata } from '@launch77/plugin-runtime'
7
7
 
8
+ import { downloadNpmPackage } from '../../../infrastructure/npm-package.js'
8
9
  import { PluginInstallationError, InvalidPluginContextError, createInvalidContextError, MissingPluginTargetsError, createInvalidTargetError, NpmInstallationError, PluginResolutionError } from '../errors/plugin-errors.js'
9
- import { validatePluginInput, resolvePluginLocation } from '../lib/plugin-resolver.js'
10
+ import { PluginResolver } from '../lib/plugin-resolver.js'
10
11
 
11
12
  import type { Launch77Context, Launch77LocationType, Launch77PackageManifest, InstalledPluginMetadata, PluginMetadata } from '@launch77/plugin-runtime'
12
13
  import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js'
@@ -30,6 +31,12 @@ function locationTypeToTarget(locationType: Launch77LocationType): string | null
30
31
  }
31
32
 
32
33
  export class PluginService {
34
+ private pluginResolver: PluginResolver
35
+
36
+ constructor() {
37
+ this.pluginResolver = new PluginResolver()
38
+ }
39
+
33
40
  /**
34
41
  * Validate that we're in a valid package directory and return the target type
35
42
  */
@@ -43,29 +50,37 @@ export class PluginService {
43
50
  /**
44
51
  * Validate plugin name, resolve its location, and download if needed
45
52
  */
46
- private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string }> {
53
+ private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string; version: string }> {
47
54
  logger(chalk.blue(`\n🔍 Resolving plugin "${pluginName}"...`))
48
55
  logger(` ├─ Validating plugin name...`)
49
56
 
50
- const validation = validatePluginInput(pluginName)
57
+ const validation = this.pluginResolver.validateInput(pluginName)
51
58
  if (!validation.isValid) {
52
59
  throw new PluginResolutionError(pluginName, validation.error || 'Invalid plugin name')
53
60
  }
54
61
  logger(` │ └─ ${chalk.green('✓')} Valid plugin name`)
55
62
 
56
63
  logger(` ├─ Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`)
57
- const resolution = await resolvePluginLocation(pluginName, workspaceRoot)
64
+ const resolution = await this.pluginResolver.resolveLocation(pluginName, workspaceRoot)
58
65
 
59
66
  let pluginPath: string
67
+ let version: string
60
68
 
61
69
  if (resolution.source === 'local') {
62
70
  logger(` │ └─ ${chalk.green('✓')} Found local plugin`)
63
71
  pluginPath = resolution.localPath!
72
+ version = resolution.version! // Local plugins always have version after verification
64
73
  } else {
65
74
  logger(` │ └─ ${chalk.dim('Not found locally')}`)
66
75
  logger(` ├─ Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`)
67
76
 
68
77
  pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, workspaceRoot, logger)
78
+
79
+ // Read version from downloaded package
80
+ const packageJsonPath = path.join(pluginPath, 'package.json')
81
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
82
+ const packageJson = JSON.parse(packageJsonContent)
83
+ version = packageJson.version
69
84
  }
70
85
 
71
86
  logger(` └─ ${chalk.green('✓')} Plugin resolved\n`)
@@ -74,6 +89,7 @@ export class PluginService {
74
89
  pluginPath,
75
90
  source: resolution.source,
76
91
  npmPackage: resolution.npmPackage,
92
+ version,
77
93
  }
78
94
  }
79
95
 
@@ -123,7 +139,7 @@ export class PluginService {
123
139
  const { pluginName } = request
124
140
 
125
141
  const currentTarget = this.validateContext(context)
126
- const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
142
+ const { pluginPath, source, npmPackage, version } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
127
143
  const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget)
128
144
 
129
145
  const packagePath = this.getPackagePath(context)
@@ -136,7 +152,7 @@ export class PluginService {
136
152
  await this.writePluginManifest(packagePath, {
137
153
  pluginName,
138
154
  packageName,
139
- version: metadata.version,
155
+ version,
140
156
  source,
141
157
  })
142
158
 
@@ -154,19 +170,12 @@ export class PluginService {
154
170
  private async downloadNpmPlugin(npmPackage: string, workspaceRoot: string, logger: (message: string) => void): Promise<string> {
155
171
  logger(` └─ Installing from npm: ${chalk.cyan(npmPackage)}...`)
156
172
 
157
- try {
158
- // Install the npm package to the workspace
159
- await execa('npm', ['install', npmPackage, '--save-dev'], {
160
- cwd: workspaceRoot,
161
- stdio: 'pipe', // Capture output for clean logging
162
- })
173
+ const result = await downloadNpmPackage({
174
+ packageName: npmPackage,
175
+ workspaceRoot,
176
+ })
163
177
 
164
- // Return path to installed plugin in node_modules
165
- const pluginPath = path.join(workspaceRoot, 'node_modules', npmPackage)
166
- return pluginPath
167
- } catch (error) {
168
- throw new NpmInstallationError(npmPackage, error instanceof Error ? error : undefined)
169
- }
178
+ return result.packagePath
170
179
  }
171
180
 
172
181
  private getPackagePath(context: Launch77Context): string {
@@ -2,7 +2,7 @@ import * as path from 'path'
2
2
 
3
3
  import chalk from 'chalk'
4
4
 
5
- import type { MonorepoContext } from './monorepo.js'
5
+ import type { Launch77Context } from '@launch77/plugin-runtime'
6
6
 
7
7
  /**
8
8
  * Validation result with helpful error messages
@@ -12,50 +12,22 @@ export interface ValidationResult {
12
12
  errorMessage?: string
13
13
  }
14
14
 
15
- /**
16
- * Validate that we're in a startup context (for create-app)
17
- */
18
- export function validateStartupContext(context: MonorepoContext): ValidationResult {
19
- if (!context.isValid) {
20
- return {
21
- valid: false,
22
- errorMessage: 'Must be run from within a Launch77 monorepo (could not find .launch-monorepo-root.md)',
23
- }
24
- }
25
-
26
- if (context.location !== 'startup-root' && context.location !== 'startup-app') {
27
- return {
28
- valid: false,
29
- errorMessage: `Command must be run from a startup directory.\n\n` + `${chalk.gray('Current location:')} ${context.location}\n` + `${chalk.gray('Expected:')} Within startups/<startup-name>/\n\n` + `${chalk.yellow('Navigate to a startup first:')}\n` + ` cd startups/<startup-name>/\n\n` + `${chalk.yellow('Or create a startup:')}\n` + ` launch77 create-startup <startup-name>`,
30
- }
31
- }
32
-
33
- if (!context.appsDir) {
34
- return {
35
- valid: false,
36
- errorMessage: `Could not determine apps directory for location: ${context.location}\n` + `This is a bug. Please report it.`,
37
- }
38
- }
39
-
40
- return { valid: true }
41
- }
42
-
43
15
  /**
44
16
  * Validate that we're inside an app directory (for plugin:install)
45
17
  */
46
- export function validateAppContext(context: MonorepoContext): ValidationResult {
18
+ export function validateAppContext(context: Launch77Context): ValidationResult {
47
19
  if (!context.isValid) {
48
20
  return {
49
21
  valid: false,
50
- errorMessage: 'Must be run from within a Launch77 monorepo (could not find .launch-monorepo-root.md)',
22
+ errorMessage: 'Must be run from within a Launch77 workspace (could not find .launch77/workspace.json)',
51
23
  }
52
24
  }
53
25
 
54
- // Must be inside an app, not just a startup
55
- if (context.location !== 'startup-app') {
26
+ // Must be inside an app
27
+ if (context.locationType !== 'workspace-app') {
56
28
  return {
57
29
  valid: false,
58
- errorMessage: `plugin:install must be run from within an app directory.\n\n` + `${chalk.gray('Current location:')} ${context.location}\n` + `${chalk.gray('Expected:')} startups/<startup-name>/apps/<app-name>/\n\n` + `${chalk.yellow('Navigate to an app directory:')}\n` + ` cd startups/<startup-name>/apps/<app-name>/\n\n` + `${chalk.yellow('Or create an app first:')}\n` + ` cd startups/<startup-name>/\n` + ` launch77 app:create api <app-name>`,
30
+ errorMessage: `plugin:install must be run from within an app directory.\n\n` + `${chalk.gray('Current location:')} ${context.locationType}\n` + `${chalk.gray('Expected:')} apps/<app-name>/\n\n` + `${chalk.yellow('Navigate to an app directory:')}\n` + ` cd apps/<app-name>/\n\n` + `${chalk.yellow('Or create an app first:')}\n` + ` launch77 app:create <template> <app-name>`,
59
31
  }
60
32
  }
61
33
 
@@ -73,16 +45,16 @@ export function validateAppContext(context: MonorepoContext): ValidationResult {
73
45
  * Get the app directory path
74
46
  * Returns null if not in an app context
75
47
  */
76
- export function getAppDirectory(context: MonorepoContext): string | null {
77
- if (context.location !== 'startup-app') {
48
+ export function getAppDirectory(context: Launch77Context): string | null {
49
+ if (context.locationType !== 'workspace-app') {
78
50
  return null
79
51
  }
80
52
 
81
- if (!context.monorepoRoot || !context.startupName || !context.appName) {
53
+ if (!context.workspaceRoot || !context.appName) {
82
54
  return null
83
55
  }
84
56
 
85
- return path.join(context.monorepoRoot, 'startups', context.startupName, 'apps', context.appName)
57
+ return path.join(context.workspaceRoot, 'apps', context.appName)
86
58
  }
87
59
 
88
60
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@{{workspaceName}}/plugin-{{pluginName}}",
3
- "version": "1.0.0",
3
+ "version": "0.0.1",
4
4
  "description": "{{description}}",
5
5
  "license": "UNLICENSED",
6
6
  "private": true,
@@ -1,7 +1,5 @@
1
1
  {
2
- "name": "{{pluginName}}",
3
- "version": "1.0.0",
4
- "description": "{{description}}",
2
+ "targets": ["app", "library"],
5
3
  "pluginDependencies": {},
6
4
  "libraryDependencies": {}
7
5
  }
@@ -1,3 +0,0 @@
1
- import { Command } from 'commander';
2
- export declare function generateManifestCommand(): Command;
3
- //# sourceMappingURL=generate-manifest.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-manifest.d.ts","sourceRoot":"","sources":["../../../../src/modules/app/commands/generate-manifest.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAQnC,wBAAgB,uBAAuB,IAAI,OAAO,CA8DjD"}