@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.d.ts +734 -143
- package/dist/index.js +1631 -283
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,65 +1,469 @@
|
|
|
1
|
-
// src/
|
|
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
|
|
10
|
-
import * as
|
|
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/
|
|
15
|
-
import * as
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
await
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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/
|
|
39
|
-
import * as
|
|
40
|
-
import
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 =
|
|
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 =
|
|
85
|
-
const packageJsonContent = await
|
|
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
|
|
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 =
|
|
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 =
|
|
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/
|
|
127
|
-
import * as
|
|
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/
|
|
130
|
-
import * as
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
622
|
+
type: "unscoped",
|
|
623
|
+
isValid: true,
|
|
624
|
+
name: trimmedName
|
|
141
625
|
};
|
|
142
626
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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/
|
|
165
|
-
import * as
|
|
166
|
-
import
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
1029
|
+
source: "npm",
|
|
1030
|
+
resolvedName: trimmedName,
|
|
1031
|
+
npmPackage
|
|
252
1032
|
};
|
|
253
1033
|
}
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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:
|
|
260
|
-
|
|
1126
|
+
isValid: errors.length === 0,
|
|
1127
|
+
errors: errors.length > 0 ? errors : void 0
|
|
261
1128
|
};
|
|
262
1129
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
277
|
-
|
|
1206
|
+
isValid: errors.length === 0,
|
|
1207
|
+
errors: errors.length > 0 ? errors : void 0
|
|
278
1208
|
};
|
|
279
1209
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
1249
|
+
pluginPath,
|
|
1250
|
+
source: resolution.source,
|
|
1251
|
+
npmPackage: resolution.npmPackage,
|
|
1252
|
+
version
|
|
288
1253
|
};
|
|
289
1254
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1312
|
+
pluginName,
|
|
1313
|
+
filesInstalled: true,
|
|
1314
|
+
packageJsonUpdated: true,
|
|
1315
|
+
dependenciesInstalled: true
|
|
314
1316
|
};
|
|
315
1317
|
}
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
isValid: false,
|
|
321
|
-
error: validation.error
|
|
1414
|
+
pluginName
|
|
322
1415
|
};
|
|
323
1416
|
}
|
|
324
|
-
|
|
325
|
-
type: "unscoped",
|
|
326
|
-
isValid: true,
|
|
327
|
-
name: trimmedName
|
|
328
|
-
};
|
|
329
|
-
}
|
|
1417
|
+
};
|
|
330
1418
|
|
|
331
|
-
// src/
|
|
332
|
-
import * as
|
|
333
|
-
import
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
1750
|
+
WorkspaceManifestService,
|
|
1751
|
+
WorkspaceService,
|
|
1752
|
+
createInvalidContextError,
|
|
1753
|
+
createInvalidTargetError,
|
|
400
1754
|
detectLaunch77Context,
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
pathExists,
|
|
404
|
-
readPluginMetadata,
|
|
405
|
-
readTemplateMetadata,
|
|
406
|
-
validatePluginConsistency,
|
|
407
|
-
validatePluginConsistencyOrThrow,
|
|
408
|
-
validatePluginName
|
|
1755
|
+
downloadNpmPackage,
|
|
1756
|
+
parseLocationFromPath
|
|
409
1757
|
};
|
|
410
1758
|
//# sourceMappingURL=index.js.map
|