@launch77/cli 1.4.0 → 1.4.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @launch77/cli
2
2
 
3
+ ## 1.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 535ae04: Moved manifest writing from generatros to CLI
8
+ - Updated dependencies [535ae04]
9
+ - @launch77/plugin-runtime@0.2.2
10
+
3
11
  ## 1.4.0
4
12
 
5
13
  ### Minor Changes
@@ -1,6 +1,22 @@
1
1
  import type { Launch77Context } from '@launch77/plugin-runtime';
2
2
  import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js';
3
3
  export declare class PluginService {
4
+ /**
5
+ * Validate that we're in a valid package directory and return the target type
6
+ */
7
+ private validateContext;
8
+ /**
9
+ * Validate plugin name, resolve its location, and download if needed
10
+ */
11
+ private validateAndResolvePlugin;
12
+ /**
13
+ * Read plugin metadata and validate it supports the current target
14
+ */
15
+ private validatePluginTargets;
16
+ /**
17
+ * Check if plugin is already installed and return early-exit result if so
18
+ */
19
+ private checkExistingInstallation;
4
20
  /**
5
21
  * Install a plugin to the current package
6
22
  */
@@ -11,5 +27,10 @@ export declare class PluginService {
11
27
  private downloadNpmPlugin;
12
28
  private getPackagePath;
13
29
  private runGenerator;
30
+ private isPluginInstalled;
31
+ /**
32
+ * Write plugin installation metadata to the target package's package.json
33
+ */
34
+ private writePluginManifest;
14
35
  }
15
36
  //# sourceMappingURL=plugin-svc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-svc.d.ts","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAwB,MAAM,0BAA0B,CAAA;AACrF,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAoBzF,qBAAa,aAAa;IACxB;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA8D3J;;OAEG;YACW,iBAAiB;IAkB/B,OAAO,CAAC,cAAc;YAgBR,YAAY;CAY3B"}
1
+ {"version":3,"file":"plugin-svc.d.ts","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAA0F,MAAM,0BAA0B,CAAA;AACvJ,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAoBzF,qBAAa,aAAa;IACxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;YACW,wBAAwB;IAkCtC;;OAEG;YACW,qBAAqB;IAcnC;;OAEG;YACW,yBAAyB;IAmBvC;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA6B3J;;OAEG;YACW,iBAAiB;IAkB/B,OAAO,CAAC,cAAc;YAgBR,YAAY;YAeZ,iBAAiB;IAmB/B;;OAEG;YACW,mBAAmB;CA4BlC"}
@@ -1,4 +1,5 @@
1
1
  import * as path from 'path';
2
+ import * as fs from 'fs/promises';
2
3
  import chalk from 'chalk';
3
4
  import { execa } from 'execa';
4
5
  import { readPluginMetadata } from '@launch77/plugin-runtime';
@@ -23,17 +24,20 @@ function locationTypeToTarget(locationType) {
23
24
  }
24
25
  export class PluginService {
25
26
  /**
26
- * Install a plugin to the current package
27
+ * Validate that we're in a valid package directory and return the target type
27
28
  */
28
- async installPlugin(request, context, logger = console.log) {
29
- const { pluginName } = request;
30
- // Must be in a package directory (app, library, plugin, or app-template)
29
+ validateContext(context) {
31
30
  const currentTarget = locationTypeToTarget(context.locationType);
32
31
  if (!currentTarget)
33
32
  throw createInvalidContextError(context.locationType);
34
33
  if (!context.appName)
35
34
  throw new InvalidPluginContextError('Could not determine package name. This is a bug. Please report it.');
36
- // Step 1: Validate plugin input
35
+ return currentTarget;
36
+ }
37
+ /**
38
+ * Validate plugin name, resolve its location, and download if needed
39
+ */
40
+ async validateAndResolvePlugin(pluginName, workspaceRoot, logger) {
37
41
  logger(chalk.blue(`\nšŸ” Resolving plugin "${pluginName}"...`));
38
42
  logger(` ā”œā”€ Validating plugin name...`);
39
43
  const validation = validatePluginInput(pluginName);
@@ -41,9 +45,8 @@ export class PluginService {
41
45
  throw new PluginResolutionError(pluginName, validation.error || 'Invalid plugin name');
42
46
  }
43
47
  logger(` │ └─ ${chalk.green('āœ“')} Valid plugin name`);
44
- // Step 2: Resolve plugin location
45
48
  logger(` ā”œā”€ Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`);
46
- const resolution = await resolvePluginLocation(pluginName, context.workspaceRoot);
49
+ const resolution = await resolvePluginLocation(pluginName, workspaceRoot);
47
50
  let pluginPath;
48
51
  if (resolution.source === 'local') {
49
52
  logger(` │ └─ ${chalk.green('āœ“')} Found local plugin`);
@@ -52,11 +55,19 @@ export class PluginService {
52
55
  else {
53
56
  logger(` │ └─ ${chalk.dim('Not found locally')}`);
54
57
  logger(` ā”œā”€ Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`);
55
- // Download npm plugin
56
- pluginPath = await this.downloadNpmPlugin(resolution.npmPackage, context.workspaceRoot, logger);
58
+ pluginPath = await this.downloadNpmPlugin(resolution.npmPackage, workspaceRoot, logger);
57
59
  }
58
60
  logger(` └─ ${chalk.green('āœ“')} Plugin resolved\n`);
59
- // Step 3: Read plugin metadata and validate targets
61
+ return {
62
+ pluginPath,
63
+ source: resolution.source,
64
+ npmPackage: resolution.npmPackage,
65
+ };
66
+ }
67
+ /**
68
+ * Read plugin metadata and validate it supports the current target
69
+ */
70
+ async validatePluginTargets(pluginPath, pluginName, currentTarget) {
60
71
  const metadata = await readPluginMetadata(pluginPath);
61
72
  if (!metadata.targets || metadata.targets.length === 0) {
62
73
  throw new MissingPluginTargetsError(pluginName);
@@ -64,10 +75,49 @@ export class PluginService {
64
75
  if (!metadata.targets.includes(currentTarget)) {
65
76
  throw createInvalidTargetError(pluginName, currentTarget, metadata.targets);
66
77
  }
67
- // Step 4: Get package directory path
78
+ return metadata;
79
+ }
80
+ /**
81
+ * Check if plugin is already installed and return early-exit result if so
82
+ */
83
+ async checkExistingInstallation(pluginName, packagePath, logger) {
84
+ const existingInstallation = await this.isPluginInstalled(pluginName, packagePath);
85
+ if (existingInstallation) {
86
+ logger(chalk.yellow(`\nā„¹ļø Plugin '${pluginName}' is already installed in this package.\n`));
87
+ logger(`Package: ${chalk.cyan(existingInstallation.package)} (${existingInstallation.source})`);
88
+ logger(`Version: ${existingInstallation.version}`);
89
+ logger(`Installed: ${existingInstallation.installedAt}\n`);
90
+ logger(chalk.gray('To reinstall: Remove from package.json launch77.installedPlugins'));
91
+ logger(chalk.gray('(plugin:remove command coming soon)\n'));
92
+ return {
93
+ pluginName,
94
+ filesInstalled: false,
95
+ packageJsonUpdated: false,
96
+ dependenciesInstalled: false,
97
+ };
98
+ }
99
+ return null;
100
+ }
101
+ /**
102
+ * Install a plugin to the current package
103
+ */
104
+ async installPlugin(request, context, logger = console.log) {
105
+ const { pluginName } = request;
106
+ const currentTarget = this.validateContext(context);
107
+ const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger);
108
+ const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget);
68
109
  const packagePath = this.getPackagePath(context);
69
- // Step 5: Run generator
110
+ const earlyExit = await this.checkExistingInstallation(pluginName, packagePath, logger);
111
+ if (earlyExit)
112
+ return earlyExit;
70
113
  await this.runGenerator(pluginPath, packagePath, context);
114
+ const packageName = source === 'npm' ? npmPackage : pluginName;
115
+ await this.writePluginManifest(packagePath, {
116
+ pluginName,
117
+ packageName,
118
+ version: metadata.version,
119
+ source,
120
+ });
71
121
  return {
72
122
  pluginName,
73
123
  filesInstalled: true,
@@ -112,7 +162,8 @@ export class PluginService {
112
162
  async runGenerator(pluginPath, appPath, context) {
113
163
  try {
114
164
  const generatorPath = path.join(pluginPath, 'dist/generator.js');
115
- await execa('node', [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`], {
165
+ const args = [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`];
166
+ await execa('node', args, {
116
167
  cwd: pluginPath,
117
168
  stdio: 'inherit',
118
169
  });
@@ -121,5 +172,48 @@ export class PluginService {
121
172
  throw new PluginInstallationError(`Generator failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
122
173
  }
123
174
  }
175
+ async isPluginInstalled(pluginName, packagePath) {
176
+ try {
177
+ const packageJsonPath = path.join(packagePath, 'package.json');
178
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
179
+ const packageJson = JSON.parse(packageJsonContent);
180
+ const manifest = packageJson.launch77;
181
+ if (manifest?.installedPlugins?.[pluginName]) {
182
+ return manifest.installedPlugins[pluginName];
183
+ }
184
+ return null;
185
+ }
186
+ catch (error) {
187
+ // If package.json doesn't exist or can't be read, assume not installed
188
+ return null;
189
+ }
190
+ }
191
+ /**
192
+ * Write plugin installation metadata to the target package's package.json
193
+ */
194
+ async writePluginManifest(packagePath, installationInfo) {
195
+ try {
196
+ const packageJsonPath = path.join(packagePath, 'package.json');
197
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
198
+ const packageJson = JSON.parse(packageJsonContent);
199
+ if (!packageJson.launch77) {
200
+ packageJson.launch77 = {};
201
+ }
202
+ if (!packageJson.launch77.installedPlugins) {
203
+ packageJson.launch77.installedPlugins = {};
204
+ }
205
+ const manifest = packageJson.launch77;
206
+ manifest.installedPlugins[installationInfo.pluginName] = {
207
+ package: installationInfo.packageName,
208
+ version: installationInfo.version,
209
+ installedAt: new Date().toISOString(),
210
+ source: installationInfo.source,
211
+ };
212
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
213
+ }
214
+ catch (error) {
215
+ throw new PluginInstallationError(`Failed to write plugin manifest: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
216
+ }
217
+ }
124
218
  }
125
219
  //# sourceMappingURL=plugin-svc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-svc.js","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin-svc.js","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin-svc.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-svc.test.d.ts","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,362 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import fs from 'fs-extra';
5
+ import { PluginService } from './plugin-svc.js';
6
+ describe('PluginService', () => {
7
+ let tempDir;
8
+ let service;
9
+ beforeEach(async () => {
10
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-svc-test-'));
11
+ service = new PluginService();
12
+ });
13
+ afterEach(async () => {
14
+ await fs.remove(tempDir);
15
+ });
16
+ describe('validateContext', () => {
17
+ // Valid contexts - test return values
18
+ test('should return "app" for workspace-app location', () => {
19
+ const context = {
20
+ isValid: true,
21
+ locationType: 'workspace-app',
22
+ workspaceRoot: tempDir,
23
+ workspaceName: 'test-workspace',
24
+ workspaceVersion: '1.0.0',
25
+ appsDir: path.join(tempDir, 'apps'),
26
+ packageName: 'test-app',
27
+ appName: 'test-app',
28
+ };
29
+ const result = service.validateContext(context);
30
+ expect(result).toBe('app');
31
+ });
32
+ test('should return "library" for workspace-library location', () => {
33
+ const context = {
34
+ isValid: true,
35
+ locationType: 'workspace-library',
36
+ workspaceRoot: tempDir,
37
+ workspaceName: 'test-workspace',
38
+ workspaceVersion: '1.0.0',
39
+ appsDir: path.join(tempDir, 'apps'),
40
+ packageName: 'test-library',
41
+ appName: 'test-library',
42
+ };
43
+ const result = service.validateContext(context);
44
+ expect(result).toBe('library');
45
+ });
46
+ test('should return "plugin" for workspace-plugin location', () => {
47
+ const context = {
48
+ isValid: true,
49
+ locationType: 'workspace-plugin',
50
+ workspaceRoot: tempDir,
51
+ workspaceName: 'test-workspace',
52
+ workspaceVersion: '1.0.0',
53
+ appsDir: path.join(tempDir, 'apps'),
54
+ packageName: 'test-plugin',
55
+ appName: 'test-plugin',
56
+ };
57
+ const result = service.validateContext(context);
58
+ expect(result).toBe('plugin');
59
+ });
60
+ test('should return "app-template" for workspace-app-template location', () => {
61
+ const context = {
62
+ isValid: true,
63
+ locationType: 'workspace-app-template',
64
+ workspaceRoot: tempDir,
65
+ workspaceName: 'test-workspace',
66
+ workspaceVersion: '1.0.0',
67
+ appsDir: path.join(tempDir, 'apps'),
68
+ packageName: 'test-template',
69
+ appName: 'test-template',
70
+ };
71
+ const result = service.validateContext(context);
72
+ expect(result).toBe('app-template');
73
+ });
74
+ // Invalid contexts - test exact error messages
75
+ test('should throw InvalidPluginContextError for workspace-root', () => {
76
+ const context = {
77
+ isValid: true,
78
+ locationType: 'workspace-root',
79
+ workspaceRoot: tempDir,
80
+ workspaceName: 'test-workspace',
81
+ workspaceVersion: '1.0.0',
82
+ appsDir: path.join(tempDir, 'apps'),
83
+ packageName: 'workspace',
84
+ appName: undefined,
85
+ };
86
+ expect(() => service.validateContext(context)).toThrow('plugin:install must be run from within a package directory.');
87
+ });
88
+ test('should throw InvalidPluginContextError for non-workspace', () => {
89
+ const context = {
90
+ isValid: false,
91
+ locationType: 'non-workspace',
92
+ workspaceRoot: tempDir,
93
+ workspaceName: 'test-workspace',
94
+ workspaceVersion: '1.0.0',
95
+ appsDir: path.join(tempDir, 'apps'),
96
+ packageName: 'unknown',
97
+ appName: undefined,
98
+ };
99
+ expect(() => service.validateContext(context)).toThrow('plugin:install must be run from within a package directory.');
100
+ });
101
+ test('should throw InvalidPluginContextError when appName is missing', () => {
102
+ const context = {
103
+ isValid: true,
104
+ locationType: 'workspace-app',
105
+ workspaceRoot: tempDir,
106
+ workspaceName: 'test-workspace',
107
+ workspaceVersion: '1.0.0',
108
+ appsDir: path.join(tempDir, 'apps'),
109
+ packageName: 'test-app',
110
+ appName: undefined,
111
+ };
112
+ expect(() => service.validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.');
113
+ });
114
+ test('should throw InvalidPluginContextError when appName is empty string', () => {
115
+ const context = {
116
+ isValid: true,
117
+ locationType: 'workspace-app',
118
+ workspaceRoot: tempDir,
119
+ workspaceName: 'test-workspace',
120
+ workspaceVersion: '1.0.0',
121
+ appsDir: path.join(tempDir, 'apps'),
122
+ packageName: 'test-app',
123
+ appName: '',
124
+ };
125
+ expect(() => service.validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.');
126
+ });
127
+ });
128
+ describe('validatePluginTargets', () => {
129
+ // Valid scenarios - verify metadata returned
130
+ test('should return metadata when plugin targets current package type', async () => {
131
+ const pluginDir = path.join(tempDir, 'test-plugin');
132
+ await fs.ensureDir(pluginDir);
133
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
134
+ name: 'test-plugin',
135
+ version: '1.0.0',
136
+ targets: ['app', 'library'],
137
+ });
138
+ const result = await service.validatePluginTargets(pluginDir, 'test-plugin', 'app');
139
+ expect(result).toEqual({
140
+ name: 'test-plugin',
141
+ version: '1.0.0',
142
+ targets: ['app', 'library'],
143
+ });
144
+ });
145
+ test('should accept plugin with multiple targets including current', async () => {
146
+ const pluginDir = path.join(tempDir, 'multi-target-plugin');
147
+ await fs.ensureDir(pluginDir);
148
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
149
+ name: 'multi-target-plugin',
150
+ version: '2.0.0',
151
+ targets: ['app', 'library', 'plugin', 'app-template'],
152
+ });
153
+ const result = await service.validatePluginTargets(pluginDir, 'multi-target-plugin', 'library');
154
+ expect(result.targets).toContain('library');
155
+ expect(result.targets).toHaveLength(4);
156
+ });
157
+ test('should handle plugin.json with optional fields (pluginDependencies, libraryDependencies)', async () => {
158
+ const pluginDir = path.join(tempDir, 'full-plugin');
159
+ await fs.ensureDir(pluginDir);
160
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
161
+ name: 'full-plugin',
162
+ version: '1.5.0',
163
+ targets: ['app'],
164
+ pluginDependencies: { 'other-plugin': '^1.0.0' },
165
+ libraryDependencies: { react: '^18.0.0' },
166
+ });
167
+ const result = await service.validatePluginTargets(pluginDir, 'full-plugin', 'app');
168
+ expect(result.pluginDependencies).toEqual({ 'other-plugin': '^1.0.0' });
169
+ expect(result.libraryDependencies).toEqual({ react: '^18.0.0' });
170
+ });
171
+ // Error scenarios - test exact error messages
172
+ test('should throw MissingPluginTargetsError when targets is undefined', async () => {
173
+ const pluginDir = path.join(tempDir, 'no-targets-plugin');
174
+ await fs.ensureDir(pluginDir);
175
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
176
+ name: 'no-targets-plugin',
177
+ version: '1.0.0',
178
+ });
179
+ await expect(service.validatePluginTargets(pluginDir, 'no-targets-plugin', 'app')).rejects.toThrow("Plugin 'no-targets-plugin' is missing the required 'targets' field in plugin.json.");
180
+ });
181
+ test('should throw MissingPluginTargetsError when targets is empty array', async () => {
182
+ const pluginDir = path.join(tempDir, 'empty-targets-plugin');
183
+ await fs.ensureDir(pluginDir);
184
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
185
+ name: 'empty-targets-plugin',
186
+ version: '1.0.0',
187
+ targets: [],
188
+ });
189
+ await expect(service.validatePluginTargets(pluginDir, 'empty-targets-plugin', 'app')).rejects.toThrow("Plugin 'empty-targets-plugin' is missing the required 'targets' field in plugin.json.");
190
+ });
191
+ test('should throw error when targets does not include current target', async () => {
192
+ const pluginDir = path.join(tempDir, 'incompatible-plugin');
193
+ await fs.ensureDir(pluginDir);
194
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
195
+ name: 'incompatible-plugin',
196
+ version: '1.0.0',
197
+ targets: ['library', 'plugin'],
198
+ });
199
+ await expect(service.validatePluginTargets(pluginDir, 'incompatible-plugin', 'app')).rejects.toThrow("Plugin 'incompatible-plugin' cannot be installed in a 'app' package.");
200
+ });
201
+ test('should handle missing plugin.json file gracefully', async () => {
202
+ const pluginDir = path.join(tempDir, 'no-plugin-json');
203
+ await fs.ensureDir(pluginDir);
204
+ await expect(service.validatePluginTargets(pluginDir, 'no-plugin-json', 'app')).rejects.toThrow();
205
+ });
206
+ });
207
+ describe('checkExistingInstallation', () => {
208
+ const mockLogger = (message) => {
209
+ /* capture logs */
210
+ };
211
+ // Not installed - return null
212
+ test('should return null when plugin is not installed', async () => {
213
+ const packageDir = path.join(tempDir, 'app1');
214
+ await fs.ensureDir(packageDir);
215
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
216
+ name: 'app1',
217
+ version: '1.0.0',
218
+ launch77: {
219
+ installedPlugins: {},
220
+ },
221
+ });
222
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
223
+ expect(result).toBeNull();
224
+ });
225
+ test('should return null when package.json does not exist', async () => {
226
+ const packageDir = path.join(tempDir, 'nonexistent');
227
+ await fs.ensureDir(packageDir);
228
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
229
+ expect(result).toBeNull();
230
+ });
231
+ test('should return null when launch77 field is missing', async () => {
232
+ const packageDir = path.join(tempDir, 'app2');
233
+ await fs.ensureDir(packageDir);
234
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
235
+ name: 'app2',
236
+ version: '1.0.0',
237
+ });
238
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
239
+ expect(result).toBeNull();
240
+ });
241
+ test('should return null when installedPlugins is missing', async () => {
242
+ const packageDir = path.join(tempDir, 'app3');
243
+ await fs.ensureDir(packageDir);
244
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
245
+ name: 'app3',
246
+ version: '1.0.0',
247
+ launch77: {},
248
+ });
249
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
250
+ expect(result).toBeNull();
251
+ });
252
+ test('should return null when installedPlugins exists but plugin not in it', async () => {
253
+ const packageDir = path.join(tempDir, 'app4');
254
+ await fs.ensureDir(packageDir);
255
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
256
+ name: 'app4',
257
+ version: '1.0.0',
258
+ launch77: {
259
+ installedPlugins: {
260
+ 'other-plugin': {
261
+ package: 'other-plugin',
262
+ version: '1.0.0',
263
+ installedAt: '2024-01-01T00:00:00.000Z',
264
+ source: 'local',
265
+ },
266
+ },
267
+ },
268
+ });
269
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
270
+ expect(result).toBeNull();
271
+ });
272
+ // Already installed - return result object
273
+ test('should return early-exit result with correct structure when plugin is installed', async () => {
274
+ const packageDir = path.join(tempDir, 'app5');
275
+ await fs.ensureDir(packageDir);
276
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
277
+ name: 'app5',
278
+ version: '1.0.0',
279
+ launch77: {
280
+ installedPlugins: {
281
+ 'test-plugin': {
282
+ package: 'test-plugin',
283
+ version: '1.5.0',
284
+ installedAt: '2024-01-15T10:30:00.000Z',
285
+ source: 'local',
286
+ },
287
+ },
288
+ },
289
+ });
290
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
291
+ expect(result).toEqual({
292
+ pluginName: 'test-plugin',
293
+ filesInstalled: false,
294
+ packageJsonUpdated: false,
295
+ dependenciesInstalled: false,
296
+ });
297
+ });
298
+ test('should log correct message for local plugin (package name matches plugin name)', async () => {
299
+ const packageDir = path.join(tempDir, 'app6');
300
+ await fs.ensureDir(packageDir);
301
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
302
+ name: 'app6',
303
+ version: '1.0.0',
304
+ launch77: {
305
+ installedPlugins: {
306
+ release: {
307
+ package: 'release',
308
+ version: '2.0.0',
309
+ installedAt: '2024-02-01T12:00:00.000Z',
310
+ source: 'local',
311
+ },
312
+ },
313
+ },
314
+ });
315
+ const logs = [];
316
+ const captureLogger = (message) => logs.push(message);
317
+ const result = await service.checkExistingInstallation('release', packageDir, captureLogger);
318
+ expect(result).not.toBeNull();
319
+ expect(logs.some((log) => log.includes("Plugin 'release' is already installed"))).toBe(true);
320
+ expect(logs.some((log) => log.includes('release') && log.includes('local'))).toBe(true);
321
+ });
322
+ test('should log correct message for npm plugin (package name is scoped)', async () => {
323
+ const packageDir = path.join(tempDir, 'app7');
324
+ await fs.ensureDir(packageDir);
325
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
326
+ name: 'app7',
327
+ version: '1.0.0',
328
+ launch77: {
329
+ installedPlugins: {
330
+ analytics: {
331
+ package: '@myorg/analytics-plugin',
332
+ version: '3.1.0',
333
+ installedAt: '2024-03-01T15:45:00.000Z',
334
+ source: 'npm',
335
+ },
336
+ },
337
+ },
338
+ });
339
+ const logs = [];
340
+ const captureLogger = (message) => logs.push(message);
341
+ const result = await service.checkExistingInstallation('analytics', packageDir, captureLogger);
342
+ expect(result).not.toBeNull();
343
+ expect(logs.some((log) => log.includes("Plugin 'analytics' is already installed"))).toBe(true);
344
+ expect(logs.some((log) => log.includes('@myorg/analytics-plugin') && log.includes('npm'))).toBe(true);
345
+ });
346
+ // Edge cases
347
+ test('should handle malformed package.json gracefully (invalid JSON)', async () => {
348
+ const packageDir = path.join(tempDir, 'app8');
349
+ await fs.ensureDir(packageDir);
350
+ await fs.writeFile(path.join(packageDir, 'package.json'), '{ invalid json }');
351
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
352
+ expect(result).toBeNull();
353
+ });
354
+ test('should handle package.json read errors (permissions, etc.)', async () => {
355
+ const packageDir = path.join(tempDir, 'app9');
356
+ // Don't create the directory - simulate permission/access error
357
+ const result = await service.checkExistingInstallation('test-plugin', packageDir, mockLogger);
358
+ expect(result).toBeNull();
359
+ });
360
+ });
361
+ });
362
+ //# sourceMappingURL=plugin-svc.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-svc.test.js","sourceRoot":"","sources":["../../../../src/modules/plugin/services/plugin-svc.test.ts"],"names":[],"mappings":"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"}
@@ -19,7 +19,8 @@
19
19
  "changeset": "changeset",
