@launch77/plugin-runtime 0.3.2 → 0.3.3

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/dist/index.js CHANGED
@@ -1,65 +1,469 @@
1
- // src/generator.ts
1
+ // src/modules/workspace/services/workspace-service.ts
2
+ import * as path3 from "path";
3
+
4
+ // src/modules/workspace/services/workspace-manifest-service.ts
5
+ import * as path from "path";
6
+ import fs from "fs-extra";
7
+ var _WorkspaceManifestService = class _WorkspaceManifestService {
8
+ /**
9
+ * Check if a workspace manifest exists at the given root
10
+ */
11
+ async exists(workspaceRoot) {
12
+ const manifestPath = path.join(workspaceRoot, _WorkspaceManifestService.WORKSPACE_MANIFEST);
13
+ return await fs.pathExists(manifestPath);
14
+ }
15
+ /**
16
+ * Read the workspace manifest
17
+ */
18
+ async readWorkspaceManifest(workspaceRoot) {
19
+ const manifestPath = path.join(workspaceRoot, _WorkspaceManifestService.WORKSPACE_MANIFEST);
20
+ if (!await fs.pathExists(manifestPath)) {
21
+ return null;
22
+ }
23
+ try {
24
+ return await fs.readJSON(manifestPath);
25
+ } catch (error) {
26
+ throw new Error(`Failed to read workspace manifest: ${error instanceof Error ? error.message : "Unknown error"}`);
27
+ }
28
+ }
29
+ /**
30
+ * Write the workspace manifest
31
+ */
32
+ async writeWorkspaceManifest(workspaceRoot, manifest) {
33
+ const manifestPath = path.join(workspaceRoot, _WorkspaceManifestService.WORKSPACE_MANIFEST);
34
+ try {
35
+ await fs.ensureDir(path.dirname(manifestPath));
36
+ await fs.writeJSON(manifestPath, manifest, { spaces: 2 });
37
+ } catch (error) {
38
+ throw new Error(`Failed to write workspace manifest: ${error instanceof Error ? error.message : "Unknown error"}`);
39
+ }
40
+ }
41
+ /**
42
+ * Get the manifest file path for a workspace
43
+ */
44
+ getManifestPath(workspaceRoot) {
45
+ return path.join(workspaceRoot, _WorkspaceManifestService.WORKSPACE_MANIFEST);
46
+ }
47
+ };
48
+ _WorkspaceManifestService.WORKSPACE_MANIFEST = ".launch77/workspace.json";
49
+ var WorkspaceManifestService = _WorkspaceManifestService;
50
+
51
+ // src/modules/workspace/utils/location-parser.ts
52
+ import * as path2 from "path";
53
+ function parseLocationFromPath(cwdPath, workspaceRoot) {
54
+ const relativePath = path2.relative(workspaceRoot, cwdPath);
55
+ if (!relativePath || relativePath === ".") {
56
+ return { locationType: "workspace-root" };
57
+ }
58
+ const parts = relativePath.split(path2.sep);
59
+ if (parts[0] === "apps" && parts.length >= 2) {
60
+ return {
61
+ locationType: "workspace-app",
62
+ appName: parts[1]
63
+ };
64
+ }
65
+ if (parts[0] === "libraries" && parts.length >= 2) {
66
+ return {
67
+ locationType: "workspace-library",
68
+ appName: parts[1]
69
+ };
70
+ }
71
+ if (parts[0] === "plugins" && parts.length >= 2) {
72
+ return {
73
+ locationType: "workspace-plugin",
74
+ appName: parts[1]
75
+ };
76
+ }
77
+ if (parts[0] === "app-templates" && parts.length >= 2) {
78
+ return {
79
+ locationType: "workspace-app-template",
80
+ appName: parts[1]
81
+ };
82
+ }
83
+ return { locationType: "workspace-root" };
84
+ }
85
+
86
+ // src/modules/workspace/services/workspace-service.ts
87
+ var WorkspaceService = class {
88
+ constructor(workspaceManifestService) {
89
+ this.workspaceManifestService = workspaceManifestService || new WorkspaceManifestService();
90
+ }
91
+ /**
92
+ * Check if a directory is a Launch77 workspace root
93
+ */
94
+ async isWorkspaceRoot(dir) {
95
+ return await this.workspaceManifestService.exists(dir);
96
+ }
97
+ /**
98
+ * Get the workspace root directory from the current directory
99
+ */
100
+ async findWorkspaceRoot(startDir) {
101
+ let currentDir = path3.resolve(startDir);
102
+ while (currentDir !== path3.dirname(currentDir)) {
103
+ if (await this.isWorkspaceRoot(currentDir)) {
104
+ return currentDir;
105
+ }
106
+ currentDir = path3.dirname(currentDir);
107
+ }
108
+ return null;
109
+ }
110
+ /**
111
+ * Validate that we're in a Launch77 workspace context
112
+ */
113
+ async validateWorkspaceContext(context) {
114
+ if (context.locationType === "unknown") {
115
+ return {
116
+ valid: false,
117
+ errorMessage: "Must be run from within a Launch77 workspace.\n\nCreate a workspace first:\n launch77 init my-workspace\n cd my-workspace"
118
+ };
119
+ }
120
+ return { valid: true };
121
+ }
122
+ /**
123
+ * Validate a workspace context using ValidationResult format
124
+ */
125
+ validateContext(context) {
126
+ if (context.locationType === "unknown") {
127
+ return {
128
+ isValid: false,
129
+ errors: ["Must be run from within a Launch77 workspace. Create a workspace first: launch77 init my-workspace"]
130
+ };
131
+ }
132
+ return { isValid: true };
133
+ }
134
+ /**
135
+ * Validate an app name
136
+ */
137
+ validateAppName(name) {
138
+ const trimmed = name.trim();
139
+ if (!trimmed) {
140
+ return { isValid: false, errors: ["App name cannot be empty"] };
141
+ }
142
+ const validPattern = /^[a-z][a-z0-9-]*$/;
143
+ if (!validPattern.test(trimmed)) {
144
+ return {
145
+ isValid: false,
146
+ errors: ["App name must start with lowercase letter and contain only lowercase letters, numbers, and hyphens"]
147
+ };
148
+ }
149
+ if (trimmed.length > 50) {
150
+ return { isValid: false, errors: ["App name must be 50 characters or less"] };
151
+ }
152
+ return { isValid: true };
153
+ }
154
+ /**
155
+ * Detect the Launch77 workspace context from the current working directory
156
+ *
157
+ * @param cwd - Current working directory path
158
+ * @returns Context information about the workspace location
159
+ */
160
+ async detectLaunch77Context(cwd) {
161
+ const resolvedCwd = path3.resolve(cwd);
162
+ const workspaceRoot = await this.findWorkspaceRoot(resolvedCwd);
163
+ if (!workspaceRoot) {
164
+ return {
165
+ isValid: false,
166
+ locationType: "unknown",
167
+ workspaceRoot: "",
168
+ appsDir: "",
169
+ workspaceVersion: "",
170
+ workspaceName: "",
171
+ packageName: ""
172
+ };
173
+ }
174
+ let manifest;
175
+ try {
176
+ manifest = await this.workspaceManifestService.readWorkspaceManifest(workspaceRoot);
177
+ if (!manifest) {
178
+ return {
179
+ isValid: false,
180
+ locationType: "unknown",
181
+ workspaceRoot: "",
182
+ appsDir: "",
183
+ workspaceVersion: "",
184
+ workspaceName: "",
185
+ packageName: ""
186
+ };
187
+ }
188
+ } catch (error) {
189
+ return {
190
+ isValid: false,
191
+ locationType: "unknown",
192
+ workspaceRoot: "",
193
+ appsDir: "",
194
+ workspaceVersion: "",
195
+ workspaceName: "",
196
+ packageName: ""
197
+ };
198
+ }
199
+ const parsed = parseLocationFromPath(resolvedCwd, workspaceRoot);
200
+ const workspaceName = path3.basename(workspaceRoot);
201
+ const appsDir = path3.join(workspaceRoot, "apps");
202
+ const packageName = parsed.appName ? `@${workspaceName}/${parsed.appName}` : "";
203
+ return {
204
+ isValid: true,
205
+ locationType: parsed.locationType,
206
+ workspaceRoot,
207
+ appsDir,
208
+ workspaceVersion: manifest.version,
209
+ workspaceName,
210
+ appName: parsed.appName,
211
+ packageName
212
+ };
213
+ }
214
+ };
215
+ async function detectLaunch77Context(cwd) {
216
+ const service = new WorkspaceService();
217
+ return service.detectLaunch77Context(cwd);
218
+ }
219
+
220
+ // src/modules/plugin/generators/generator.ts
2
221
  var Generator = class {
3
222
  constructor(context) {
4
223
  this.context = context;
5
224
  }
6
225
  };
7
226
 
8
- // src/standard-generator.ts
9
- import * as path3 from "path";
10
- import * as fs3 from "fs/promises";
227
+ // src/modules/plugin/generators/standard-generator.ts
228
+ import * as fs4 from "fs/promises";
229
+ import * as path6 from "path";
11
230
  import chalk from "chalk";
12
231
  import { execa } from "execa";
13
232
 
