@looplia/looplia-cli 0.6.10 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ __dirname2 as __dirname,
4
+ isValidGitUrl,
5
+ pathExists
6
+ } from "./chunk-FCL2HRTX.js";
7
+
8
+ // ../../packages/provider/dist/chunk-FBJ3JTGZ.js
9
+ import { exec } from "child_process";
10
+ import { cp as cp2, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
11
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
12
+ import { basename, join as join2 } from "path";
13
+ import { promisify } from "util";
14
+ import { createHash } from "crypto";
15
+ import {
16
+ cp,
17
+ mkdir,
18
+ readdir,
19
+ readFile,
20
+ realpath,
21
+ rm,
22
+ writeFile
23
+ } from "fs/promises";
24
+ import { homedir, tmpdir } from "os";
25
+ import { dirname, join } from "path";
26
+ import { fileURLToPath } from "url";
27
+ async function computeSha256(filePath) {
28
+ const content = await readFile(filePath);
29
+ return createHash("sha256").update(content).digest("hex");
30
+ }
31
+ async function validateExtractedPaths(baseDir) {
32
+ const realBase = await realpath(baseDir);
33
+ const entries = await readdir(baseDir, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const fullPath = join(baseDir, entry.name);
36
+ const entryRealPath = await realpath(fullPath);
37
+ if (!entryRealPath.startsWith(realBase)) {
38
+ throw new Error(
39
+ `Security: Path traversal detected in extracted file: ${entry.name}`
40
+ );
41
+ }
42
+ if (entry.isDirectory()) {
43
+ await validateExtractedPaths(fullPath);
44
+ }
45
+ }
46
+ }
47
+ function getLoopliaPluginPath() {
48
+ return process.env.LOOPLIA_HOME ?? join(homedir(), ".looplia");
49
+ }
50
+ function getBundledPluginsPath() {
51
+ const currentFile = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
52
+ return join(currentFile, "..", "..", "plugins");
53
+ }
54
+ async function parseMarketplace(marketplacePath) {
55
+ const content = await readFile(marketplacePath, "utf-8");
56
+ return JSON.parse(content);
57
+ }
58
+ async function getPluginNamesFromSource(bundledPath) {
59
+ const marketplacePath = join(
60
+ bundledPath,
61
+ "..",
62
+ ".claude-plugin",
63
+ "marketplace.json"
64
+ );
65
+ if (await pathExists(marketplacePath)) {
66
+ const marketplace = await parseMarketplace(marketplacePath);
67
+ return marketplace.plugins.map((p) => {
68
+ const parts = p.source.split("/");
69
+ return parts.at(-1);
70
+ }).filter((name) => name !== void 0);
71
+ }
72
+ const entries = await readdir(bundledPath, { withFileTypes: true });
73
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
74
+ }
75
+ function createDefaultProfile() {
76
+ return {
77
+ userId: "default",
78
+ topics: [],
79
+ style: {
80
+ tone: "intermediate",
81
+ targetWordCount: 1e3,
82
+ voice: "first-person"
83
+ }
84
+ };
85
+ }
86
+ async function extractWorkflows(targetDir, pluginNames) {
87
+ const workflowsDir = join(targetDir, "workflows");
88
+ await mkdir(workflowsDir, { recursive: true });
89
+ for (const pluginName of pluginNames) {
90
+ const pluginWorkflowsPath = join(targetDir, pluginName, "workflows");
91
+ if (!await pathExists(pluginWorkflowsPath)) {
92
+ continue;
93
+ }
94
+ const entries = await readdir(pluginWorkflowsPath, { withFileTypes: true });
95
+ for (const entry of entries) {
96
+ if (entry.isFile() && entry.name.endsWith(".md")) {
97
+ await cp(
98
+ join(pluginWorkflowsPath, entry.name),
99
+ join(workflowsDir, entry.name)
100
+ );
101
+ }
102
+ }
103
+ await rm(pluginWorkflowsPath, { recursive: true, force: true });
104
+ }
105
+ }
106
+ async function copyPlugins(targetDir, sourcePath) {
107
+ const bundledPath = sourcePath ?? getBundledPluginsPath();
108
+ const pluginNames = await getPluginNamesFromSource(bundledPath);
109
+ if (pluginNames.length === 0) {
110
+ throw new Error(`No plugins found at ${bundledPath}`);
111
+ }
112
+ if (await pathExists(targetDir)) {
113
+ await rm(targetDir, { recursive: true, force: true });
114
+ }
115
+ await mkdir(targetDir, { recursive: true });
116
+ for (const pluginName of pluginNames) {
117
+ const pluginPath = join(bundledPath, pluginName);
118
+ if (await pathExists(pluginPath)) {
119
+ await cp(pluginPath, join(targetDir, pluginName), { recursive: true });
120
+ }
121
+ }
122
+ await extractWorkflows(targetDir, pluginNames);
123
+ await mkdir(join(targetDir, "sandbox"), { recursive: true });
124
+ await writeFile(
125
+ join(targetDir, "user-profile.json"),
126
+ JSON.stringify(createDefaultProfile(), null, 2),
127
+ "utf-8"
128
+ );
129
+ const { installDefaultSources: installDefaultSources2 } = await import("./skill-installer-GJYXIKXE-VS4MWW3V.js");
130
+ const installResults = await installDefaultSources2();
131
+ for (const result of installResults) {
132
+ if (result.status === "failed") {
133
+ console.warn(
134
+ `Warning: Failed to download ${result.skill}: ${result.error}`
135
+ );
136
+ }
137
+ }
138
+ const { compileRegistry } = await import("./compiler-J4DARL4X-2TXNVYEY.js");
139
+ await compileRegistry();
140
+ }
141
+ async function downloadRemotePlugins(version, targetDir) {
142
+ const releaseUrl = version === "latest" ? "https://github.com/memorysaver/looplia-core/releases/latest/download/plugins.tar.gz" : `https://github.com/memorysaver/looplia-core/releases/download/${version}/plugins.tar.gz`;
143
+ console.log(`Downloading looplia plugins from ${releaseUrl}...`);
144
+ const tempDir = join(tmpdir(), `looplia-download-${Date.now()}`);
145
+ await mkdir(tempDir, { recursive: true });
146
+ try {
147
+ const response = await fetch(releaseUrl);
148
+ if (!response.ok) {
149
+ throw new Error(
150
+ `Failed to download plugins: ${response.status} ${response.statusText}`
151
+ );
152
+ }
153
+ const tarPath = join(tempDir, "plugins.tar.gz");
154
+ await writeFile(tarPath, Buffer.from(await response.arrayBuffer()));
155
+ const checksumUrl = `${releaseUrl}.sha256`;
156
+ const checksumResponse = await fetch(checksumUrl);
157
+ if (checksumResponse.ok) {
158
+ const checksumText = await checksumResponse.text();
159
+ const checksumParts = checksumText.split(" ");
160
+ const expectedChecksum = checksumParts[0]?.trim();
161
+ if (!expectedChecksum) {
162
+ throw new Error("Invalid checksum file format");
163
+ }
164
+ const actualChecksum = await computeSha256(tarPath);
165
+ if (actualChecksum !== expectedChecksum) {
166
+ throw new Error(
167
+ `Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}`
168
+ );
169
+ }
170
+ console.log("\u2713 Checksum verified");
171
+ } else {
172
+ console.log(
173
+ "\u26A0 Checksum file not available (older release), skipping verification"
174
+ );
175
+ }
176
+ const { exec: exec2 } = await import("child_process");
177
+ const { promisify: promisify2 } = await import("util");
178
+ const execAsync2 = promisify2(exec2);
179
+ try {
180
+ await execAsync2("tar -xzf plugins.tar.gz", { cwd: tempDir });
181
+ } catch (error) {
182
+ const errorMessage = error instanceof Error ? error.message : String(error);
183
+ throw new Error(
184
+ `Failed to extract plugins tarball. Ensure 'tar' is available. Error: ${errorMessage}`
185
+ );
186
+ }
187
+ await rm(tarPath);
188
+ await validateExtractedPaths(tempDir);
189
+ await copyPlugins(targetDir, tempDir);
190
+ console.log(`Plugins downloaded and extracted to ${targetDir}`);
191
+ } finally {
192
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {
193
+ });
194
+ }
195
+ }
196
+ async function isLoopliaInitialized() {
197
+ const pluginPath = getLoopliaPluginPath();
198
+ try {
199
+ const entries = await readdir(pluginPath, { withFileTypes: true });
200
+ return entries.some(
201
+ (e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "sandbox" && e.name !== "workflows"
202
+ );
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+ function getDevPluginPaths(projectRoot) {
208
+ return [
209
+ { type: "local", path: join(projectRoot, "plugins", "looplia-core") },
210
+ { type: "local", path: join(projectRoot, "plugins", "looplia-writer") }
211
+ ];
212
+ }
213
+ async function getProdPluginPaths() {
214
+ const loopliaPath = getLoopliaPluginPath();
215
+ const results = [];
216
+ try {
217
+ const entries = await readdir(loopliaPath, { withFileTypes: true });
218
+ const pluginDirs = entries.filter(
219
+ (e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "sandbox" && e.name !== "workflows" && e.name !== "plugins" && e.name !== "registry"
220
+ ).map((e) => e.name);
221
+ for (const name of pluginDirs) {
222
+ results.push({ type: "local", path: join(loopliaPath, name) });
223
+ }
224
+ } catch {
225
+ }
226
+ const pluginsDir = join(loopliaPath, "plugins");
227
+ try {
228
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
229
+ const thirdPartyDirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
230
+ for (const name of thirdPartyDirs) {
231
+ results.push({ type: "local", path: join(pluginsDir, name) });
232
+ }
233
+ } catch {
234
+ }
235
+ return results;
236
+ }
237
+ async function getPluginPaths() {
238
+ if (process.env.LOOPLIA_HOME) {
239
+ return await getProdPluginPaths();
240
+ }
241
+ if (process.env.LOOPLIA_DEV === "true") {
242
+ const devRoot = process.env.LOOPLIA_DEV_ROOT ?? process.cwd();
243
+ return getDevPluginPaths(devRoot);
244
+ }
245
+ return await getProdPluginPaths();
246
+ }
247
+ var DEFAULT_SOURCES = [
248
+ {
249
+ name: "anthropic-skills",
250
+ url: "https://github.com/anthropics/skills",
251
+ description: "Official Anthropic skills - xlsx, pdf, pptx, docx, frontend-design, and more"
252
+ },
253
+ {
254
+ name: "awesome-claude-skills",
255
+ url: "https://github.com/ComposioHQ/awesome-claude-skills",
256
+ description: "Community-curated Claude skills collection by ComposioHQ"
257
+ }
258
+ ];
259
+ var execAsync = promisify(exec);
260
+ var PROTOCOL_REGEX = /^https?:\/\//;
261
+ var TRAILING_SLASH_REGEX = /\/$/;
262
+ var SLASH_TO_DASH_REGEX = /\//g;
263
+ var CORE_SKILLS = [
264
+ "workflow-executor",
265
+ "workflow-executor-inline",
266
+ "workflow-validator",
267
+ "registry-loader"
268
+ ];
269
+ async function buildSkillPluginMap(pluginPaths) {
270
+ const map = /* @__PURE__ */ new Map();
271
+ for (const { path: pluginPath } of pluginPaths) {
272
+ const skillsDir = join2(pluginPath, "skills");
273
+ if (!await pathExists(skillsDir)) {
274
+ continue;
275
+ }
276
+ try {
277
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
278
+ for (const entry of entries) {
279
+ if (entry.isDirectory()) {
280
+ map.set(entry.name, pluginPath);
281
+ }
282
+ }
283
+ } catch {
284
+ }
285
+ }
286
+ return map;
287
+ }
288
+ async function getSelectivePluginPaths(requiredSkills) {
289
+ const allPluginPaths = await getPluginPaths();
290
+ if (!requiredSkills || requiredSkills.length === 0) {
291
+ return allPluginPaths;
292
+ }
293
+ const neededSkills = /* @__PURE__ */ new Set([...CORE_SKILLS, ...requiredSkills]);
294
+ const skillToPlugin = await buildSkillPluginMap(allPluginPaths);
295
+ const pluginsToLoad = /* @__PURE__ */ new Set();
296
+ for (const skill of neededSkills) {
297
+ const pluginPath = skillToPlugin.get(skill);
298
+ if (pluginPath) {
299
+ pluginsToLoad.add(pluginPath);
300
+ }
301
+ }
302
+ return allPluginPaths.filter((p) => pluginsToLoad.has(p.path));
303
+ }
304
+ async function installThirdPartyPlugin(gitUrl, skillName) {
305
+ const loopliaPath = join2(homedir2(), ".looplia");
306
+ const pluginsDir = join2(loopliaPath, "plugins");
307
+ await mkdir2(pluginsDir, { recursive: true });
308
+ const repoName = gitUrl.replace(PROTOCOL_REGEX, "").replace("github.com/", "").replace(TRAILING_SLASH_REGEX, "").replace(SLASH_TO_DASH_REGEX, "-");
309
+ const targetPath = join2(pluginsDir, repoName);
310
+ try {
311
+ if (await pathExists(targetPath)) {
312
+ await execAsync("git pull", { cwd: targetPath });
313
+ return {
314
+ skill: skillName ?? repoName,
315
+ status: "updated",
316
+ path: targetPath
317
+ };
318
+ }
319
+ const fullUrl = gitUrl.startsWith("http") ? gitUrl : `https://${gitUrl}`;
320
+ if (!isValidGitUrl(fullUrl)) {
321
+ return {
322
+ skill: skillName ?? repoName,
323
+ status: "failed",
324
+ error: `Invalid or untrusted git URL: ${fullUrl}`
325
+ };
326
+ }
327
+ await execAsync(`git clone "${fullUrl}" "${targetPath}"`);
328
+ return {
329
+ skill: skillName ?? repoName,
330
+ status: "installed",
331
+ path: targetPath
332
+ };
333
+ } catch (error) {
334
+ const message = error instanceof Error ? error.message : String(error);
335
+ return {
336
+ skill: skillName ?? repoName,
337
+ status: "failed",
338
+ error: message
339
+ };
340
+ }
341
+ }
342
+ function isCoreSkill(skillName) {
343
+ return CORE_SKILLS.includes(skillName);
344
+ }
345
+ async function getPluginSkills(pluginPath) {
346
+ const skillsDir = join2(pluginPath, "skills");
347
+ if (!await pathExists(skillsDir)) {
348
+ return [];
349
+ }
350
+ try {
351
+ const entries = await readdir2(skillsDir, { withFileTypes: true });
352
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
353
+ } catch {
354
+ return [];
355
+ }
356
+ }
357
+ async function createPluginStructure(opts) {
358
+ const { pluginDir, plugin, marketplace } = opts;
359
+ await mkdir2(join2(pluginDir, ".claude-plugin"), { recursive: true });
360
+ await mkdir2(join2(pluginDir, "skills"), { recursive: true });
361
+ const pluginJson = {
362
+ name: plugin.name,
363
+ description: plugin.description,
364
+ source: { marketplace: marketplace.name, url: marketplace.url }
365
+ };
366
+ await writeFile2(
367
+ join2(pluginDir, ".claude-plugin", "plugin.json"),
368
+ JSON.stringify(pluginJson, null, 2)
369
+ );
370
+ }
371
+ async function copyPluginSkills(plugin, tempDir, pluginDir) {
372
+ if (plugin.skills?.length) {
373
+ for (const skillPath of plugin.skills) {
374
+ const skillName = basename(skillPath);
375
+ const srcPath = join2(tempDir, skillPath);
376
+ const destPath = join2(pluginDir, "skills", skillName);
377
+ if (await pathExists(srcPath)) {
378
+ await cp2(srcPath, destPath, { recursive: true });
379
+ }
380
+ }
381
+ } else {
382
+ const srcPath = join2(tempDir, plugin.source);
383
+ const destPath = join2(pluginDir, "skills", plugin.name);
384
+ if (await pathExists(srcPath)) {
385
+ await cp2(srcPath, destPath, { recursive: true });
386
+ }
387
+ }
388
+ }
389
+ async function installMarketplacePlugin(opts) {
390
+ const { plugin, tempDir, pluginsDir, marketplace } = opts;
391
+ const pluginDir = join2(pluginsDir, plugin.name);
392
+ try {
393
+ await createPluginStructure({ pluginDir, plugin, marketplace });
394
+ await copyPluginSkills(plugin, tempDir, pluginDir);
395
+ return { skill: plugin.name, status: "installed", path: pluginDir };
396
+ } catch (error) {
397
+ const message = error instanceof Error ? error.message : String(error);
398
+ return { skill: plugin.name, status: "failed", error: message };
399
+ }
400
+ }
401
+ async function installMarketplaceSource(source, tempDir, pluginsDir) {
402
+ const marketplacePath = join2(tempDir, ".claude-plugin", "marketplace.json");
403
+ if (!await pathExists(marketplacePath)) {
404
+ return [];
405
+ }
406
+ const marketplaceContent = await readFile2(marketplacePath, "utf-8");
407
+ const manifest = JSON.parse(marketplaceContent);
408
+ const marketplace = { name: manifest.name, url: source.url };
409
+ const results = [];
410
+ for (const plugin of manifest.plugins) {
411
+ const result = await installMarketplacePlugin({
412
+ plugin,
413
+ tempDir,
414
+ pluginsDir,
415
+ marketplace
416
+ });
417
+ results.push(result);
418
+ }
419
+ return results;
420
+ }
421
+ async function installDefaultSources() {
422
+ const loopliaPath = process.env.LOOPLIA_HOME ?? join2(homedir2(), ".looplia");
423
+ const pluginsDir = join2(loopliaPath, "plugins");
424
+ const registryDir = join2(loopliaPath, "registry");
425
+ await mkdir2(pluginsDir, { recursive: true });
426
+ await mkdir2(registryDir, { recursive: true });
427
+ const installPromises = DEFAULT_SOURCES.map(
428
+ async (source) => {
429
+ const tempDir = join2(tmpdir2(), `looplia-${source.name}-${Date.now()}`);
430
+ try {
431
+ if (!isValidGitUrl(source.url)) {
432
+ return {
433
+ results: [
434
+ {
435
+ skill: source.name,
436
+ status: "failed",
437
+ error: `Invalid or untrusted git URL: ${source.url}`
438
+ }
439
+ ]
440
+ };
441
+ }
442
+ await execAsync(`git clone --depth 1 "${source.url}" "${tempDir}"`);
443
+ const marketplacePath = join2(
444
+ tempDir,
445
+ ".claude-plugin",
446
+ "marketplace.json"
447
+ );
448
+ const hasMarketplace = await pathExists(marketplacePath);
449
+ let installResults2;
450
+ if (hasMarketplace) {
451
+ installResults2 = await installMarketplaceSource(
452
+ source,
453
+ tempDir,
454
+ pluginsDir
455
+ );
456
+ } else {
457
+ const targetPath = join2(pluginsDir, source.name);
458
+ await cp2(tempDir, targetPath, { recursive: true });
459
+ installResults2 = [
460
+ {
461
+ skill: source.name,
462
+ status: "installed",
463
+ path: targetPath
464
+ }
465
+ ];
466
+ }
467
+ return {
468
+ results: installResults2,
469
+ sourceEntry: {
470
+ id: `github:${source.name}`,
471
+ type: "github",
472
+ url: source.url,
473
+ enabled: true,
474
+ priority: 50,
475
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
476
+ }
477
+ };
478
+ } catch (error) {
479
+ const message = error instanceof Error ? error.message : String(error);
480
+ return {
481
+ results: [
482
+ {
483
+ skill: source.name,
484
+ status: "failed",
485
+ error: message
486
+ }
487
+ ]
488
+ };
489
+ } finally {
490
+ await rm2(tempDir, { recursive: true, force: true }).catch(() => {
491
+ });
492
+ }
493
+ }
494
+ );
495
+ const installResults = await Promise.all(installPromises);
496
+ const results = [];
497
+ const sourceEntries = [];
498
+ for (const { results: pluginResults, sourceEntry } of installResults) {
499
+ results.push(...pluginResults);
500
+ if (sourceEntry) {
501
+ sourceEntries.push(sourceEntry);
502
+ }
503
+ }
504
+ if (sourceEntries.length > 0) {
505
+ const sourcesPath = join2(registryDir, "sources.json");
506
+ await writeFile2(sourcesPath, JSON.stringify(sourceEntries, null, 2));
507
+ }
508
+ return results;
509
+ }
510
+
511
+ export {
512
+ getLoopliaPluginPath,
513
+ copyPlugins,
514
+ downloadRemotePlugins,
515
+ isLoopliaInitialized,
516
+ getPluginPaths,
517
+ CORE_SKILLS,
518
+ getSelectivePluginPaths,
519
+ installThirdPartyPlugin,
520
+ isCoreSkill,
521
+ getPluginSkills,
522
+ installDefaultSources
523
+ };