20
20
  "version-packages": "changeset version",
21
21
  "release": "turbo run build lint typecheck test && changeset publish",
22
- "prepare": "husky"
22
+ "prepare": "husky",
23
+ "clean": "rm -rf node_modules apps/*/node_modules libraries/*/node_modules plugins/*/node_modules app-templates/*/node_modules package-lock.json apps/*/package-lock.json libraries/*/package-lock.json plugins/*/package-lock.json app-templates/*/package-lock.json dist apps/*/dist libraries/*/dist plugins/*/dist app-templates/*/dist .next apps/*/.next .turbo apps/*/.turbo libraries/*/.turbo plugins/*/.turbo app-templates/*/.turbo build apps/*/build out apps/*/out coverage apps/*/coverage .cache *.log .eslintcache"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@changesets/cli": "^2.29.8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launch77/cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Launch77 Platform CLI",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -0,0 +1,418 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest'
2
+ import * as path from 'path'
3
+ import * as os from 'os'
4
+ import fs from 'fs-extra'
5
+ import { PluginService } from './plugin-svc.js'
6
+ import type { Launch77Context } from '@launch77/plugin-runtime'
7
+
8
+ describe('PluginService', () => {
9
+ let tempDir: string
10
+ let service: PluginService
11
+
12
+ beforeEach(async () => {
13
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-svc-test-'))
14
+ service = new PluginService()
15
+ })
16
+
17
+ afterEach(async () => {
18
+ await fs.remove(tempDir)
19
+ })
20
+
21
+ describe('validateContext', () => {
22
+ // Valid contexts - test return values
23
+ test('should return "app" for workspace-app location', () => {
24
+ const context: Launch77Context = {
25
+ isValid: true,
26
+ locationType: 'workspace-app',
27
+ workspaceRoot: tempDir,
28
+ workspaceName: 'test-workspace',
29
+ workspaceVersion: '1.0.0',
30
+ appsDir: path.join(tempDir, 'apps'),
31
+ packageName: 'test-app',
32
+ appName: 'test-app',
33
+ }
34
+
35
+ const result = (service as any).validateContext(context)
36
+ expect(result).toBe('app')
37
+ })
38
+
39
+ test('should return "library" for workspace-library location', () => {
40
+ const context: Launch77Context = {
41
+ isValid: true,
42
+ locationType: 'workspace-library',
43
+ workspaceRoot: tempDir,
44
+ workspaceName: 'test-workspace',
45
+ workspaceVersion: '1.0.0',
46
+ appsDir: path.join(tempDir, 'apps'),
47
+ packageName: 'test-library',
48
+ appName: 'test-library',
49
+ }
50
+
51
+ const result = (service as any).validateContext(context)
52
+ expect(result).toBe('library')
53
+ })
54
+
55
+ test('should return "plugin" for workspace-plugin location', () => {
56
+ const context: Launch77Context = {
57
+ isValid: true,
58
+ locationType: 'workspace-plugin',
59
+ workspaceRoot: tempDir,
60
+ workspaceName: 'test-workspace',
61
+ workspaceVersion: '1.0.0',
62
+ appsDir: path.join(tempDir, 'apps'),
63
+ packageName: 'test-plugin',
64
+ appName: 'test-plugin',
65
+ }
66
+
67
+ const result = (service as any).validateContext(context)
68
+ expect(result).toBe('plugin')
69
+ })
70
+
71
+ test('should return "app-template" for workspace-app-template location', () => {
72
+ const context: Launch77Context = {
73
+ isValid: true,
74
+ locationType: 'workspace-app-template',
75
+ workspaceRoot: tempDir,
76
+ workspaceName: 'test-workspace',
77
+ workspaceVersion: '1.0.0',
78
+ appsDir: path.join(tempDir, 'apps'),
79
+ packageName: 'test-template',
80
+ appName: 'test-template',
81
+ }
82
+
83
+ const result = (service as any).validateContext(context)
84
+ expect(result).toBe('app-template')
85
+ })
86
+
87
+ // Invalid contexts - test exact error messages
88
+ test('should throw InvalidPluginContextError for workspace-root', () => {
89
+ const context: Launch77Context = {
90
+ isValid: true,
91
+ locationType: 'workspace-root',
92
+ workspaceRoot: tempDir,
93
+ workspaceName: 'test-workspace',
94
+ workspaceVersion: '1.0.0',
95
+ appsDir: path.join(tempDir, 'apps'),
96
+ packageName: 'workspace',
97
+ appName: undefined,
98
+ }
99
+
100
+ expect(() => (service as any).validateContext(context)).toThrow('plugin:install must be run from within a package directory.')
101
+ })
102
+
103
+ test('should throw InvalidPluginContextError for non-workspace', () => {
104
+ const context = {
105
+ isValid: false,
106
+ locationType: 'non-workspace' as const,
107
+ workspaceRoot: tempDir,
108
+ workspaceName: 'test-workspace',
109
+ workspaceVersion: '1.0.0',
110
+ appsDir: path.join(tempDir, 'apps'),
111
+ packageName: 'unknown',
112
+ appName: undefined,
113
+ }
114
+
115
+ expect(() => (service as any).validateContext(context)).toThrow('plugin:install must be run from within a package directory.')
116
+ })
117
+
118
+ test('should throw InvalidPluginContextError when appName is missing', () => {
119
+ const context: Launch77Context = {
120
+ isValid: true,
121
+ locationType: 'workspace-app',
122
+ workspaceRoot: tempDir,
123
+ workspaceName: 'test-workspace',
124
+ workspaceVersion: '1.0.0',
125
+ appsDir: path.join(tempDir, 'apps'),
126
+ packageName: 'test-app',
127
+ appName: undefined,
128
+ }
129
+
130
+ expect(() => (service as any).validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.')
131
+ })
132
+
133
+ test('should throw InvalidPluginContextError when appName is empty string', () => {
134
+ const context: Launch77Context = {
135
+ isValid: true,
136
+ locationType: 'workspace-app',
137
+ workspaceRoot: tempDir,
138
+ workspaceName: 'test-workspace',
139
+ workspaceVersion: '1.0.0',
140
+ appsDir: path.join(tempDir, 'apps'),
141
+ packageName: 'test-app',
142
+ appName: '',
143
+ }
144
+
145
+ expect(() => (service as any).validateContext(context)).toThrow('Could not determine package name. This is a bug. Please report it.')
146
+ })
147
+ })
148
+
149
+ describe('validatePluginTargets', () => {
150
+ // Valid scenarios - verify metadata returned
151
+ test('should return metadata when plugin targets current package type', async () => {
152
+ const pluginDir = path.join(tempDir, 'test-plugin')
153
+ await fs.ensureDir(pluginDir)
154
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
155
+ name: 'test-plugin',
156
+ version: '1.0.0',
157
+ targets: ['app', 'library'],
158
+ })
159
+
160
+ const result = await (service as any).validatePluginTargets(pluginDir, 'test-plugin', 'app')
161
+ expect(result).toEqual({
162
+ name: 'test-plugin',
163
+ version: '1.0.0',
164
+ targets: ['app', 'library'],
165
+ })
166
+ })
167
+
168
+ test('should accept plugin with multiple targets including current', async () => {
169
+ const pluginDir = path.join(tempDir, 'multi-target-plugin')
170
+ await fs.ensureDir(pluginDir)
171
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
172
+ name: 'multi-target-plugin',
173
+ version: '2.0.0',
174
+ targets: ['app', 'library', 'plugin', 'app-template'],
175
+ })
176
+
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)
180
+ })
181
+
182
+ test('should handle plugin.json with optional fields (pluginDependencies, libraryDependencies)', async () => {
183
+ const pluginDir = path.join(tempDir, 'full-plugin')
184
+ await fs.ensureDir(pluginDir)
185
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
186
+ name: 'full-plugin',
187
+ version: '1.5.0',
188
+ targets: ['app'],
189
+ pluginDependencies: { 'other-plugin': '^1.0.0' },
190
+ libraryDependencies: { react: '^18.0.0' },
191
+ })
192
+
193
+ 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' })
196
+ })
197
+
198
+ // Error scenarios - test exact error messages
199
+ test('should throw MissingPluginTargetsError when targets is undefined', async () => {
200
+ const pluginDir = path.join(tempDir, 'no-targets-plugin')
201
+ await fs.ensureDir(pluginDir)
202
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
203
+ name: 'no-targets-plugin',
204
+ version: '1.0.0',
205
+ })
206
+
207
+ await expect((service as any).validatePluginTargets(pluginDir, 'no-targets-plugin', 'app')).rejects.toThrow("Plugin 'no-targets-plugin' is missing the required 'targets' field in plugin.json.")
208
+ })
209
+
210
+ test('should throw MissingPluginTargetsError when targets is empty array', async () => {
211
+ const pluginDir = path.join(tempDir, 'empty-targets-plugin')
212
+ await fs.ensureDir(pluginDir)
213
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
214
+ name: 'empty-targets-plugin',
215
+ version: '1.0.0',
216
+ targets: [],
217
+ })
218
+
219
+ await expect((service as any).validatePluginTargets(pluginDir, 'empty-targets-plugin', 'app')).rejects.toThrow("Plugin 'empty-targets-plugin' is missing the required 'targets' field in plugin.json.")
220
+ })
221
+
222
+ test('should throw error when targets does not include current target', async () => {
223
+ const pluginDir = path.join(tempDir, 'incompatible-plugin')
224
+ await fs.ensureDir(pluginDir)
225
+ await fs.writeJson(path.join(pluginDir, 'plugin.json'), {
226
+ name: 'incompatible-plugin',
227
+ version: '1.0.0',
228
+ targets: ['library', 'plugin'],
229
+ })
230
+
231
+ await expect((service as any).validatePluginTargets(pluginDir, 'incompatible-plugin', 'app')).rejects.toThrow("Plugin 'incompatible-plugin' cannot be installed in a 'app' package.")
232
+ })
233
+
234
+ test('should handle missing plugin.json file gracefully', async () => {
235
+ const pluginDir = path.join(tempDir, 'no-plugin-json')
236
+ await fs.ensureDir(pluginDir)
237
+
238
+ await expect((service as any).validatePluginTargets(pluginDir, 'no-plugin-json', 'app')).rejects.toThrow()
239
+ })
240
+ })
241
+
242
+ describe('checkExistingInstallation', () => {
243
+ const mockLogger = (message: string) => {
244
+ /* capture logs */
245
+ }
246
+
247
+ // Not installed - return null
248
+ test('should return null when plugin is not installed', async () => {
249
+ const packageDir = path.join(tempDir, 'app1')
250
+ await fs.ensureDir(packageDir)
251
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
252
+ name: 'app1',
253
+ version: '1.0.0',
254
+ launch77: {
255
+ installedPlugins: {},
256
+ },
257
+ })
258
+
259
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
260
+ expect(result).toBeNull()
261
+ })
262
+
263
+ test('should return null when package.json does not exist', async () => {
264
+ const packageDir = path.join(tempDir, 'nonexistent')
265
+ await fs.ensureDir(packageDir)
266
+
267
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
268
+ expect(result).toBeNull()
269
+ })
270
+
271
+ test('should return null when launch77 field is missing', async () => {
272
+ const packageDir = path.join(tempDir, 'app2')
273
+ await fs.ensureDir(packageDir)
274
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
275
+ name: 'app2',
276
+ version: '1.0.0',
277
+ })
278
+
279
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
280
+ expect(result).toBeNull()
281
+ })
282
+
283
+ test('should return null when installedPlugins is missing', async () => {
284
+ const packageDir = path.join(tempDir, 'app3')
285
+ await fs.ensureDir(packageDir)
286
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
287
+ name: 'app3',
288
+ version: '1.0.0',
289
+ launch77: {},
290
+ })
291
+
292
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
293
+ expect(result).toBeNull()
294
+ })
295
+
296
+ test('should return null when installedPlugins exists but plugin not in it', async () => {
297
+ const packageDir = path.join(tempDir, 'app4')
298
+ await fs.ensureDir(packageDir)
299
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
300
+ name: 'app4',
301
+ version: '1.0.0',
302
+ launch77: {
303
+ installedPlugins: {
304
+ 'other-plugin': {
305
+ package: 'other-plugin',
306
+ version: '1.0.0',
307
+ installedAt: '2024-01-01T00:00:00.000Z',
308
+ source: 'local',
309
+ },
310
+ },
311
+ },
312
+ })
313
+
314
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
315
+ expect(result).toBeNull()
316
+ })
317
+
318
+ // Already installed - return result object
319
+ test('should return early-exit result with correct structure when plugin is installed', async () => {
320
+ const packageDir = path.join(tempDir, 'app5')
321
+ await fs.ensureDir(packageDir)
322
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
323
+ name: 'app5',
324
+ version: '1.0.0',
325
+ launch77: {
326
+ installedPlugins: {
327
+ 'test-plugin': {
328
+ package: 'test-plugin',
329
+ version: '1.5.0',
330
+ installedAt: '2024-01-15T10:30:00.000Z',
331
+ source: 'local',
332
+ },
333
+ },
334
+ },
335
+ })
336
+
337
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
338
+ expect(result).toEqual({
339
+ pluginName: 'test-plugin',
340
+ filesInstalled: false,
341
+ packageJsonUpdated: false,
342
+ dependenciesInstalled: false,
343
+ })
344
+ })
345
+
346
+ test('should log correct message for local plugin (package name matches plugin name)', async () => {
347
+ const packageDir = path.join(tempDir, 'app6')
348
+ await fs.ensureDir(packageDir)
349
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
350
+ name: 'app6',
351
+ version: '1.0.0',
352
+ launch77: {
353
+ installedPlugins: {
354
+ release: {
355
+ package: 'release',
356
+ version: '2.0.0',
357
+ installedAt: '2024-02-01T12:00:00.000Z',
358
+ source: 'local',
359
+ },
360
+ },
361
+ },
362
+ })
363
+
364
+ const logs: string[] = []
365
+ const captureLogger = (message: string) => logs.push(message)
366
+
367
+ const result = await (service as any).checkExistingInstallation('release', packageDir, captureLogger)
368
+ expect(result).not.toBeNull()
369
+ expect(logs.some((log) => log.includes("Plugin 'release' is already installed"))).toBe(true)
370
+ expect(logs.some((log) => log.includes('release') && log.includes('local'))).toBe(true)
371
+ })
372
+
373
+ test('should log correct message for npm plugin (package name is scoped)', async () => {
374
+ const packageDir = path.join(tempDir, 'app7')
375
+ await fs.ensureDir(packageDir)
376
+ await fs.writeJson(path.join(packageDir, 'package.json'), {
377
+ name: 'app7',
378
+ version: '1.0.0',
379
+ launch77: {
380
+ installedPlugins: {
381
+ analytics: {
382
+ package: '@myorg/analytics-plugin',
383
+ version: '3.1.0',
384
+ installedAt: '2024-03-01T15:45:00.000Z',
385
+ source: 'npm',
386
+ },
387
+ },
388
+ },
389
+ })
390
+
391
+ const logs: string[] = []
392
+ const captureLogger = (message: string) => logs.push(message)
393
+
394
+ const result = await (service as any).checkExistingInstallation('analytics', packageDir, captureLogger)
395
+ expect(result).not.toBeNull()
396
+ expect(logs.some((log) => log.includes("Plugin 'analytics' is already installed"))).toBe(true)
397
+ expect(logs.some((log) => log.includes('@myorg/analytics-plugin') && log.includes('npm'))).toBe(true)
398
+ })
399
+
400
+ // Edge cases
401
+ test('should handle malformed package.json gracefully (invalid JSON)', async () => {
402
+ const packageDir = path.join(tempDir, 'app8')
403
+ await fs.ensureDir(packageDir)
404
+ await fs.writeFile(path.join(packageDir, 'package.json'), '{ invalid json }')
405
+
406
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
407
+ expect(result).toBeNull()
408
+ })
409
+
410
+ test('should handle package.json read errors (permissions, etc.)', async () => {
411
+ const packageDir = path.join(tempDir, 'app9')
412
+ // Don't create the directory - simulate permission/access error
413
+
414
+ const result = await (service as any).checkExistingInstallation('test-plugin', packageDir, mockLogger)
415
+ expect(result).toBeNull()
416
+ })
417
+ })
418
+ })
@@ -1,4 +1,5 @@
1
1
  import * as path from 'path'
