@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
@@ -0,0 +1,313 @@
1
+ import * as os from 'os'
2
+ import * as path from 'path'
3
+
4
+ import fs from 'fs-extra'
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
6
+
7
+ import { PackageResolver } from './package-resolver.js'
8
+
9
+ /**
10
+ * Test implementation of PackageResolver for unit testing
11
+ */
12
+ class TestPackageResolver extends PackageResolver {
13
+ protected getFolderName(): string {
14
+ return 'test-packages'
15
+ }
16
+
17
+ protected getPackagePrefix(): string {
18
+ return '@test-org/test-'
19
+ }
20
+
21
+ protected async verify(localPath: string): Promise<boolean> {
22
+ // Verify by checking for package.json
23
+ const packageJsonPath = path.join(localPath, 'package.json')
24
+ return await fs.pathExists(packageJsonPath)
25
+ }
26
+ }
27
+
28
+ describe('PackageResolver', () => {
29
+ let resolver: TestPackageResolver
30
+ let tempDir: string
31
+
32
+ beforeEach(async () => {
33
+ resolver = new TestPackageResolver()
34
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'package-resolver-test-'))
35
+ })
36
+
37
+ afterEach(async () => {
38
+ await fs.remove(tempDir)
39
+ })
40
+
41
+ describe('validateInput', () => {
42
+ describe('valid inputs', () => {
43
+ it('should accept valid unscoped package names', () => {
44
+ const result = resolver.validateInput('release')
45
+ expect(result.isValid).toBe(true)
46
+ expect(result.error).toBeUndefined()
47
+ })
48
+
49
+ it('should accept unscoped names with hyphens', () => {
50
+ const result = resolver.validateInput('my-package')
51
+ expect(result.isValid).toBe(true)
52
+ })
53
+
54
+ it('should accept unscoped names with numbers', () => {
55
+ const result = resolver.validateInput('package-123')
56
+ expect(result.isValid).toBe(true)
57
+ })
58
+
59
+ it('should accept valid scoped packages', () => {
60
+ const result = resolver.validateInput('@ibm/analytics')
61
+ expect(result.isValid).toBe(true)
62
+ expect(result.error).toBeUndefined()
63
+ })
64
+
65
+ it('should accept scoped packages with hyphens', () => {
66
+ const result = resolver.validateInput('@launch77-shared/plugin-release')
67
+ expect(result.isValid).toBe(true)
68
+ })
69
+
70
+ it('should trim whitespace from valid names', () => {
71
+ const result = resolver.validateInput(' release ')
72
+ expect(result.isValid).toBe(true)
73
+ })
74
+
75
+ it('should trim whitespace from scoped names', () => {
76
+ const result = resolver.validateInput(' @ibm/analytics ')
77
+ expect(result.isValid).toBe(true)
78
+ })
79
+ })
80
+
81
+ describe('invalid inputs', () => {
82
+ it('should reject empty string', () => {
83
+ const result = resolver.validateInput('')
84
+ expect(result.isValid).toBe(false)
85
+ expect(result.error).toBe('Package name cannot be empty')
86
+ })
87
+
88
+ it('should reject whitespace-only string', () => {
89
+ const result = resolver.validateInput(' ')
90
+ expect(result.isValid).toBe(false)
91
+ expect(result.error).toBe('Package name cannot be empty')
92
+ })
93
+
94
+ it('should reject invalid scoped package without name', () => {
95
+ const result = resolver.validateInput('@invalid')
96
+ expect(result.isValid).toBe(false)
97
+ expect(result.error).toContain('Scoped package must be in format @org/package')
98
+ })
99
+
100
+ it('should reject invalid scoped package without scope', () => {
101
+ const result = resolver.validateInput('@/package')
102
+ expect(result.isValid).toBe(false)
103
+ expect(result.error).toBeDefined()
104
+ })
105
+
106
+ it('should reject invalid scoped package with missing name after slash', () => {
107
+ const result = resolver.validateInput('@org/')
108
+ expect(result.isValid).toBe(false)
109
+ expect(result.error).toContain('Scoped package must be in format @org/package')
110
+ })
111
+
112
+ it('should reject names with uppercase letters', () => {
113
+ const result = resolver.validateInput('MyPackage')
114
+ expect(result.isValid).toBe(false)
115
+ expect(result.error).toBeDefined()
116
+ })
117
+
118
+ it('should reject names starting with numbers', () => {
119
+ const result = resolver.validateInput('123package')
120
+ expect(result.isValid).toBe(false)
121
+ expect(result.error).toBeDefined()
122
+ })
123
+
124
+ it('should reject names with underscores', () => {
125
+ const result = resolver.validateInput('my_package')
126
+ expect(result.isValid).toBe(false)
127
+ expect(result.error).toBeDefined()
128
+ })
129
+
130
+ it('should reject names with spaces', () => {
131
+ const result = resolver.validateInput('my package')
132
+ expect(result.isValid).toBe(false)
133
+ expect(result.error).toBeDefined()
134
+ })
135
+ })
136
+ })
137
+
138
+ describe('toNpmPackageName', () => {
139
+ describe('unscoped names', () => {
140
+ it('should prefix unscoped package names', () => {
141
+ const result = resolver.toNpmPackageName('release')
142
+ expect(result).toBe('@test-org/test-release')
143
+ })
144
+
145
+ it('should prefix unscoped names with hyphens', () => {
146
+ const result = resolver.toNpmPackageName('my-package')
147
+ expect(result).toBe('@test-org/test-my-package')
148
+ })
149
+
150
+ it('should trim whitespace from unscoped names', () => {
151
+ const result = resolver.toNpmPackageName(' release ')
152
+ expect(result).toBe('@test-org/test-release')
153
+ })
154
+ })
155
+
156
+ describe('scoped names', () => {
157
+ it('should return scoped packages as-is', () => {
158
+ const result = resolver.toNpmPackageName('@ibm/analytics')
159
+ expect(result).toBe('@ibm/analytics')
160
+ })
161
+
162
+ it('should return scoped packages with hyphens as-is', () => {
163
+ const result = resolver.toNpmPackageName('@launch77-shared/plugin-release')
164
+ expect(result).toBe('@launch77-shared/plugin-release')
165
+ })
166
+
167
+ it('should trim whitespace from scoped names', () => {
168
+ const result = resolver.toNpmPackageName(' @ibm/analytics ')
169
+ expect(result).toBe('@ibm/analytics')
170
+ })
171
+ })
172
+ })
173
+
174
+ describe('resolveLocation', () => {
175
+ describe('local resolution (unscoped names)', () => {
176
+ it('should resolve to local when valid package exists', async () => {
177
+ const packageName = 'my-local-package'
178
+ const localPath = path.join(tempDir, 'test-packages', packageName)
179
+ await fs.ensureDir(localPath)
180
+ await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName, version: '0.0.1' })
181
+
182
+ const result = await resolver.resolveLocation(packageName, tempDir)
183
+
184
+ expect(result.source).toBe('local')
185
+ expect(result.resolvedName).toBe(packageName)
186
+ expect(result.localPath).toBe(localPath)
187
+ expect(result.npmPackage).toBeUndefined()
188
+ })
189
+
190
+ it('should fall back to npm when local directory exists but is invalid', async () => {
191
+ const packageName = 'invalid-local-package'
192
+ const localPath = path.join(tempDir, 'test-packages', packageName)
193
+ await fs.ensureDir(localPath)
194
+ // Don't create package.json, making it invalid
195
+
196
+ const result = await resolver.resolveLocation(packageName, tempDir)
197
+
198
+ expect(result.source).toBe('npm')
199
+ expect(result.resolvedName).toBe(packageName)
200
+ expect(result.npmPackage).toBe('@test-org/test-invalid-local-package')
201
+ expect(result.localPath).toBeUndefined()
202
+ })
203
+
204
+ it('should fall back to npm when local directory does not exist', async () => {
205
+ const packageName = 'nonexistent-package'
206
+
207
+ const result = await resolver.resolveLocation(packageName, tempDir)
208
+
209
+ expect(result.source).toBe('npm')
210
+ expect(result.resolvedName).toBe(packageName)
211
+ expect(result.npmPackage).toBe('@test-org/test-nonexistent-package')
212
+ expect(result.localPath).toBeUndefined()
213
+ })
214
+
215
+ it('should trim whitespace from package names', async () => {
216
+ const packageName = 'my-package'
217
+ const localPath = path.join(tempDir, 'test-packages', packageName)
218
+ await fs.ensureDir(localPath)
219
+ await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName, version: '0.0.1' })
220
+
221
+ const result = await resolver.resolveLocation(' my-package ', tempDir)
222
+
223
+ expect(result.source).toBe('local')
224
+ expect(result.resolvedName).toBe(packageName)
225
+ expect(result.localPath).toBe(localPath)
226
+ })
227
+ })
228
+
229
+ describe('npm resolution (scoped names)', () => {
230
+ it('should always resolve scoped packages to npm', async () => {
231
+ const packageName = '@ibm/analytics'
232
+
233
+ const result = await resolver.resolveLocation(packageName, tempDir)
234
+
235
+ expect(result.source).toBe('npm')
236
+ expect(result.resolvedName).toBe(packageName)
237
+ expect(result.npmPackage).toBe(packageName)
238
+ expect(result.localPath).toBeUndefined()
239
+ })
240
+
241
+ it('should resolve scoped packages to npm even if local directory exists', async () => {
242
+ const packageName = '@ibm/analytics'
243
+ // Create a local directory with the scoped package name (edge case)
244
+ const localPath = path.join(tempDir, 'test-packages', '@ibm/analytics')
245
+ await fs.ensureDir(localPath)
246
+ await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName })
247
+
248
+ const result = await resolver.resolveLocation(packageName, tempDir)
249
+
250
+ expect(result.source).toBe('npm')
251
+ expect(result.resolvedName).toBe(packageName)
252
+ expect(result.npmPackage).toBe(packageName)
253
+ expect(result.localPath).toBeUndefined()
254
+ })
255
+
256
+ it('should handle scoped packages with hyphens', async () => {
257
+ const packageName = '@launch77-shared/plugin-release'
258
+
259
+ const result = await resolver.resolveLocation(packageName, tempDir)
260
+
261
+ expect(result.source).toBe('npm')
262
+ expect(result.resolvedName).toBe(packageName)
263
+ expect(result.npmPackage).toBe(packageName)
264
+ })
265
+
266
+ it('should trim whitespace from scoped package names', async () => {
267
+ const packageName = '@ibm/analytics'
268
+
269
+ const result = await resolver.resolveLocation(' @ibm/analytics ', tempDir)
270
+
271
+ expect(result.source).toBe('npm')
272
+ expect(result.resolvedName).toBe(packageName)
273
+ expect(result.npmPackage).toBe(packageName)
274
+ })
275
+ })
276
+
277
+ describe('edge cases', () => {
278
+ it('should handle empty workspace test-packages directory', async () => {
279
+ // Ensure the directory exists but is empty
280
+ await fs.ensureDir(path.join(tempDir, 'test-packages'))
281
+
282
+ const result = await resolver.resolveLocation('some-package', tempDir)
283
+
284
+ expect(result.source).toBe('npm')
285
+ expect(result.resolvedName).toBe('some-package')
286
+ expect(result.npmPackage).toBe('@test-org/test-some-package')
287
+ })
288
+
289
+ it('should handle missing workspace test-packages directory', async () => {
290
+ // Don't create the test-packages directory at all
291
+
292
+ const result = await resolver.resolveLocation('some-package', tempDir)
293
+
294
+ expect(result.source).toBe('npm')
295
+ expect(result.resolvedName).toBe('some-package')
296
+ expect(result.npmPackage).toBe('@test-org/test-some-package')
297
+ })
298
+
299
+ it('should handle different workspace root paths', async () => {
300
+ const customRoot = path.join(tempDir, 'custom', 'workspace')
301
+ const packageName = 'my-package'
302
+ const localPath = path.join(customRoot, 'test-packages', packageName)
303
+ await fs.ensureDir(localPath)
304
+ await fs.writeJSON(path.join(localPath, 'package.json'), { name: packageName, version: '0.0.1' })
305
+
306
+ const result = await resolver.resolveLocation(packageName, customRoot)
307
+
308
+ expect(result.source).toBe('local')
309
+ expect(result.localPath).toBe(localPath)
310
+ })
311
+ })
312
+ })
313
+ })
@@ -0,0 +1,223 @@
1
+ import * as path from 'path'
2
+
3
+ import { parsePluginName, isValidNpmPackageName } from '@launch77/plugin-runtime'
4
+ import fs from 'fs-extra'
5
+
6
+ import type { ValidationResult } from '@launch77/plugin-runtime'
7
+
8
+ /**
9
+ * Base interface for package resolution results
10
+ */
11
+ export interface PackageResolution {
12
+ /** The source of the package */
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
+ /** The version from package.json (required for local packages, undefined for npm until installed) */
21
+ version?: string
22
+ }
23
+
24
+ /**
25
+ * Abstract base class for resolving Launch77 packages
26
+ *
27
+ * Provides generic resolution logic for finding packages in:
28
+ * 1. Local workspace directory (e.g., plugins/, app-templates/)
29
+ * 2. npm packages with configured prefix (e.g., @launch77-shared/plugin-*, @launch77-shared/app-template-*)
30
+ *
31
+ * Concrete implementations must provide:
32
+ * - Folder name for local resolution
33
+ * - Package prefix for npm resolution
34
+ * - Verification logic to validate local packages
35
+ */
36
+ export abstract class PackageResolver {
37
+ /**
38
+ * Get the local folder name where packages of this type are stored
39
+ * @example 'plugins' or 'app-templates'
40
+ */
41
+ protected abstract getFolderName(): string
42
+
43
+ /**
44
+ * Get the npm package prefix for unscoped packages
45
+ * @example '@launch77-shared/plugin-' or '@launch77-shared/app-template-'
46
+ */
47
+ protected abstract getPackagePrefix(): string
48
+
49
+ /**
50
+ * Verify that a local package is valid and complete
51
+ * @param localPath - The local directory path to verify
52
+ * @returns true if the package is valid, false otherwise
53
+ */
54
+ protected abstract verify(localPath: string): Promise<boolean>
55
+
56
+ /**
57
+ * Validate package input name
58
+ *
59
+ * Accepts:
60
+ * - Unscoped names (e.g., "release", "my-package")
61
+ * - Scoped npm packages (e.g., "@ibm/package-name")
62
+ *
63
+ * Rejects:
64
+ * - Invalid formats
65
+ * - Empty strings
66
+ * - Names with invalid characters
67
+ *
68
+ * @param name - The package name to validate
69
+ * @returns ValidationResult with isValid and optional error message
70
+ *
71
+ * @example
72
+ * validateInput('release') // { isValid: true }
73
+ * validateInput('@ibm/analytics') // { isValid: true }
74
+ * validateInput('@invalid') // { isValid: false, error: '...' }
75
+ */
76
+ validateInput(name: string): ValidationResult {
77
+ if (!name || name.trim().length === 0) {
78
+ return {
79
+ isValid: false,
80
+ error: 'Package name cannot be empty',
81
+ }
82
+ }
83
+
84
+ const trimmedName = name.trim()
85
+
86
+ // Parse the name to determine type and validate
87
+ //TODO: move/rename parsePluginName() to parsePackageName()
88
+ const parsed = parsePluginName(trimmedName)
89
+
90
+ if (!parsed.isValid) {
91
+ return {
92
+ isValid: false,
93
+ error: parsed.error,
94
+ }
95
+ }
96
+
97
+ // If it's scoped, it must be a valid npm package name
98
+ if (parsed.type === 'scoped') {
99
+ return isValidNpmPackageName(trimmedName)
100
+ }
101
+
102
+ // Unscoped names are valid for both local and npm
103
+ return { isValid: true }
104
+ }
105
+
106
+ /**
107
+ * Convert an unscoped package name to an npm package name
108
+ *
109
+ * Rules:
110
+ * - Unscoped names: prefix with configured package prefix
111
+ * - Scoped names: use as-is
112
+ *
113
+ * @param name - The package name (must be validated first)
114
+ * @returns The npm package name
115
+ *
116
+ * @example
117
+ * toNpmPackageName('release') // '@launch77-shared/plugin-release' (for PluginResolver)
118
+ * toNpmPackageName('@ibm/analytics') // '@ibm/analytics'
119
+ */
120
+ toNpmPackageName(name: string): string {
121
+ const trimmedName = name.trim()
122
+
123
+ // If already scoped, use as-is
124
+ if (trimmedName.startsWith('@')) {
125
+ return trimmedName
126
+ }
127
+
128
+ // Otherwise, convert using configured prefix
129
+ return `${this.getPackagePrefix()}${trimmedName}`
130
+ }
131
+
132
+ /**
133
+ * Read version from package.json
134
+ * @param packagePath - The path to the package directory
135
+ * @returns The version string from package.json
136
+ * @throws If package.json doesn't exist, can't be read, or is missing the version field
137
+ */
138
+ private async readVersion(packagePath: string): Promise<string> {
139
+ const packageJsonPath = path.join(packagePath, 'package.json')
140
+ try {
141
+ const packageJson = await fs.readJson(packageJsonPath)
142
+ if (!packageJson.version) {
143
+ throw new Error(`Invalid package structure: package.json at ${packagePath} is missing required version field. ` + `All Launch77 packages must include a valid package.json with a version field.`)
144
+ }
145
+ return packageJson.version
146
+ } catch (error) {
147
+ // Re-throw our own error messages
148
+ if (error instanceof Error && error.message.includes('Invalid package structure')) {
149
+ throw error
150
+ }
151
+ // File not found or invalid JSON
152
+ throw new Error(`Invalid package structure: package.json not found or invalid at ${packagePath}. ` + `All Launch77 packages must include a valid package.json with a version field.`)
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Resolve package location from name
158
+ *
159
+ * Resolution order:
160
+ * 1. Check local workspace directory (configured by getFolderName())
161
+ * 2. Verify local package is valid (using verify())
162
+ * 3. Fall back to npm package name (with configured prefix)
163
+ * 4. Read version from package.json (if available)
164
+ *
165
+ * @param name - The package name to resolve
166
+ * @param workspaceRoot - The workspace root directory
167
+ * @returns PackageResolution with source, resolved location, and version
168
+ *
169
+ * @example
170
+ * // Local package found
171
+ * await resolveLocation('my-package', '/workspace')
172
+ * // { source: 'local', resolvedName: 'my-package', localPath: '/workspace/plugins/my-package', version: '1.0.0' }
173
+ *
174
+ * // Not found locally, resolve to npm
175
+ * await resolveLocation('release', '/workspace')
176
+ * // { source: 'npm', resolvedName: 'release', npmPackage: '@launch77-shared/plugin-release' }
177
+ *
178
+ * // Scoped package always resolves to npm
179
+ * await resolveLocation('@ibm/analytics', '/workspace')
180
+ * // { source: 'npm', resolvedName: '@ibm/analytics', npmPackage: '@ibm/analytics' }
181
+ */
182
+ async resolveLocation(name: string, workspaceRoot: string): Promise<PackageResolution> {
183
+ const trimmedName = name.trim()
184
+ const parsed = parsePluginName(trimmedName)
185
+
186
+ // If scoped, always use npm (local packages are never scoped)
187
+ if (parsed.type === 'scoped') {
188
+ return {
189
+ source: 'npm',
190
+ resolvedName: trimmedName,
191
+ npmPackage: trimmedName,
192
+ }
193
+ }
194
+
195
+ // Check local workspace directory
196
+ const localPath = path.join(workspaceRoot, this.getFolderName(), trimmedName)
197
+ const localExists = await fs.pathExists(localPath)
198
+
199
+ if (localExists) {
200
+ // Verify it's a valid package
201
+ const isValid = await this.verify(localPath)
202
+
203
+ if (isValid) {
204
+ const version = await this.readVersion(localPath)
205
+ return {
206
+ source: 'local',
207
+ resolvedName: trimmedName,
208
+ localPath,
209
+ version,
210
+ }
211
+ }
212
+ }
213
+
214
+ // Not found locally or invalid, resolve to npm package
215
+ const npmPackage = this.toNpmPackageName(trimmedName)
216
+
217
+ return {
218
+ source: 'npm',
219
+ resolvedName: trimmedName,
220
+ npmPackage,
221
+ }
222
+ }
223
+ }
@@ -6,7 +6,6 @@ import Handlebars from 'handlebars'
6
6
 
7
7
  export interface TemplateContext {
8
8
  appName: string
9
- startupName?: string
10
9
  port?: string
11
10
  [key: string]: string | number | boolean | undefined
12
11
  }
@@ -7,27 +7,19 @@ import ora from 'ora'
7
7
 
8
8
  import { detectLaunch77Context } from '@launch77/plugin-runtime'
9
9
  import { AppService } from '../services/app-svc.js'
10
- import { APP_TYPES, APP_TYPES_LIST } from '../types/app-types.js'
11
-
12
- import type { AppType } from '../types/app-types.js'
13
10
 
14
11
  export function createAppCommand(): Command {
15
12
  const command = new Command('app:create')
16
- .argument('<type>', 'App type (api, webapp, marketing-site)')
13
+ .argument('<template>', 'App template name (e.g., webapp, api, marketing-site)')
17
14
  .argument('<app-name>', 'Name of the application')
18
- .description('Create a new application')
19
- .option('-p, --port <port>', 'Default port for the app')
20
- .action(async (type: string, appName: string, options) => {
15
+ .description('Create a new application from a template')
16
+ .option('-p, --port <port>', 'Default port for the app (default: 3000)')
17
+ .action(async (template: string, appName: string, options) => {
21
18
  try {
22
- // Validate app type
23
- if (!APP_TYPES.includes(type as AppType)) {
24
- throw new Error(`Invalid app type: ${type}\n\nSupported types: ${APP_TYPES_LIST}`)
25
- }
26
-
27
- console.log(chalk.blue(`\nšŸš€ Creating ${type} app: ${appName}\n`))
19
+ console.log(chalk.blue(`\nšŸš€ Creating app: ${appName} from template: ${template}\n`))
28
20
 
29
- // Set default port based on app type
30
- const port = options.port || (type === 'marketing-site' || type === 'webapp' ? '3000' : '4000')
21
+ // Set default port
22
+ const port = options.port || '3000'
31
23
 
32
24
  // Detect context
33
25
  const context = await detectLaunch77Context(process.cwd())
@@ -39,7 +31,7 @@ export function createAppCommand(): Command {
39
31
  const templateSpinner = ora('Generating app structure...').start()
40
32
  let result
41
33
  try {
42
- result = await appService.createApp({ type: type as AppType, appName, port }, context)
34
+ result = await appService.createApp({ type: template, appName, port }, context)
43
35
  templateSpinner.succeed('App structure generated')
44
36
  } catch (error) {
45
37
  templateSpinner.fail('Failed to generate app structure')
@@ -47,8 +39,12 @@ export function createAppCommand(): Command {
47
39
  }
48
40
 
49
41
  // Success message
50
- console.log(chalk.green(`\nāœ… App created successfully at ${path.relative(process.cwd(), result.appPath)}`))
51
- console.log(chalk.gray(`\nRun 'npm run dev' from workspace root to start the server at http://localhost:${port}\n`))
42
+ const relativeAppPath = path.relative(process.cwd(), result.appPath)
43
+ console.log(chalk.green(`\nāœ… App created successfully at ${relativeAppPath}`))
44
+ console.log(chalk.gray(`\nNext steps:`))
45
+ console.log(chalk.gray(` cd ${relativeAppPath}`))
46
+ console.log(chalk.gray(` npm run dev`))
47
+ console.log(chalk.gray(`\nServer will start at http://localhost:${port}\n`))
52
48
  } catch (error) {
53
49
  const message = error instanceof Error ? error.message : String(error)
54
50
  console.error(chalk.red('Error:'), message)
@@ -12,7 +12,7 @@ import { AppService } from '../services/app-svc.js'
12
12
  export function deleteAppCommand(): Command {
13
13
  const command = new Command('app:delete')
14
14
  .argument('<app-name>', 'Name of the app to delete')
15
- .description('Delete an app from the startup (requires confirmation)')
15
+ .description('Delete an app from the workspace (requires confirmation)')
16
16
  .action(async (appName: string) => {
17
17
  try {
18
18
  console.log(chalk.yellow(`\nāš ļø WARNING: You are about to delete the app '${appName}'\n`))
@@ -25,7 +25,6 @@ export function validateManifestCommand(): Command {
25
25
  console.log()
26
26
  console.log(chalk.cyan('Manifest details:'))
27
27
  console.log(chalk.gray(' Schema version:'), manifest.schemaVersion)
28
- console.log(chalk.gray(' App type:'), manifest.type)
29
28
  console.log(chalk.gray(' Package:'), manifest.package)
30
29
 
31
30
  if ('name' in manifest && manifest.name) {
@@ -3,7 +3,7 @@ export { AppService } from './services/app-svc.js'
3
3
  export { ManifestService } from './services/manifest-svc.js'
4
4
 
5
5
  // Types
6
- export type { CreateAppRequest, CreateAppResult, DeleteAppRequest, DeleteAppResult, AppType } from './types/app-types.js'
6
+ export type { CreateAppRequest, CreateAppResult, DeleteAppRequest, DeleteAppResult } from './types/app-types.js'
7
7
  export type { AppManifest, DeploymentConfig } from './lib/manifest-schema.js'
8
8
 
9
9
  // Errors
@@ -12,5 +12,4 @@ export { AppAlreadyExistsError, AppNotFoundError, InvalidAppNameError, TemplateN
12
12
  // Commands
13
13
  export { createAppCommand } from './commands/create-app.js'
14
14
  export { deleteAppCommand } from './commands/delete-app.js'
15
- export { generateManifestCommand } from './commands/generate-manifest.js'
16
15
  export { validateManifestCommand } from './commands/validate-manifest.js'