14
- // src/utils/file-operations.ts
15
- import * as fs from "fs/promises";
16
- import * as path from "path";
17
- async function copyRecursive(src, dest) {
18
- const stat2 = await fs.stat(src);
19
- if (stat2.isDirectory()) {
20
- await fs.mkdir(dest, { recursive: true });
21
- const entries = await fs.readdir(src);
22
- for (const entry of entries) {
23
- await copyRecursive(path.join(src, entry), path.join(dest, entry));
233
+ // src/modules/filesystem/services/filesystem-service.ts
234
+ import * as path4 from "path";
235
+ import fs2 from "fs-extra";
236
+ var FilesystemService = class {
237
+ /**
238
+ * Check if a path exists
239
+ */
240
+ async pathExists(filePath) {
241
+ try {
242
+ return await fs2.pathExists(filePath);
243
+ } catch {
244
+ return false;
24
245
  }
25
- } else {
26
- await fs.copyFile(src, dest);
27
246
  }
28
- }
29
- async function pathExists(filePath) {
30
- try {
31
- await fs.access(filePath);
32
- return true;
33
- } catch {
34
- return false;
247
+ /**
248
+ * Copy a file or directory recursively
249
+ */
250
+ async copyRecursive(source, destination, options) {
251
+ try {
252
+ await fs2.copy(source, destination, {
253
+ overwrite: options?.overwrite ?? true,
254
+ errorOnExist: false
255
+ });
256
+ } catch (error) {
257
+ throw new Error(`Failed to copy from ${source} to ${destination}: ${error instanceof Error ? error.message : "Unknown error"}`);
258
+ }
35
259
  }
36
- }
260
+ /**
261
+ * Read a JSON file
262
+ */
263
+ async readJSON(filePath) {
264
+ try {
265
+ return await fs2.readJSON(filePath);
266
+ } catch (error) {
267
+ throw new Error(`Failed to read JSON from ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
268
+ }
269
+ }
270
+ /**
271
+ * Write a JSON file
272
+ */
273
+ async writeJSON(filePath, data, options) {
274
+ try {
275
+ await fs2.writeJSON(filePath, data, { spaces: options?.spaces ?? 2 });
276
+ } catch (error) {
277
+ throw new Error(`Failed to write JSON to ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
278
+ }
279
+ }
280
+ /**
281
+ * Read a text file
282
+ */
283
+ async readFile(filePath, encoding = "utf8") {
284
+ try {
285
+ return await fs2.readFile(filePath, encoding);
286
+ } catch (error) {
287
+ throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
288
+ }
289
+ }
290
+ /**
291
+ * Write a text file
292
+ */
293
+ async writeFile(filePath, content, encoding = "utf8") {
294
+ try {
295
+ await fs2.writeFile(filePath, content, encoding);
296
+ } catch (error) {
297
+ throw new Error(`Failed to write file ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
298
+ }
299
+ }
300
+ /**
301
+ * Create a directory (including parent directories)
302
+ */
303
+ async ensureDir(dirPath) {
304
+ try {
305
+ await fs2.ensureDir(dirPath);
306
+ } catch (error) {
307
+ throw new Error(`Failed to create directory ${dirPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
308
+ }
309
+ }
310
+ /**
311
+ * Remove a file or directory
312
+ */
313
+ async remove(filePath) {
314
+ try {
315
+ await fs2.remove(filePath);
316
+ } catch (error) {
317
+ throw new Error(`Failed to remove ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
318
+ }
319
+ }
320
+ /**
321
+ * List files in a directory
322
+ */
323
+ async readDir(dirPath) {
324
+ try {
325
+ return await fs2.readdir(dirPath);
326
+ } catch (error) {
327
+ throw new Error(`Failed to read directory ${dirPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
328
+ }
329
+ }
330
+ /**
331
+ * Get file or directory stats
332
+ */
333
+ async getStats(filePath) {
334
+ try {
335
+ return await fs2.stat(filePath);
336
+ } catch (error) {
337
+ throw new Error(`Failed to get stats for ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
338
+ }
339
+ }
340
+ /**
341
+ * Check if a path is a directory
342
+ */
343
+ async isDirectory(filePath) {
344
+ try {
345
+ const stats = await this.getStats(filePath);
346
+ return stats.isDirectory();
347
+ } catch {
348
+ return false;
349
+ }
350
+ }
351
+ /**
352
+ * Check if a path is a file
353
+ */
354
+ async isFile(filePath) {
355
+ try {
356
+ const stats = await this.getStats(filePath);
357
+ return stats.isFile();
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+ /**
363
+ * Move a file or directory
364
+ */
365
+ async move(source, destination, options) {
366
+ try {
367
+ await fs2.move(source, destination, {
368
+ overwrite: options?.overwrite ?? false
369
+ });
370
+ } catch (error) {
371
+ throw new Error(`Failed to move from ${source} to ${destination}: ${error instanceof Error ? error.message : "Unknown error"}`);
372
+ }
373
+ }
374
+ /**
375
+ * Create a temporary directory
376
+ */
377
+ async createTempDir(prefix) {
378
+ const os2 = await import("os");
379
+ try {
380
+ return await fs2.mkdtemp(path4.join(os2.tmpdir(), prefix));
381
+ } catch (error) {
382
+ throw new Error(`Failed to create temp directory: ${error instanceof Error ? error.message : "Unknown error"}`);
383
+ }
384
+ }
385
+ };
37
386
 
38
- // src/utils/metadata.ts
39
- import * as fs2 from "fs/promises";
40
- import * as path2 from "path";
41
- async function readPluginMetadata(pluginPath) {
42
- const metadataPath = path2.join(pluginPath, "plugin.json");
43
- const content = await fs2.readFile(metadataPath, "utf-8");
44
- const parsed = JSON.parse(content);
45
- return {
46
- targets: parsed.targets || [],
47
- pluginDependencies: parsed.pluginDependencies,
48
- libraryDependencies: parsed.libraryDependencies
49
- };
50
- }
51
- async function readTemplateMetadata(templatePath) {
52
- const templateJsonPath = path2.join(templatePath, "template.json");
53
- try {
54
- const content = await fs2.readFile(templateJsonPath, "utf-8");
55
- return JSON.parse(content);
56
- } catch {
57
- return {};
387
+ // src/modules/plugin/services/metadata-service.ts
388
+ import * as path5 from "path";
389
+ import fs3 from "fs-extra";
390
+ var MetadataService = class {
391
+ /**
392
+ * Read plugin metadata from a plugin directory
393
+ */
394
+ async readPluginMetadata(pluginPath) {
395
+ const metadataPath = path5.join(pluginPath, "plugin.json");
396
+ if (!await fs3.pathExists(metadataPath)) {
397
+ throw new Error(`Plugin metadata file not found at: ${metadataPath}`);
398
+ }
399
+ try {
400
+ const fullMetadata = await fs3.readJSON(metadataPath);
401
+ const { name: _name, version: _version, ...metadata } = fullMetadata;
402
+ return metadata;
403
+ } catch (error) {
404
+ throw new Error(`Failed to read plugin metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
405
+ }
58
406
  }
59
- }
407
+ /**
408
+ * Read template metadata from a template directory
409
+ */
410
+ async readTemplateMetadata(templatePath) {
411
+ const metadataPath = path5.join(templatePath, "template.json");
412
+ if (!await fs3.pathExists(metadataPath)) {
413
+ throw new Error(`Template metadata file not found at: ${metadataPath}`);
414
+ }
415
+ try {
416
+ const metadata = await fs3.readJSON(metadataPath);
417
+ return metadata;
418
+ } catch (error) {
419
+ throw new Error(`Failed to read template metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
420
+ }
421
+ }
422
+ /**
423
+ * Check if a plugin metadata file exists
424
+ */
425
+ async hasPluginMetadata(pluginPath) {
426
+ const metadataPath = path5.join(pluginPath, "plugin.json");
427
+ return await fs3.pathExists(metadataPath);
428
+ }
429
+ /**
430
+ * Check if a template metadata file exists
431
+ */
432
+ async hasTemplateMetadata(templatePath) {
433
+ const metadataPath = path5.join(templatePath, "template.json");
434
+ return await fs3.pathExists(metadataPath);
435
+ }
436
+ /**
437
+ * Write plugin metadata to a directory
438
+ */
439
+ async writePluginMetadata(pluginPath, metadata) {
440
+ const metadataPath = path5.join(pluginPath, "plugin.json");
441
+ try {
442
+ await fs3.writeJSON(metadataPath, metadata, { spaces: 2 });
443
+ } catch (error) {
444
+ throw new Error(`Failed to write plugin metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
445
+ }
446
+ }
447
+ /**
448
+ * Write template metadata to a directory
449
+ */
450
+ async writeTemplateMetadata(templatePath, metadata) {
451
+ const metadataPath = path5.join(templatePath, "template.json");
452
+ try {
453
+ await fs3.writeJSON(metadataPath, metadata, { spaces: 2 });
454
+ } catch (error) {
455
+ throw new Error(`Failed to write template metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
456
+ }
457
+ }
458
+ };
60
459
 
61
- // src/standard-generator.ts
460
+ // src/modules/plugin/generators/standard-generator.ts
62
461
  var StandardGenerator = class extends Generator {
462
+ constructor(context) {
463
+ super(context);
464
+ this.filesystemService = new FilesystemService();
465
+ this.metadataService = new MetadataService();
466
+ }
63
467
  async run() {
64
468
  console.log(chalk.green(`
65
469
  \u2705 Installing plugin...
@@ -74,21 +478,21 @@ var StandardGenerator = class extends Generator {
74
478
  this.showNextSteps();
75
479
  }
76
480
  async updateDependencies() {
77
- const pluginJsonPath = path3.join(this.context.pluginPath, "plugin.json");
78
- if (!await pathExists(pluginJsonPath)) return;
481
+ const pluginJsonPath = path6.join(this.context.pluginPath, "plugin.json");
482
+ if (!await this.filesystemService.pathExists(pluginJsonPath)) return;
79
483
  try {
80
- const pluginMetadata = await readPluginMetadata(this.context.pluginPath);
484
+ const pluginMetadata = await this.metadataService.readPluginMetadata(this.context.pluginPath);
81
485
  if (!pluginMetadata.libraryDependencies || Object.keys(pluginMetadata.libraryDependencies).length === 0) {
82
486
  return;
83
487
  }
84
- const packageJsonPath = path3.join(this.context.appPath, "package.json");
85
- const packageJsonContent = await fs3.readFile(packageJsonPath, "utf-8");
488
+ const packageJsonPath = path6.join(this.context.appPath, "package.json");
489
+ const packageJsonContent = await fs4.readFile(packageJsonPath, "utf-8");
86
490
  const packageJson = JSON.parse(packageJsonContent);
87
491
  packageJson.dependencies = {
88
492
  ...packageJson.dependencies,
89
493
  ...pluginMetadata.libraryDependencies
90
494
  };
91
- await fs3.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
495
+ await fs4.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
92
496
  console.log(chalk.green(" \u2713 Updated package.json with dependencies"));
93
497
  } catch (error) {
94
498
  console.log(chalk.yellow(` \u26A0\uFE0F Could not update dependencies: ${error}`));
@@ -97,7 +501,7 @@ var StandardGenerator = class extends Generator {
97
501
  async installDependencies() {
98
502
  try {
99
503
  console.log(chalk.cyan(" Installing dependencies..."));
100
- const workspaceRoot = path3.dirname(path3.dirname(this.context.appPath));
504
+ const workspaceRoot = path6.dirname(path6.dirname(this.context.appPath));
101
505
  await execa("npm", ["install"], {
102
506
  cwd: workspaceRoot,
103
507
  stdio: "pipe"
@@ -108,10 +512,10 @@ var StandardGenerator = class extends Generator {
108
512
  }
109
513
  }
110
514
  async copyTemplates() {
111
- const templatesDir = path3.join(this.context.pluginPath, "templates");
112
- if (!await pathExists(templatesDir)) return;
515
+ const templatesDir = path6.join(this.context.pluginPath, "templates");
516
+ if (!await this.filesystemService.pathExists(templatesDir)) return;
113
517
  try {
114
- await copyRecursive(templatesDir, this.context.appPath);
518
+ await this.filesystemService.copyRecursive(templatesDir, this.context.appPath);
115
519
  console.log(chalk.green(" \u2713 Copied template files"));
116
520
  } catch (error) {
117
521
  console.log(chalk.yellow(` \u26A0\uFE0F Could not copy template files: ${error}`));
@@ -123,288 +527,1232 @@ var StandardGenerator = class extends Generator {
123
527
  }
124
528
  };
125
529
 
126
- // src/context/index.ts
127
- import * as path6 from "path";
530
+ // src/modules/plugin/services/plugin-service.ts
531
+ import * as fs8 from "fs/promises";
532
+ import * as path11 from "path";
533
+ import chalk2 from "chalk";
534
+ import { execa as execa4 } from "execa";
535
+ import fsExtra from "fs-extra";
128
536
 
129
- // src/context/location-parser.ts
130
- import * as path4 from "path";
131
- function parseLocationFromPath(cwdPath, workspaceRoot) {
132
- const relativePath = path4.relative(workspaceRoot, cwdPath);
133
- if (!relativePath || relativePath === ".") {
134
- return { locationType: "workspace-root" };
537
+ // src/modules/npm/services/npm-service.ts
538
+ import * as os from "os";
539
+ import * as path7 from "path";
540
+ import { execa as execa2 } from "execa";
541
+ import fs5 from "fs-extra";
542
+ var NpmService = class {
543
+ /**
544
+ * Validate an npm package name (scoped or unscoped)
545
+ *
546
+ * Rules for unscoped packages:
547
+ * - Must start with a lowercase letter
548
+ * - Can contain lowercase letters, numbers, hyphens, periods, underscores
549
+ *
550
+ * Rules for scoped packages:
551
+ * - Format: @org/package
552
+ * - Both org and package follow npm naming rules
553
+ *
554
+ * @param name - The npm package name to validate
555
+ * @returns ValidationResult indicating if valid and error message if not
556
+ */
557
+ validatePackageName(name) {
558
+ if (!name || name.trim().length === 0) {
559
+ return {
560
+ isValid: false,
561
+ error: "Package name cannot be empty"
562
+ };
563
+ }
564
+ const trimmedName = name.trim();
565
+ if (trimmedName.startsWith("@")) {
566
+ const scopedPattern = /^@([a-z0-9._-]+)\/([a-z0-9._-]+)$/;
567
+ if (!scopedPattern.test(trimmedName)) {
568
+ return {
569
+ isValid: false,
570
+ error: "Scoped package must be in format @org/package where org and package contain only lowercase letters, numbers, hyphens, periods, and underscores"
571
+ };
572
+ }
573
+ return { isValid: true };
574
+ }
575
+ const validPattern = /^[a-z0-9][a-z0-9._-]*$/;
576
+ if (!validPattern.test(trimmedName)) {
577
+ return {
578
+ isValid: false,
579
+ error: "Package name must be lowercase and contain only letters (a-z), numbers (0-9), hyphens (-), periods (.), and underscores (_)"
580
+ };
581
+ }
582
+ return { isValid: true };
135
583
  }
136
- const parts = relativePath.split(path4.sep);
137
- if (parts[0] === "apps" && parts.length >= 2) {
584
+ /**
585
+ * Parse a package name input and determine its type
586
+ *
587
+ * Returns information about whether the input is:
588
+ * - A scoped npm package (e.g., @org/package)
589
+ * - An unscoped name (e.g., my-package)
590
+ * - Invalid
591
+ */
592
+ parsePackageName(name) {
593
+ if (!name || name.trim().length === 0) {
594
+ return {
595
+ type: "invalid",
596
+ isValid: false,
597
+ error: "Package name cannot be empty"
598
+ };
599
+ }
600
+ const trimmedName = name.trim();
601
+ const validation = this.validatePackageName(trimmedName);
602
+ if (!validation.isValid) {
603
+ return {
604
+ type: "invalid",
605
+ isValid: false,
606
+ error: validation.error
607
+ };
608
+ }
609
+ if (trimmedName.startsWith("@")) {
610
+ const match = trimmedName.match(/^@([a-z0-9._-]+)\/([a-z0-9._-]+)$/);
611
+ if (match) {
612
+ return {
613
+ type: "scoped",
614
+ isValid: true,
615
+ name: trimmedName,
616
+ org: match[1],
617
+ package: match[2]
618
+ };
619
+ }
620
+ }
138
621
  return {
139
- locationType: "workspace-app",
140
- appName: parts[1]
622
+ type: "unscoped",
623
+ isValid: true,
624
+ name: trimmedName
141
625
  };
142
626
  }
143
- if (parts[0] === "libraries" && parts.length >= 2) {
144
- return {
145
- locationType: "workspace-library",
146
- appName: parts[1]
147
- };
627
+ /**
628
+ * Install npm dependencies in a directory
629
+ */
630
+ async install(cwd) {
631
+ try {
632
+ await execa2("npm", ["install"], { cwd });
633
+ } catch (error) {
634
+ const message = error instanceof Error ? error.message : "Unknown error";
635
+ throw new Error(`Failed to install npm dependencies: ${message}`);
636
+ }
148
637
  }
149
- if (parts[0] === "plugins" && parts.length >= 2) {
150
- return {
151
- locationType: "workspace-plugin",
152
- appName: parts[1]
153
- };
638
+ /**
639
+ * Install npm dependencies with legacy peer deps flag
640
+ */
641
+ async installWithLegacyPeerDeps(cwd) {
642
+ try {
643
+ await execa2("npm", ["install", "--legacy-peer-deps"], { cwd });
644
+ } catch (error) {
645
+ const message = error instanceof Error ? error.message : "Unknown error";
646
+ throw new Error(`Failed to install npm dependencies: ${message}`);
647
+ }
154
648
  }
155
- if (parts[0] === "app-templates" && parts.length >= 2) {
156
- return {
157
- locationType: "workspace-app-template",
158
- appName: parts[1]
159
- };
649
+ /**
650
+ * Download an npm package to a temporary directory
651
+ */
652
+ async downloadPackage(options) {
653
+ const { packageName } = options;
654
+ const tempDir = await fs5.mkdtemp(path7.join(os.tmpdir(), "npm-download-"));
655
+ try {
656
+ const { stdout } = await execa2("npm", ["pack", packageName, "--json"], {
657
+ cwd: tempDir
658
+ });
659
+ const packResult = JSON.parse(stdout);
660
+ const tarballName = Array.isArray(packResult) ? packResult[0].filename : packResult.filename;
661
+ if (!tarballName) {
662
+ throw new Error("Failed to get tarball name from npm pack");
663
+ }
664
+ const tarballPath = path7.join(tempDir, tarballName);
665
+ await execa2("tar", ["-xzf", tarballPath], { cwd: tempDir });
666
+ const packagePath = path7.join(tempDir, "package");
667
+ const packageJsonPath = path7.join(packagePath, "package.json");
668
+ const packageJson = await fs5.readJSON(packageJsonPath);
669
+ return {
670
+ path: packagePath,
671
+ version: packageJson.version,
672
+ name: packageJson.name
673
+ };
674
+ } catch (error) {
675
+ await fs5.remove(tempDir).catch(() => {
676
+ });
677
+ const message = error instanceof Error ? error.message : "Unknown error";
678
+ throw new Error(`Failed to download npm package '${packageName}': ${message}`);
679
+ }
160
680
  }
161
- return { locationType: "workspace-root" };
162
- }
681
+ /**
682
+ * Get information about an npm package
683
+ */
684
+ async getPackageInfo(packageName) {
685
+ try {
686
+ const { stdout } = await execa2("npm", ["view", packageName, "--json"]);
687
+ return JSON.parse(stdout);
688
+ } catch (error) {
689
+ const message = error instanceof Error ? error.message : "Unknown error";
690
+ throw new Error(`Failed to get package info for '${packageName}': ${message}`);
691
+ }
692
+ }
693
+ /**
694
+ * Check if a package exists on npm
695
+ */
696
+ async packageExists(packageName) {
697
+ try {
698
+ await execa2("npm", ["view", packageName, "name"]);
699
+ return true;
700
+ } catch {
701
+ return false;
702
+ }
703
+ }
704
+ /**
705
+ * Install a specific package as a dependency
706
+ */
707
+ async installPackage(packageName, cwd, isDev = false) {
708
+ try {
709
+ const args = ["install", packageName];
710
+ if (isDev) {
711
+ args.push("--save-dev");
712
+ }
713
+ await execa2("npm", args, { cwd });
714
+ } catch (error) {
715
+ const message = error instanceof Error ? error.message : "Unknown error";
716
+ throw new Error(`Failed to install package '${packageName}': ${message}`);
717
+ }
718
+ }
719
+ /**
720
+ * Uninstall a package
721
+ */
722
+ async uninstallPackage(packageName, cwd) {
723
+ try {
724
+ await execa2("npm", ["uninstall", packageName], { cwd });
725
+ } catch (error) {
726
+ const message = error instanceof Error ? error.message : "Unknown error";
727
+ throw new Error(`Failed to uninstall package '${packageName}': ${message}`);
728
+ }
729
+ }
730
+ /**
731
+ * Clean up a temporary download directory
732
+ */
733
+ async cleanupDownload(downloadPath) {
734
+ const tempDir = path7.dirname(downloadPath);
735
+ if (tempDir.includes("npm-download-")) {
736
+ await fs5.remove(tempDir).catch(() => {
737
+ });
738
+ }
739
+ }
740
+ };
163
741
 
164
- // src/context/manifest.ts
165
- import * as path5 from "path";
166
- import fs4 from "fs-extra";
167
- var WORKSPACE_MANIFEST = ".launch77/workspace.json";
168
- async function isWorkspaceRoot(dir) {
169
- const manifestPath = path5.join(dir, WORKSPACE_MANIFEST);
170
- return await fs4.pathExists(manifestPath);
171
- }
172
- async function readWorkspaceManifest(workspaceRoot) {
173
- const manifestPath = path5.join(workspaceRoot, WORKSPACE_MANIFEST);
174
- return await fs4.readJSON(manifestPath);
742
+ // src/modules/npm/utils/npm-package.ts
743
+ import * as path8 from "path";
744
+ import { execa as execa3 } from "execa";
745
+
746
+ // src/modules/plugin/errors/plugin-errors.ts
747
+ var PluginNotFoundError = class extends Error {
748
+ constructor(pluginName) {
749
+ super(`Plugin '${pluginName}' not found.`);
750
+ this.name = "PluginNotFoundError";
751
+ }
752
+ };
753
+ var InvalidPluginContextError = class extends Error {
754
+ constructor(message) {
755
+ super(message);
756
+ this.name = "InvalidPluginContextError";
757
+ }
758
+ };
759
+ function createInvalidContextError(currentLocation) {
760
+ return new InvalidPluginContextError(
761
+ `plugin:install must be run from within a package directory.
762
+
763
+ Current location: ${currentLocation}
764
+ Expected: apps/<name>/, libraries/<name>/, plugins/<name>/, or app-templates/<name>/
765
+
766
+ Navigate to a package directory:
767
+ cd apps/<app-name>/
768
+ cd libraries/<lib-name>/
769
+ cd plugins/<plugin-name>/
770
+ cd app-templates/<template-name>/`
771
+ );
175
772
  }
176
- async function findWorkspaceRoot(startDir) {
177
- let currentDir = path5.resolve(startDir);
178
- const rootDir = path5.parse(currentDir).root;
179
- while (currentDir !== rootDir) {
180
- if (await isWorkspaceRoot(currentDir)) {
181
- return currentDir;
182
- }
183
- currentDir = path5.dirname(currentDir);
773
+ var MissingPluginTargetsError = class extends Error {
774
+ constructor(pluginName) {
775
+ super(`Plugin '${pluginName}' is missing the required 'targets' field in plugin.json.
776
+
777
+ The plugin.json file must include a 'targets' array specifying which package types
778
+ the plugin can be installed into.
779
+
780
+ Example plugin.json:
781
+ {
782
+ "name": "${pluginName}",
783
+ "version": "1.0.0",
784
+ "targets": ["app", "library", "plugin", "app-template"],
785
+ "pluginDependencies": {},
786
+ "libraryDependencies": {}
787
+ }`);
788
+ this.name = "MissingPluginTargetsError";
184
789
  }
185
- return null;
790
+ };
791
+ function createInvalidTargetError(pluginName, currentTarget, allowedTargets) {
792
+ const targetLocations = allowedTargets.map((target) => {
793
+ switch (target) {
794
+ case "app":
795
+ return "apps/<name>/";
796
+ case "library":
797
+ return "libraries/<name>/";
798
+ case "plugin":
799
+ return "plugins/<name>/";
800
+ case "app-template":
801
+ return "app-templates/<name>/";
802
+ default:
803
+ return target;
804
+ }
805
+ });
806
+ return new InvalidPluginContextError(
807
+ `Plugin '${pluginName}' cannot be installed in a '${currentTarget}' package.
808
+
809
+ This plugin can only be installed in: ${allowedTargets.join(", ")}
810
+
811
+ Allowed locations:
812
+ ${targetLocations.map((loc) => ` ${loc}`).join("\n")}`
813
+ );
186
814
  }
815
+ var PluginInstallationError = class extends Error {
816
+ constructor(message, cause) {
817
+ super(message);
818
+ this.cause = cause;
819
+ this.name = "PluginInstallationError";
820
+ }
821
+ };
822
+ var PluginResolutionError = class extends Error {
823
+ constructor(pluginName, reason) {
824
+ super(`Failed to resolve plugin '${pluginName}': ${reason}`);
825
+ this.name = "PluginResolutionError";
826
+ }
827
+ };
828
+ var NpmInstallationError = class extends Error {
829
+ constructor(packageName, cause) {
830
+ super(`Failed to install npm package '${packageName}'.
187
831
 
188
- // src/context/index.ts
189
- async function detectLaunch77Context(cwd) {
190
- const resolvedCwd = path6.resolve(cwd);
191
- const workspaceRoot = await findWorkspaceRoot(resolvedCwd);
192
- if (!workspaceRoot) {
193
- return {
194
- isValid: false,
195
- locationType: "unknown",
196
- workspaceRoot: "",
197
- appsDir: "",
198
- workspaceVersion: "",
199
- workspaceName: "",
200
- packageName: ""
201
- };
832
+ Please check:
833
+ - Your internet connection
834
+ - npm registry access (https://registry.npmjs.org)
835
+ - Package exists: https://www.npmjs.com/package/${packageName}
836
+
837
+ ${cause ? `
838
+ Original error: ${cause.message}` : ""}`);
839
+ this.cause = cause;
840
+ this.name = "NpmInstallationError";
841
+ }
842
+ };
843
+ var PluginDirectoryNotFoundError = class extends Error {
844
+ constructor(pluginName, expectedPath) {
845
+ super(`Plugin '${pluginName}' does not exist.
846
+
847
+ Expected location: ${expectedPath}
848
+
849
+ Available plugins:
850
+ cd plugins/
851
+ ls`);
852
+ this.name = "PluginDirectoryNotFoundError";
853
+ }
854
+ };
855
+ var InvalidPluginNameError = class extends Error {
856
+ constructor(message) {
857
+ super(message);
858
+ this.name = "InvalidPluginNameError";
859
+ }
860
+ };
861
+
862
+ // src/modules/npm/utils/npm-package.ts
863
+ async function downloadNpmPackage(options) {
864
+ const { packageName, workspaceRoot, logger } = options;
865
+ if (logger) {
866
+ logger(`Installing from npm: ${packageName}...`);
202
867
  }
203
- let manifest;
204
868
  try {
205
- manifest = await readWorkspaceManifest(workspaceRoot);
206
- } catch (error) {
869
+ await execa3("npm", ["install", packageName, "--save-dev"], {
870
+ cwd: workspaceRoot,
871
+ stdio: "pipe"
872
+ // Capture output for clean logging
873
+ });
874
+ const packagePath = path8.join(workspaceRoot, "node_modules", packageName);
207
875
  return {
208
- isValid: false,
209
- locationType: "unknown",
210
- workspaceRoot: "",
211
- appsDir: "",
212
- workspaceVersion: "",
213
- workspaceName: "",
214
- packageName: ""
876
+ packagePath,
877
+ packageName
215
878
  };
879
+ } catch (error) {
880
+ throw new NpmInstallationError(packageName, error instanceof Error ? error : void 0);
216
881
  }
217
- const parsed = parseLocationFromPath(resolvedCwd, workspaceRoot);
218
- const workspaceName = path6.basename(workspaceRoot);
219
- const appsDir = path6.join(workspaceRoot, "apps");
220
- const packageName = parsed.appName ? `@${workspaceName}/${parsed.appName}` : "";
221
- return {
222
- isValid: true,
223
- locationType: parsed.locationType,
224
- workspaceRoot,
225
- appsDir,
226
- workspaceVersion: manifest.version,
227
- workspaceName,
228
- appName: parsed.appName,
229
- packageName
230
- };
231
882
  }
232
883
 
233
- // src/utils/name-validation.ts
234
- function validatePluginName(name) {
235
- if (!name || name.trim().length === 0) {
236
- return {
237
- isValid: false,
238
- error: "Plugin name cannot be empty"
239
- };
884
+ // src/modules/plugin/resolvers/plugin-resolver.ts
885
+ import * as path10 from "path";
886
+ import fs7 from "fs-extra";
887
+
888
+ // src/modules/npm/resolvers/package-resolver.ts
889
+ import * as path9 from "path";
890
+ import fs6 from "fs-extra";
891
+ var PackageResolver = class {
892
+ constructor() {
893
+ this.npmService = new NpmService();
240
894
  }
241
- const trimmedName = name.trim();
242
- if (!/^[a-z]/.test(trimmedName)) {
243
- return {
244
- isValid: false,
245
- error: "Plugin name must start with a lowercase letter (a-z)"
246
- };
895
+ /**
896
+ * Validate package input name
897
+ *
898
+ * Accepts:
899
+ * - Unscoped names (e.g., "release", "my-package")
900
+ * - Scoped npm packages (e.g., "@ibm/package-name")
901
+ *
902
+ * Rejects:
903
+ * - Invalid formats
904
+ * - Empty strings
905
+ * - Names with invalid characters
906
+ *
907
+ * @param name - The package name to validate
908
+ * @returns ValidationResult with isValid and optional error message
909
+ *
910
+ * @example
911
+ * validateInput('release') // { isValid: true }
912
+ * validateInput('@ibm/analytics') // { isValid: true }
913
+ * validateInput('@invalid') // { isValid: false, error: '...' }
914
+ */
915
+ validateInput(name) {
916
+ if (!name || name.trim().length === 0) {
917
+ return {
918
+ isValid: false,
919
+ error: "Package name cannot be empty"
920
+ };
921
+ }
922
+ const trimmedName = name.trim();
923
+ const parsed = this.npmService.parsePackageName(trimmedName);
924
+ if (!parsed.isValid) {
925
+ return {
926
+ isValid: false,
927
+ error: parsed.error
928
+ };
929
+ }
930
+ if (parsed.type === "scoped") {
931
+ return this.npmService.validatePackageName(trimmedName);
932
+ }
933
+ return { isValid: true };
934
+ }
935
+ /**
936
+ * Convert an unscoped package name to an npm package name
937
+ *
938
+ * Rules:
939
+ * - Unscoped names: prefix with configured package prefix
940
+ * - Scoped names: use as-is
941
+ *
942
+ * @param name - The package name (must be validated first)
943
+ * @returns The npm package name
944
+ *
945
+ * @example
946
+ * toNpmPackageName('release') // '@launch77-shared/plugin-release' (for PluginResolver)
947
+ * toNpmPackageName('@ibm/analytics') // '@ibm/analytics'
948
+ */
949
+ toNpmPackageName(name) {
950
+ const trimmedName = name.trim();
951
+ if (trimmedName.startsWith("@")) {
952
+ return trimmedName;
953
+ }
954
+ return `${this.getPackagePrefix()}${trimmedName}`;
955
+ }
956
+ /**
957
+ * Read version from package.json
958
+ * @param packagePath - The path to the package directory
959
+ * @returns The version string from package.json
960
+ * @throws If package.json doesn't exist, can't be read, or is missing the version field
961
+ */
962
+ async readVersion(packagePath) {
963
+ const packageJsonPath = path9.join(packagePath, "package.json");
964
+ try {
965
+ const packageJson = await fs6.readJson(packageJsonPath);
966
+ if (!packageJson.version) {
967
+ 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.`);
968
+ }
969
+ return packageJson.version;
970
+ } catch (error) {
971
+ if (error instanceof Error && error.message.includes("Invalid package structure")) {
972
+ throw error;
973
+ }
974
+ 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.`);
975
+ }
247
976
  }
248
- if (!/^[a-z][a-z0-9-]*$/.test(trimmedName)) {
977
+ /**
978
+ * Resolve package location from name
979
+ *
980
+ * Resolution order:
981
+ * 1. Check local workspace directory (configured by getFolderName())
982
+ * 2. Verify local package is valid (using verify())
983
+ * 3. Fall back to npm package name (with configured prefix)
984
+ * 4. Read version from package.json (if available)
985
+ *
986
+ * @param name - The package name to resolve
987
+ * @param workspaceRoot - The workspace root directory
988
+ * @returns PackageResolution with source, resolved location, and version
989
+ *
990
+ * @example
991
+ * // Local package found
992
+ * await resolveLocation('my-package', '/workspace')
993
+ * // { source: 'local', resolvedName: 'my-package', localPath: '/workspace/plugins/my-package', version: '1.0.0' }
994
+ *
995
+ * // Not found locally, resolve to npm
996
+ * await resolveLocation('release', '/workspace')
997
+ * // { source: 'npm', resolvedName: 'release', npmPackage: '@launch77-shared/plugin-release' }
998
+ *
999
+ * // Scoped package always resolves to npm
1000
+ * await resolveLocation('@ibm/analytics', '/workspace')
1001
+ * // { source: 'npm', resolvedName: '@ibm/analytics', npmPackage: '@ibm/analytics' }
1002
+ */
1003
+ async resolveLocation(name, workspaceRoot) {
1004
+ const trimmedName = name.trim();
1005
+ const parsed = this.npmService.parsePackageName(trimmedName);
1006
+ if (parsed.type === "scoped") {
1007
+ return {
1008
+ source: "npm",
1009
+ resolvedName: trimmedName,
1010
+ npmPackage: trimmedName
1011
+ };
1012
+ }
1013
+ const localPath = path9.join(workspaceRoot, this.getFolderName(), trimmedName);
1014
+ const localExists = await fs6.pathExists(localPath);
1015
+ if (localExists) {
1016
+ const isValid = await this.verify(localPath);
1017
+ if (isValid) {
1018
+ const version = await this.readVersion(localPath);
1019
+ return {
1020
+ source: "local",
1021
+ resolvedName: trimmedName,
1022
+ localPath,
1023
+ version
1024
+ };
1025
+ }
1026
+ }
1027
+ const npmPackage = this.toNpmPackageName(trimmedName);
249
1028
  return {
250
- isValid: false,
251
- error: "Plugin name can only contain lowercase letters (a-z), numbers (0-9), and hyphens (-)"
1029
+ source: "npm",
1030
+ resolvedName: trimmedName,
1031
+ npmPackage
252
1032
  };
253
1033
  }
254
- return { isValid: true };
1034
+ };
1035
+
1036
+ // src/modules/plugin/resolvers/plugin-resolver.ts
1037
+ var PluginResolver = class extends PackageResolver {
1038
+ getFolderName() {
1039
+ return "plugins";
1040
+ }
1041
+ getPackagePrefix() {
1042
+ return "@launch77-shared/plugin-";
1043
+ }
1044
+ async verify(localPath) {
1045
+ const hasPluginJson = await fs7.pathExists(path10.join(localPath, "plugin.json"));
1046
+ const hasGenerator = await fs7.pathExists(path10.join(localPath, "dist/generator.js"));
1047
+ const hasPackageJson = await fs7.pathExists(path10.join(localPath, "package.json"));
1048
+ if (!hasPluginJson || !hasGenerator || !hasPackageJson) {
1049
+ return false;
1050
+ }
1051
+ try {
1052
+ const packageJson = await fs7.readJson(path10.join(localPath, "package.json"));
1053
+ return !!packageJson.version;
1054
+ } catch {
1055
+ return false;
1056
+ }
1057
+ }
1058
+ };
1059
+
1060
+ // src/modules/plugin/utils/plugin-utils.ts
1061
+ function mapLocationTypeToTarget(locationType) {
1062
+ switch (locationType) {
1063
+ case "workspace-app":
1064
+ return "app";
1065
+ case "workspace-library":
1066
+ return "library";
1067
+ case "workspace-plugin":
1068
+ return "plugin";
1069
+ case "workspace-app-template":
1070
+ return "app-template";
1071
+ default:
1072
+ throw createInvalidContextError(locationType);
1073
+ }
255
1074
  }
256
- function isValidNpmPackageName(name) {
257
- if (!name || name.trim().length === 0) {
1075
+
1076
+ // src/modules/plugin/services/plugin-service.ts
1077
+ var PluginService = class {
1078
+ constructor() {
1079
+ this.pluginResolver = new PluginResolver();
1080
+ this.metadataService = new MetadataService();
1081
+ this.npmService = new NpmService();
1082
+ }
1083
+ /**
1084
+ * Validate a plugin name
1085
+ */
1086
+ validatePluginName(name) {
1087
+ const trimmed = name.trim();
1088
+ if (!trimmed) {
1089
+ return { isValid: false, errors: ["Plugin name cannot be empty"] };
1090
+ }
1091
+ if (trimmed.startsWith("@")) {
1092
+ const parts = trimmed.split("/");
1093
+ if (parts.length !== 2 || !parts[0].slice(1) || !parts[1]) {
1094
+ return { isValid: false, errors: ["Scoped package must be in format @org/package"] };
1095
+ }
1096
+ const scope = parts[0].slice(1);
1097
+ const packageName = parts[1];
1098
+ if (!this.isValidPackageNamePart(scope)) {
1099
+ return { isValid: false, errors: [`Invalid scope: ${scope}`] };
1100
+ }
1101
+ if (!this.isValidPackageNamePart(packageName)) {
1102
+ return { isValid: false, errors: [`Invalid package name: ${packageName}`] };
1103
+ }
1104
+ } else {
1105
+ if (!this.isValidPackageNamePart(trimmed)) {
1106
+ return { isValid: false, errors: [`Invalid package name: ${trimmed}`] };
1107
+ }
1108
+ }
1109
+ return { isValid: true };
1110
+ }
1111
+ /**
1112
+ * Validate plugin metadata
1113
+ */
1114
+ validatePluginMetadata(metadata) {
1115
+ const errors = [];
1116
+ if (!metadata.targets || metadata.targets.length === 0) {
1117
+ errors.push("Plugin must specify at least one target");
1118
+ }
1119
+ if (metadata.showcaseUrl) {
1120
+ const showcaseValidation = this.validateShowcaseUrl(metadata.showcaseUrl);
1121
+ if (!showcaseValidation.isValid) {
1122
+ errors.push(showcaseValidation.errors?.[0] || "Invalid showcase URL");
1123
+ }
1124
+ }
258
1125
  return {
259
- isValid: false,
260
- error: "Package name cannot be empty"
1126
+ isValid: errors.length === 0,
1127
+ errors: errors.length > 0 ? errors : void 0
261
1128
  };
262
1129
  }
263
- const trimmedName = name.trim();
264
- if (trimmedName.startsWith("@")) {
265
- const scopedPattern = /^@([a-z0-9-]+)\/([a-z0-9-]+)$/;
266
- if (!scopedPattern.test(trimmedName)) {
1130
+ /**
1131
+ * Validate a showcase URL
1132
+ * Note: This is primarily used by the webapp template
1133
+ */
1134
+ validateShowcaseUrl(url) {
1135
+ if (!url) {
1136
+ return { isValid: true };
1137
+ }
1138
+ if (!url.startsWith("/")) {
267
1139
  return {
268
1140
  isValid: false,
269
- error: "Scoped package must be in format @org/package where org and package contain only lowercase letters, numbers, and hyphens"
1141
+ errors: ["Showcase URL must be a relative path starting with /"]
1142
+ };
1143
+ }
1144
+ if (url.includes("://") || url.startsWith("//")) {
1145
+ return {
1146
+ isValid: false,
1147
+ errors: ["Showcase URL cannot be an external URL"]
1148
+ };
1149
+ }
1150
+ try {
1151
+ new URL(url, "http://example.com");
1152
+ return { isValid: true };
1153
+ } catch {
1154
+ return {
1155
+ isValid: false,
1156
+ errors: ["Invalid URL format"]
270
1157
  };
271
1158
  }
272
- return { isValid: true };
273
1159
  }
274
- if (!/^[a-z][a-z0-9-]*$/.test(trimmedName)) {
1160
+ /**
1161
+ * Validate plugin consistency between plugin.json and package.json
1162
+ */
1163
+ async validatePluginConsistency(pluginPath) {
1164
+ const errors = [];
1165
+ try {
1166
+ const packageJsonPath = path11.join(pluginPath, "package.json");
1167
+ const pluginJsonPath = path11.join(pluginPath, "plugin.json");
1168
+ if (!await fsExtra.pathExists(packageJsonPath)) {
1169
+ errors.push("package.json not found");
1170
+ return { isValid: false, errors };
1171
+ }
1172
+ if (!await fsExtra.pathExists(pluginJsonPath)) {
1173
+ errors.push("plugin.json not found");
1174
+ return { isValid: false, errors };
1175
+ }
1176
+ const packageJson = await fsExtra.readJSON(packageJsonPath);
1177
+ const pluginJson = await fsExtra.readJSON(pluginJsonPath);
1178
+ if (pluginJson.dependencies) {
1179
+ const packageDeps = packageJson.dependencies || {};
1180
+ const packageDevDeps = packageJson.devDependencies || {};
1181
+ for (const [dep, version] of Object.entries(pluginJson.dependencies)) {
1182
+ if (!packageDeps[dep] && !packageDevDeps[dep]) {
1183
+ errors.push(`Dependency '${dep}' specified in plugin.json but not in package.json`);
1184
+ } else {
1185
+ const packageVersion = packageDeps[dep] || packageDevDeps[dep];
1186
+ if (packageVersion !== version) {
1187
+ errors.push(`Dependency '${dep}' version mismatch: plugin.json has '${version}', package.json has '${packageVersion}'`);
1188
+ }
1189
+ }
1190
+ }
1191
+ }
1192
+ if (pluginJson.devDependencies) {
1193
+ const packageDevDeps = packageJson.devDependencies || {};
1194
+ for (const [dep, version] of Object.entries(pluginJson.devDependencies)) {
1195
+ if (!packageDevDeps[dep]) {
1196
+ errors.push(`Dev dependency '${dep}' specified in plugin.json but not in package.json`);
1197
+ } else if (packageDevDeps[dep] !== version) {
1198
+ errors.push(`Dev dependency '${dep}' version mismatch: plugin.json has '${version}', package.json has '${packageDevDeps[dep]}'`);
1199
+ }
1200
+ }
1201
+ }
1202
+ } catch (error) {
1203
+ errors.push(`Failed to validate plugin consistency: ${error instanceof Error ? error.message : "Unknown error"}`);
1204
+ }
275
1205
  return {
276
- isValid: false,
277
- error: "Package name must start with a lowercase letter and contain only lowercase letters (a-z), numbers (0-9), and hyphens (-)"
1206
+ isValid: errors.length === 0,
1207
+ errors: errors.length > 0 ? errors : void 0
278
1208
  };
279
1209
  }
280
- return { isValid: true };
281
- }
282
- function parsePluginName(name) {
283
- if (!name || name.trim().length === 0) {
1210
+ /**
1211
+ * Check if a package name part is valid
1212
+ */
1213
+ isValidPackageNamePart(part) {
1214
+ const validPattern = /^[a-z][a-z0-9._-]*$/;
1215
+ return validPattern.test(part);
1216
+ }
1217
+ /**
1218
+ * Validate plugin name, resolve its location, and download if needed
1219
+ */
1220
+ async validateAndResolvePlugin(pluginName, workspaceRoot, logger) {
1221
+ logger(chalk2.blue(`
1222
+ \u{1F50D} Resolving plugin "${pluginName}"...`));
1223
+ logger(` \u251C\u2500 Validating plugin name...`);
1224
+ const validation = this.pluginResolver.validateInput(pluginName);
1225
+ if (!validation.isValid) {
1226
+ throw new PluginResolutionError(pluginName, validation.error || "Invalid plugin name");
1227
+ }
1228
+ logger(` \u2502 \u2514\u2500 ${chalk2.green("\u2713")} Valid plugin name`);
1229
+ logger(` \u251C\u2500 Checking local workspace: ${chalk2.dim(`plugins/${pluginName}`)}`);
1230
+ const resolution = await this.pluginResolver.resolveLocation(pluginName, workspaceRoot);
1231
+ let pluginPath;
1232
+ let version;
1233
+ if (resolution.source === "local") {
1234
+ logger(` \u2502 \u2514\u2500 ${chalk2.green("\u2713")} Found local plugin`);
1235
+ pluginPath = resolution.localPath;
1236
+ version = resolution.version;
1237
+ } else {
1238
+ logger(` \u2502 \u2514\u2500 ${chalk2.dim("Not found locally")}`);
1239
+ logger(` \u251C\u2500 Resolving to npm package: ${chalk2.cyan(resolution.npmPackage)}`);
1240
+ pluginPath = await this.downloadNpmPlugin(resolution.npmPackage, workspaceRoot, logger);
1241
+ const packageJsonPath = path11.join(pluginPath, "package.json");
1242
+ const packageJsonContent = await fs8.readFile(packageJsonPath, "utf-8");
1243
+ const packageJson = JSON.parse(packageJsonContent);
1244
+ version = packageJson.version;
1245
+ }
1246
+ logger(` \u2514\u2500 ${chalk2.green("\u2713")} Plugin resolved
1247
+ `);
284
1248
  return {
285
- type: "invalid",
286
- isValid: false,
287
- error: "Plugin name cannot be empty"
1249
+ pluginPath,
1250
+ source: resolution.source,
1251
+ npmPackage: resolution.npmPackage,
1252
+ version
288
1253
  };
289
1254
  }
290
- const trimmedName = name.trim();
291
- if (trimmedName.startsWith("@")) {
292
- const validation2 = isValidNpmPackageName(trimmedName);
293
- if (!validation2.isValid) {
294
- return {
295
- type: "invalid",
296
- isValid: false,
297
- error: validation2.error
298
- };
1255
+ /**
1256
+ * Read plugin metadata and validate it supports the current target
1257
+ */
1258
+ async validatePluginTargets(pluginPath, pluginName, currentTarget) {
1259
+ const metadata = await this.metadataService.readPluginMetadata(pluginPath);
1260
+ if (!metadata.targets || metadata.targets.length === 0) {
1261
+ throw new MissingPluginTargetsError(pluginName);
299
1262
  }
300
- const match = trimmedName.match(/^@([a-z0-9-]+)\/([a-z0-9-]+)$/);
301
- if (match) {
1263
+ if (!metadata.targets.includes(currentTarget)) {
1264
+ throw createInvalidTargetError(pluginName, currentTarget, metadata.targets);
1265
+ }
1266
+ return metadata;
1267
+ }
1268
+ /**
1269
+ * Check if plugin is already installed and return early-exit result if so
1270
+ */
1271
+ async checkExistingInstallation(pluginName, packagePath, logger) {
1272
+ const existingInstallation = await this.isPluginInstalled(pluginName, packagePath);
1273
+ if (existingInstallation) {
1274
+ logger(chalk2.yellow(`
1275
+ \u2139\uFE0F Plugin '${pluginName}' is already installed in this package.
1276
+ `));
1277
+ logger(`Package: ${chalk2.cyan(existingInstallation.package)} (${existingInstallation.source})`);
1278
+ logger(`Version: ${existingInstallation.version}`);
1279
+ logger(`Installed: ${existingInstallation.installedAt}
1280
+ `);
1281
+ logger(chalk2.gray("To reinstall: Remove from package.json launch77.installedPlugins"));
1282
+ logger(chalk2.gray("(plugin:remove command coming soon)\n"));
302
1283
  return {
303
- type: "scoped",
304
- isValid: true,
305
- name: trimmedName,
306
- org: match[1],
307
- package: match[2]
1284
+ pluginName,
1285
+ filesInstalled: false,
1286
+ packageJsonUpdated: false,
1287
+ dependenciesInstalled: false
308
1288
  };
309
1289
  }
1290
+ return null;
1291
+ }
1292
+ /**
1293
+ * Install a plugin to the current package
1294
+ */
1295
+ async installPlugin(request, context, logger = console.log) {
1296
+ const { pluginName } = request;
1297
+ const currentTarget = mapLocationTypeToTarget(context.locationType);
1298
+ const { pluginPath, source, npmPackage, version } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger);
1299
+ await this.validatePluginTargets(pluginPath, pluginName, currentTarget);
1300
+ const packagePath = this.getPackagePath(context);
1301
+ const earlyExit = await this.checkExistingInstallation(pluginName, packagePath, logger);
1302
+ if (earlyExit) return earlyExit;
1303
+ await this.runGenerator(pluginPath, packagePath, context);
1304
+ const packageName = source === "npm" ? npmPackage : pluginName;
1305
+ await this.writePluginManifest(packagePath, {
1306
+ pluginName,
1307
+ packageName,
1308
+ version,
1309
+ source
1310
+ });
310
1311
  return {
311
- type: "invalid",
312
- isValid: false,
313
- error: "Invalid scoped package format"
1312
+ pluginName,
1313
+ filesInstalled: true,
1314
+ packageJsonUpdated: true,
1315
+ dependenciesInstalled: true
314
1316
  };
315
1317
  }
316
- const validation = validatePluginName(trimmedName);
317
- if (!validation.isValid) {
1318
+ /**
1319
+ * Download and install an npm plugin package
1320
+ */
1321
+ async downloadNpmPlugin(npmPackage, workspaceRoot, logger) {
1322
+ logger(` \u2514\u2500 Installing from npm: ${chalk2.cyan(npmPackage)}...`);
1323
+ const result = await downloadNpmPackage({
1324
+ packageName: npmPackage,
1325
+ workspaceRoot
1326
+ });
1327
+ return result.packagePath;
1328
+ }
1329
+ getPackagePath(context) {
1330
+ switch (context.locationType) {
1331
+ case "workspace-app":
1332
+ return path11.join(context.appsDir, context.appName);
1333
+ case "workspace-library":
1334
+ return path11.join(context.workspaceRoot, "libraries", context.appName);
1335
+ case "workspace-plugin":
1336
+ return path11.join(context.workspaceRoot, "plugins", context.appName);
1337
+ case "workspace-app-template":
1338
+ return path11.join(context.workspaceRoot, "app-templates", context.appName);
1339
+ default:
1340
+ throw new InvalidPluginContextError(`Cannot install plugin from ${context.locationType}`);
1341
+ }
1342
+ }
1343
+ async runGenerator(pluginPath, appPath, context) {
1344
+ try {
1345
+ const generatorPath = path11.join(pluginPath, "dist/generator.js");
1346
+ const args = [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`];
1347
+ await execa4("node", args, {
1348
+ cwd: pluginPath,
1349
+ stdio: "inherit"
1350
+ });
1351
+ } catch (error) {
1352
+ throw new PluginInstallationError(`Generator failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : void 0);
1353
+ }
1354
+ }
1355
+ async isPluginInstalled(pluginName, packagePath) {
1356
+ try {
1357
+ const packageJsonPath = path11.join(packagePath, "package.json");
1358
+ const packageJsonContent = await fs8.readFile(packageJsonPath, "utf-8");
1359
+ const packageJson = JSON.parse(packageJsonContent);
1360
+ const manifest = packageJson.launch77;
1361
+ if (manifest?.installedPlugins?.[pluginName]) {
1362
+ return manifest.installedPlugins[pluginName];
1363
+ }
1364
+ return null;
1365
+ } catch (error) {
1366
+ return null;
1367
+ }
1368
+ }
1369
+ /**
1370
+ * Write plugin installation metadata to the target package's package.json
1371
+ */
1372
+ async writePluginManifest(packagePath, installationInfo) {
1373
+ try {
1374
+ const packageJsonPath = path11.join(packagePath, "package.json");
1375
+ const packageJsonContent = await fs8.readFile(packageJsonPath, "utf-8");
1376
+ const packageJson = JSON.parse(packageJsonContent);
1377
+ if (!packageJson.launch77) {
1378
+ packageJson.launch77 = {};
1379
+ }
1380
+ if (!packageJson.launch77.installedPlugins) {
1381
+ packageJson.launch77.installedPlugins = {};
1382
+ }
1383
+ const manifest = packageJson.launch77;
1384
+ manifest.installedPlugins[installationInfo.pluginName] = {
1385
+ package: installationInfo.packageName,
1386
+ version: installationInfo.version,
1387
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1388
+ source: installationInfo.source
1389
+ };
1390
+ await fs8.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
1391
+ } catch (error) {
1392
+ throw new PluginInstallationError(`Failed to write plugin manifest: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : void 0);
1393
+ }
1394
+ }
1395
+ /**
1396
+ * Delete a plugin from the workspace
1397
+ */
1398
+ async deletePlugin(request, context) {
1399
+ const { pluginName } = request;
1400
+ const nameValidation = this.validatePluginName(pluginName);
1401
+ if (!nameValidation.isValid) {
1402
+ throw new InvalidPluginNameError(nameValidation.errors?.[0] || "Invalid plugin name");
1403
+ }
1404
+ if (context.locationType === "unknown") {
1405
+ throw new Error("Must be run from within a Launch77 workspace. Create a workspace first: launch77 init my-workspace");
1406
+ }
1407
+ const pluginPath = path11.join(context.workspaceRoot, "plugins", pluginName);
1408
+ if (!await fsExtra.pathExists(pluginPath)) {
1409
+ throw new PluginDirectoryNotFoundError(pluginName, pluginPath);
1410
+ }
1411
+ await fsExtra.remove(pluginPath);
1412
+ await this.npmService.install(context.workspaceRoot);
318
1413
  return {
319
- type: "invalid",
320
- isValid: false,
321
- error: validation.error
1414
+ pluginName
322
1415
  };
323
1416
  }
324
- return {
325
- type: "unscoped",
326
- isValid: true,
327
- name: trimmedName
328
- };
329
- }
1417
+ };
330
1418
 
331
- // src/utils/validate-plugin-consistency.ts
332
- import * as fs5 from "fs/promises";
333
- import * as path7 from "path";
334
- import chalk2 from "chalk";
335
- async function validatePluginConsistency(pluginPath) {
336
- const packageJsonPath = path7.join(pluginPath, "package.json");
337
- const packageJsonContent = await fs5.readFile(packageJsonPath, "utf-8");
338
- const packageJson = JSON.parse(packageJsonContent);
339
- const pluginJsonPath = path7.join(pluginPath, "plugin.json");
340
- const pluginJsonContent = await fs5.readFile(pluginJsonPath, "utf-8");
341
- const pluginJson = JSON.parse(pluginJsonContent);
342
- const errors = [];
343
- if (pluginJson.libraryDependencies) {
344
- for (const [library, pluginJsonVersion] of Object.entries(pluginJson.libraryDependencies)) {
345
- const packageJsonVersion = packageJson.dependencies?.[library];
346
- if (!packageJsonVersion) {
347
- errors.push({
348
- library,
349
- packageJsonVersion: "MISSING",
350
- pluginJsonVersion
351
- });
352
- } else if (packageJsonVersion !== pluginJsonVersion) {
353
- errors.push({
354
- library,
355
- packageJsonVersion,
356
- pluginJsonVersion
357
- });
358
- }
359
- }
360
- }
361
- return {
362
- valid: errors.length === 0,
363
- errors
364
- };
365
- }
366
- async function validatePluginConsistencyOrThrow(pluginPath) {
367
- const result = await validatePluginConsistency(pluginPath);
368
- if (!result.valid) {
369
- const packageJsonPath = path7.join(pluginPath, "package.json");
370
- const packageJsonContent = await fs5.readFile(packageJsonPath, "utf-8");
371
- const packageJson = JSON.parse(packageJsonContent);
372
- const pluginName = packageJson.name || "unknown";
373
- console.error(chalk2.red("\n\u274C Plugin consistency validation failed!\n"));
374
- console.error(chalk2.yellow(`Plugin: ${pluginName}
375
- `));
376
- console.error(chalk2.white("Library versions in package.json must match plugin.json:\n"));
377
- for (const error of result.errors) {
378
- console.error(chalk2.white(` ${error.library}:`));
379
- if (error.packageJsonVersion === "MISSING") {
380
- console.error(chalk2.red(` \u2717 package.json: MISSING (library not in dependencies)`));
381
- console.error(chalk2.red(` \u2717 plugin.json: ${error.pluginJsonVersion}`));
382
- } else {
383
- console.error(chalk2.red(` \u2717 package.json: ${error.packageJsonVersion}`));
384
- console.error(chalk2.red(` \u2717 plugin.json: ${error.pluginJsonVersion}`));
385
- }
386
- console.error();
387
- }
388
- console.error(chalk2.white("Why this matters:"));
389
- console.error(chalk2.white(" - Plugin templates use package.json versions during development"));
390
- console.error(chalk2.white(" - End users get plugin.json versions when they install the plugin"));
391
- console.error(chalk2.white(" - Mismatched versions cause template code to break for users\n"));
392
- console.error(chalk2.cyan("Fix: Update both files to use the same version.\n"));
393
- throw new Error("Plugin validation failed - library versions do not match");
1419
+ // src/modules/plugin/services/plugin-installer.ts
1420
+ import * as path12 from "path";
1421
+ import { execa as execa5 } from "execa";
1422
+ var PluginInstaller = class {
1423
+ constructor(filesystemService) {
1424
+ this.filesystemService = filesystemService || new FilesystemService();
394
1425
  }
395
- }
1426
+ /**
1427
+ * Install a plugin into the target location
1428
+ */
1429
+ async install(plugin, target, targetPath, context, logger) {
1430
+ await this.runGenerator(plugin, target, targetPath, context, logger);
1431
+ await this.trackInstallation(plugin, targetPath);
1432
+ }
1433
+ /**
1434
+ * Run a plugin's generator
1435
+ */
1436
+ async runGenerator(plugin, _target, targetPath, launch77Context, logger) {
1437
+ const generatorPath = path12.join(plugin.path, "dist", "generator.js");
1438
+ if (!await this.filesystemService.pathExists(generatorPath)) {
1439
+ throw new Error(`Plugin generator not found at: ${generatorPath}`);
1440
+ }
1441
+ logger?.("Running plugin generator...");
1442
+ try {
1443
+ const generatorModule = await import(generatorPath);
1444
+ const GeneratorClass = generatorModule.default || generatorModule.Generator;
1445
+ if (!GeneratorClass) {
1446
+ throw new Error("Plugin generator does not export a valid Generator class");
1447
+ }
1448
+ const context = {
1449
+ appPath: targetPath,
1450
+ appName: launch77Context.appName || "",
1451
+ workspaceName: launch77Context.workspaceName || "",
1452
+ pluginPath: plugin.path
1453
+ };
1454
+ const generator = new GeneratorClass(context);
1455
+ if (typeof generator.run !== "function") {
1456
+ throw new Error("Plugin generator does not have a run() method");
1457
+ }
1458
+ await generator.run();
1459
+ } catch (error) {
1460
+ throw new Error(`Failed to run plugin generator: ${error instanceof Error ? error.message : "Unknown error"}`);
1461
+ }
1462
+ }
1463
+ /**
1464
+ * Track plugin installation in package.json
1465
+ */
1466
+ async trackInstallation(plugin, targetPath) {
1467
+ const packageJsonPath = path12.join(targetPath, "package.json");
1468
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1469
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1470
+ }
1471
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1472
+ if (!packageJson.launch77) {
1473
+ packageJson.launch77 = {};
1474
+ }
1475
+ if (!packageJson.launch77.installedPlugins) {
1476
+ packageJson.launch77.installedPlugins = {};
1477
+ }
1478
+ const installationRecord = {
1479
+ package: plugin.name,
1480
+ version: plugin.version,
1481
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1482
+ source: plugin.source
1483
+ };
1484
+ const pluginKey = this.extractPluginKey(plugin.name);
1485
+ packageJson.launch77.installedPlugins[pluginKey] = installationRecord;
1486
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1487
+ }
1488
+ /**
1489
+ * Check if a plugin is already installed
1490
+ */
1491
+ async isInstalled(pluginName, targetPath) {
1492
+ const packageJsonPath = path12.join(targetPath, "package.json");
1493
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1494
+ return false;
1495
+ }
1496
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1497
+ const pluginKey = this.extractPluginKey(pluginName);
1498
+ return !!packageJson.launch77?.installedPlugins?.[pluginKey];
1499
+ }
1500
+ /**
1501
+ * Get installed plugin metadata
1502
+ */
1503
+ async getInstalledPlugin(pluginName, targetPath) {
1504
+ const packageJsonPath = path12.join(targetPath, "package.json");
1505
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1506
+ return null;
1507
+ }
1508
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1509
+ const pluginKey = this.extractPluginKey(pluginName);
1510
+ return packageJson.launch77?.installedPlugins?.[pluginKey] || null;
1511
+ }
1512
+ /**
1513
+ * Uninstall a plugin
1514
+ */
1515
+ async uninstall(pluginName, targetPath) {
1516
+ const packageJsonPath = path12.join(targetPath, "package.json");
1517
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1518
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1519
+ }
1520
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1521
+ const pluginKey = this.extractPluginKey(pluginName);
1522
+ if (!packageJson.launch77?.installedPlugins?.[pluginKey]) {
1523
+ throw new Error(`Plugin '${pluginName}' is not installed`);
1524
+ }
1525
+ delete packageJson.launch77.installedPlugins[pluginKey];
1526
+ if (Object.keys(packageJson.launch77.installedPlugins).length === 0) {
1527
+ delete packageJson.launch77.installedPlugins;
1528
+ }
1529
+ if (Object.keys(packageJson.launch77).length === 0) {
1530
+ delete packageJson.launch77;
1531
+ }
1532
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1533
+ }
1534
+ /**
1535
+ * Install npm dependencies
1536
+ */
1537
+ async installDependencies(targetPath) {
1538
+ try {
1539
+ await execa5("npm", ["install"], { cwd: targetPath });
1540
+ } catch (error) {
1541
+ try {
1542
+ await execa5("npm", ["install", "--legacy-peer-deps"], { cwd: targetPath });
1543
+ } catch (retryError) {
1544
+ throw new Error(`Failed to install dependencies: ${retryError instanceof Error ? retryError.message : "Unknown error"}`);
1545
+ }
1546
+ }
1547
+ }
1548
+ /**
1549
+ * Extract plugin key from package name
1550
+ */
1551
+ extractPluginKey(packageName) {
1552
+ let key = packageName;
1553
+ if (key.startsWith("@")) {
1554
+ const parts = key.split("/");
1555
+ key = parts[1] || key;
1556
+ }
1557
+ if (key.startsWith("plugin-")) {
1558
+ key = key.substring("plugin-".length);
1559
+ }
1560
+ return key;
1561
+ }
1562
+ };
1563
+
1564
+ // src/modules/package-manifest/services/package-manifest-service.ts
1565
+ import * as path13 from "path";
1566
+ var PackageManifestService = class {
1567
+ constructor(filesystemService) {
1568
+ this.filesystemService = filesystemService || new FilesystemService();
1569
+ }
1570
+ /**
1571
+ * Read the launch77 section (package manifest) from a package.json
1572
+ */
1573
+ async readPackageManifest(packagePath) {
1574
+ const packageJsonPath = path13.join(packagePath, "package.json");
1575
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1576
+ return null;
1577
+ }
1578
+ try {
1579
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1580
+ return packageJson.launch77 || null;
1581
+ } catch (error) {
1582
+ throw new Error(`Failed to read package manifest from ${packageJsonPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Write the launch77 section (package manifest) to a package.json
1587
+ */
1588
+ async writePackageManifest(packagePath, manifest) {
1589
+ const packageJsonPath = path13.join(packagePath, "package.json");
1590
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1591
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1592
+ }
1593
+ try {
1594
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1595
+ packageJson.launch77 = manifest;
1596
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1597
+ } catch (error) {
1598
+ throw new Error(`Failed to write package manifest to ${packageJsonPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
1599
+ }
1600
+ }
1601
+ /**
1602
+ * Add an installed plugin to the manifest
1603
+ */
1604
+ async addInstalledPlugin(packagePath, pluginKey, metadata) {
1605
+ const packageJsonPath = path13.join(packagePath, "package.json");
1606
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1607
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1608
+ }
1609
+ try {
1610
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1611
+ if (!packageJson.launch77) {
1612
+ packageJson.launch77 = {};
1613
+ }
1614
+ if (!packageJson.launch77.installedPlugins) {
1615
+ packageJson.launch77.installedPlugins = {};
1616
+ }
1617
+ packageJson.launch77.installedPlugins[pluginKey] = metadata;
1618
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1619
+ } catch (error) {
1620
+ throw new Error(`Failed to add plugin to package manifest: ${error instanceof Error ? error.message : "Unknown error"}`);
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Remove an installed plugin from the manifest
1625
+ */
1626
+ async removeInstalledPlugin(packagePath, pluginKey) {
1627
+ const packageJsonPath = path13.join(packagePath, "package.json");
1628
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1629
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1630
+ }
1631
+ try {
1632
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1633
+ if (!packageJson.launch77?.installedPlugins?.[pluginKey]) {
1634
+ throw new Error(`Plugin '${pluginKey}' is not installed`);
1635
+ }
1636
+ delete packageJson.launch77.installedPlugins[pluginKey];
1637
+ if (Object.keys(packageJson.launch77.installedPlugins).length === 0) {
1638
+ delete packageJson.launch77.installedPlugins;
1639
+ }
1640
+ if (Object.keys(packageJson.launch77).length === 0) {
1641
+ delete packageJson.launch77;
1642
+ }
1643
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1644
+ } catch (error) {
1645
+ throw new Error(`Failed to remove plugin from package manifest: ${error instanceof Error ? error.message : "Unknown error"}`);
1646
+ }
1647
+ }
1648
+ /**
1649
+ * Get installed plugins from the manifest
1650
+ */
1651
+ async getInstalledPlugins(packagePath) {
1652
+ const manifest = await this.readPackageManifest(packagePath);
1653
+ return manifest?.installedPlugins || {};
1654
+ }
1655
+ /**
1656
+ * Check if a plugin is installed
1657
+ */
1658
+ async isPluginInstalled(packagePath, pluginKey) {
1659
+ const installedPlugins = await this.getInstalledPlugins(packagePath);
1660
+ return !!installedPlugins[pluginKey];
1661
+ }
1662
+ /**
1663
+ * Get a specific installed plugin metadata
1664
+ */
1665
+ async getInstalledPlugin(packagePath, pluginKey) {
1666
+ const installedPlugins = await this.getInstalledPlugins(packagePath);
1667
+ return installedPlugins[pluginKey] || null;
1668
+ }
1669
+ /**
1670
+ * Update package.json dependencies
1671
+ */
1672
+ async addDependency(packagePath, packageName, version, isDev = false) {
1673
+ const packageJsonPath = path13.join(packagePath, "package.json");
1674
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1675
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1676
+ }
1677
+ try {
1678
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1679
+ const depsKey = isDev ? "devDependencies" : "dependencies";
1680
+ if (!packageJson[depsKey]) {
1681
+ packageJson[depsKey] = {};
1682
+ }
1683
+ packageJson[depsKey][packageName] = version;
1684
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1685
+ } catch (error) {
1686
+ throw new Error(`Failed to add dependency: ${error instanceof Error ? error.message : "Unknown error"}`);
1687
+ }
1688
+ }
1689
+ /**
1690
+ * Remove a dependency from package.json
1691
+ */
1692
+ async removeDependency(packagePath, packageName) {
1693
+ const packageJsonPath = path13.join(packagePath, "package.json");
1694
+ if (!await this.filesystemService.pathExists(packageJsonPath)) {
1695
+ throw new Error(`package.json not found at: ${packageJsonPath}`);
1696
+ }
1697
+ try {
1698
+ const packageJson = await this.filesystemService.readJSON(packageJsonPath);
1699
+ if (packageJson.dependencies?.[packageName]) {
1700
+ delete packageJson.dependencies[packageName];
1701
+ if (Object.keys(packageJson.dependencies).length === 0) {
1702
+ delete packageJson.dependencies;
1703
+ }
1704
+ }
1705
+ if (packageJson.devDependencies?.[packageName]) {
1706
+ delete packageJson.devDependencies[packageName];
1707
+ if (Object.keys(packageJson.devDependencies).length === 0) {
1708
+ delete packageJson.devDependencies;
1709
+ }
1710
+ }
1711
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1712
+ } catch (error) {
1713
+ throw new Error(`Failed to remove dependency: ${error instanceof Error ? error.message : "Unknown error"}`);
1714
+ }
1715
+ }
1716
+ /**
1717
+ * Get the package.json content
1718
+ */
1719
+ async readPackageJson(packagePath) {
1720
+ const packageJsonPath = path13.join(packagePath, "package.json");
1721
+ return await this.filesystemService.readJSON(packageJsonPath);
1722
+ }
1723
+ /**
1724
+ * Write the package.json content
1725
+ */
1726
+ async writePackageJson(packagePath, packageJson) {
1727
+ const packageJsonPath = path13.join(packagePath, "package.json");
1728
+ await this.filesystemService.writeJSON(packageJsonPath, packageJson);
1729
+ }
1730
+ };
396
1731
  export {
1732
+ FilesystemService,
397
1733
  Generator,
1734
+ InvalidPluginContextError,
1735
+ InvalidPluginNameError,
1736
+ MetadataService,
1737
+ MissingPluginTargetsError,
1738
+ NpmInstallationError,
1739
+ NpmService,
1740
+ PackageManifestService,
1741
+ PackageResolver,
1742
+ PluginDirectoryNotFoundError,
1743
+ PluginInstallationError,
1744
+ PluginInstaller,
1745
+ PluginNotFoundError,
1746
+ PluginResolutionError,
1747
+ PluginResolver,
1748
+ PluginService,
398
1749
  StandardGenerator,
399
- copyRecursive,
1750
+ WorkspaceManifestService,
1751
+ WorkspaceService,
1752
+ createInvalidContextError,
1753
+ createInvalidTargetError,
400
1754
  detectLaunch77Context,
401
- isValidNpmPackageName,
402
- parsePluginName,
403
- pathExists,
404
- readPluginMetadata,
405
- readTemplateMetadata,
406
- validatePluginConsistency,
407
- validatePluginConsistencyOrThrow,
408
- validatePluginName
1755
+ downloadNpmPackage,
1756
+ parseLocationFromPath
409
1757
  };
410
1758
  //# sourceMappingURL=index.js.map