2
+ import * as fs from 'fs/promises'
2
3
 
3
4
  import chalk from 'chalk'
4
5
  import { execa } from 'execa'
@@ -7,7 +8,7 @@ import { readPluginMetadata } from '@launch77/plugin-runtime'
7
8
  import { PluginInstallationError, InvalidPluginContextError, createInvalidContextError, MissingPluginTargetsError, createInvalidTargetError, NpmInstallationError, PluginResolutionError } from '../errors/plugin-errors.js'
8
9
  import { validatePluginInput, resolvePluginLocation } from '../lib/plugin-resolver.js'
9
10
 
10
- import type { Launch77Context, Launch77LocationType } from '@launch77/plugin-runtime'
11
+ import type { Launch77Context, Launch77LocationType, Launch77PackageManifest, InstalledPluginMetadata, PluginMetadata } from '@launch77/plugin-runtime'
11
12
  import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js'
12
13
 
13
14
  /**
@@ -30,17 +31,19 @@ function locationTypeToTarget(locationType: Launch77LocationType): string | null
30
31
 
31
32
  export class PluginService {
32
33
  /**
33
- * Install a plugin to the current package
34
+ * Validate that we're in a valid package directory and return the target type
34
35
  */
35
- async installPlugin(request: InstallPluginRequest, context: Launch77Context, logger: (message: string) => void = console.log): Promise<InstallPluginResult> {
36
- const { pluginName } = request
37
-
38
- // Must be in a package directory (app, library, plugin, or app-template)
36
+ private validateContext(context: Launch77Context): string {
39
37
  const currentTarget = locationTypeToTarget(context.locationType)
40
38
  if (!currentTarget) throw createInvalidContextError(context.locationType)
41
39
  if (!context.appName) throw new InvalidPluginContextError('Could not determine package name. This is a bug. Please report it.')
40
+ return currentTarget
41
+ }
42
42
 
43
- // Step 1: Validate plugin input
43
+ /**
44
+ * Validate plugin name, resolve its location, and download if needed
45
+ */
46
+ private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string }> {
44
47
  logger(chalk.blue(`\nšŸ” Resolving plugin "${pluginName}"...`))
45
48
  logger(` ā”œā”€ Validating plugin name...`)
46
49
 
@@ -50,9 +53,8 @@ export class PluginService {
50
53
  }
51
54
  logger(` │ └─ ${chalk.green('āœ“')} Valid plugin name`)
52
55
 
53
- // Step 2: Resolve plugin location
54
56
  logger(` ā”œā”€ Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`)
55
- const resolution = await resolvePluginLocation(pluginName, context.workspaceRoot)
57
+ const resolution = await resolvePluginLocation(pluginName, workspaceRoot)
56
58
 
57
59
  let pluginPath: string
58
60
 
@@ -63,13 +65,22 @@ export class PluginService {
63
65
  logger(` │ └─ ${chalk.dim('Not found locally')}`)
64
66
  logger(` ā”œā”€ Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`)
65
67
 
66
- // Download npm plugin
67
- pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, context.workspaceRoot, logger)
68
+ pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, workspaceRoot, logger)
68
69
  }
69
70
 
70
71
  logger(` └─ ${chalk.green('āœ“')} Plugin resolved\n`)
71
72
 
72
- // Step 3: Read plugin metadata and validate targets
73
+ return {
74
+ pluginPath,
75
+ source: resolution.source,
76
+ npmPackage: resolution.npmPackage,
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Read plugin metadata and validate it supports the current target
82
+ */
83
+ private async validatePluginTargets(pluginPath: string, pluginName: string, currentTarget: string): Promise<PluginMetadata> {
73
84
  const metadata = await readPluginMetadata(pluginPath)
74
85
 
75
86
  if (!metadata.targets || metadata.targets.length === 0) {
@@ -80,12 +91,55 @@ export class PluginService {
80
91
  throw createInvalidTargetError(pluginName, currentTarget, metadata.targets)
81
92
  }
82
93
 
83
- // Step 4: Get package directory path
94
+ return metadata
95
+ }
96
+
97
+ /**
98
+ * Check if plugin is already installed and return early-exit result if so
99
+ */
100
+ private async checkExistingInstallation(pluginName: string, packagePath: string, logger: (message: string) => void): Promise<InstallPluginResult | null> {
101
+ const existingInstallation = await this.isPluginInstalled(pluginName, packagePath)
102
+ if (existingInstallation) {
103
+ logger(chalk.yellow(`\nā„¹ļø Plugin '${pluginName}' is already installed in this package.\n`))
104
+ logger(`Package: ${chalk.cyan(existingInstallation.package)} (${existingInstallation.source})`)
105
+ logger(`Version: ${existingInstallation.version}`)
106
+ logger(`Installed: ${existingInstallation.installedAt}\n`)
107
+ logger(chalk.gray('To reinstall: Remove from package.json launch77.installedPlugins'))
108
+ logger(chalk.gray('(plugin:remove command coming soon)\n'))
109
+ return {
110
+ pluginName,
111
+ filesInstalled: false,
112
+ packageJsonUpdated: false,
113
+ dependenciesInstalled: false,
114
+ }
115
+ }
116
+ return null
117
+ }
118
+
119
+ /**
120
+ * Install a plugin to the current package
121
+ */
122
+ async installPlugin(request: InstallPluginRequest, context: Launch77Context, logger: (message: string) => void = console.log): Promise<InstallPluginResult> {
123
+ const { pluginName } = request
124
+
125
+ const currentTarget = this.validateContext(context)
126
+ const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
127
+ const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget)
128
+
84
129
  const packagePath = this.getPackagePath(context)
130
+ const earlyExit = await this.checkExistingInstallation(pluginName, packagePath, logger)
131
+ if (earlyExit) return earlyExit
85
132
 
86
- // Step 5: Run generator
87
133
  await this.runGenerator(pluginPath, packagePath, context)
88
134
 
135
+ const packageName = source === 'npm' ? npmPackage! : pluginName
136
+ await this.writePluginManifest(packagePath, {
137
+ pluginName,
138
+ packageName,
139
+ version: metadata.version,
140
+ source,
141
+ })
142
+
89
143
  return {
90
144
  pluginName,
91
145
  filesInstalled: true,
@@ -135,7 +189,9 @@ export class PluginService {
135
189
  try {
136
190
  const generatorPath = path.join(pluginPath, 'dist/generator.js')
137
191
 
138
- await execa('node', [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`], {
192
+ const args = [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`]
193
+
194
+ await execa('node', args, {
139
195
  cwd: pluginPath,
140
196
  stdio: 'inherit',
141
197
  })
@@ -143,4 +199,55 @@ export class PluginService {
143
199
  throw new PluginInstallationError(`Generator failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined)
144
200
  }
145
201
  }
202
+
203
+ private async isPluginInstalled(pluginName: string, packagePath: string): Promise<InstalledPluginMetadata | null> {
204
+ try {
205
+ const packageJsonPath = path.join(packagePath, 'package.json')
206
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
207
+ const packageJson = JSON.parse(packageJsonContent)
208
+
209
+ const manifest = packageJson.launch77 as Launch77PackageManifest | undefined
210
+
211
+ if (manifest?.installedPlugins?.[pluginName]) {
212
+ return manifest.installedPlugins[pluginName]
213
+ }
214
+
215
+ return null
216
+ } catch (error) {
217
+ // If package.json doesn't exist or can't be read, assume not installed
218
+ return null
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Write plugin installation metadata to the target package's package.json
224
+ */
225
+ private async writePluginManifest(packagePath: string, installationInfo: { pluginName: string; packageName: string; version: string; source: 'local' | 'npm' }): Promise<void> {
226
+ try {
227
+ const packageJsonPath = path.join(packagePath, 'package.json')
228
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
229
+ const packageJson = JSON.parse(packageJsonContent)
230
+
231
+ if (!packageJson.launch77) {
232
+ packageJson.launch77 = {}
233
+ }
234
+
235
+ if (!packageJson.launch77.installedPlugins) {
236
+ packageJson.launch77.installedPlugins = {}
237
+ }
238
+
239
+ const manifest = packageJson.launch77 as Launch77PackageManifest
240
+
241
+ manifest.installedPlugins![installationInfo.pluginName] = {
242
+ package: installationInfo.packageName,
243
+ version: installationInfo.version,
244
+ installedAt: new Date().toISOString(),
245
+ source: installationInfo.source,
246
+ }
247
+
248
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8')
249
+ } catch (error) {
250
+ throw new PluginInstallationError(`Failed to write plugin manifest: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined)
251
+ }
252
+ }
146
253
  }
@@ -19,7 +19,8 @@
19
19
  "changeset": "changeset",
20
20
  "version-packages": "changeset version",
21
21
  "release": "turbo run build lint typecheck test && changeset publish",
22
- "prepare": "husky"
22
+ "prepare": "husky",
23
+ "clean": "rm -rf node_modules apps/*/node_modules libraries/*/node_modules plugins/*/node_modules app-templates/*/node_modules package-lock.json apps/*/package-lock.json libraries/*/package-lock.json plugins/*/package-lock.json app-templates/*/package-lock.json dist apps/*/dist libraries/*/dist plugins/*/dist app-templates/*/dist .next apps/*/.next .turbo apps/*/.turbo libraries/*/.turbo plugins/*/.turbo app-templates/*/.turbo build apps/*/build out apps/*/out coverage apps/*/coverage .cache *.log .eslintcache"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@changesets/cli": "^2.29.8",