@looplia/looplia-cli 0.7.4 → 0.8.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.
@@ -3,7 +3,7 @@ import {
3
3
  init_esm_shims
4
4
  } from "./chunk-Y55L47HC.js";
5
5
 
6
- // ../../packages/provider/dist/chunk-MM63NAER.js
6
+ // ../../packages/provider/dist/chunk-ZVGA4TFN.js
7
7
  init_esm_shims();
8
8
  import path from "path";
9
9
  import { fileURLToPath } from "url";
@@ -5,17 +5,17 @@ import {
5
5
  TRAILING_SLASH_REGEX,
6
6
  createProgress,
7
7
  loadSources
8
- } from "./chunk-XTUQVJYH.js";
8
+ } from "./chunk-MTYPUSCH.js";
9
9
  import {
10
10
  isValidGitUrl,
11
11
  isValidPathSegment,
12
12
  pathExists
13
- } from "./chunk-VRBGWKZ6.js";
13
+ } from "./chunk-326UJHZM.js";
14
14
  import {
15
15
  init_esm_shims
16
16
  } from "./chunk-Y55L47HC.js";
17
17
 
18
- // ../../packages/provider/dist/chunk-O57P5VMU.js
18
+ // ../../packages/provider/dist/chunk-AOIDFPNW.js
19
19
  init_esm_shims();
20
20
  import { exec } from "child_process";
21
21
  import { cp, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env bun
2
+ import {
3
+ __dirname,
4
+ pathExists
5
+ } from "./chunk-326UJHZM.js";
6
+ import {
7
+ init_esm_shims
8
+ } from "./chunk-Y55L47HC.js";
9
+
10
+ // ../../packages/provider/dist/chunk-WLNOVOST.js
11
+ init_esm_shims();
12
+ import { createHash } from "crypto";
13
+ import {
14
+ cp,
15
+ mkdir as mkdir2,
16
+ readdir as readdir2,
17
+ readFile,
18
+ realpath,
19
+ rm,
20
+ writeFile
21
+ } from "fs/promises";
22
+ import { homedir as homedir2, tmpdir } from "os";
23
+ import { dirname, join as join2 } from "path";
24
+ import { fileURLToPath } from "url";
25
+ import { exec } from "child_process";
26
+ import { mkdir, readdir } from "fs/promises";
27
+ import { join } from "path";
28
+ import { promisify } from "util";
29
+ var execAsync = promisify(exec);
30
+ var CORE_SKILLS = [
31
+ "workflow-executor",
32
+ "workflow-validator",
33
+ "registry-loader"
34
+ ];
35
+ async function buildSkillPluginMap(pluginPaths) {
36
+ const map = /* @__PURE__ */ new Map();
37
+ for (const { path: pluginPath } of pluginPaths) {
38
+ const skillsDir = join(pluginPath, "skills");
39
+ if (!await pathExists(skillsDir)) {
40
+ continue;
41
+ }
42
+ try {
43
+ const entries = await readdir(skillsDir, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ if (entry.isDirectory()) {
46
+ map.set(entry.name, pluginPath);
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ return map;
53
+ }
54
+ async function getSelectivePluginPaths(requiredSkills) {
55
+ const allPluginPaths = await getPluginPaths();
56
+ if (!requiredSkills || requiredSkills.length === 0) {
57
+ return allPluginPaths;
58
+ }
59
+ const neededSkills = /* @__PURE__ */ new Set([...CORE_SKILLS, ...requiredSkills]);
60
+ const skillToPlugin = await buildSkillPluginMap(allPluginPaths);
61
+ const pluginsToLoad = /* @__PURE__ */ new Set();
62
+ for (const skill of neededSkills) {
63
+ const pluginPath = skillToPlugin.get(skill);
64
+ if (pluginPath) {
65
+ pluginsToLoad.add(pluginPath);
66
+ }
67
+ }
68
+ return allPluginPaths.filter((p) => pluginsToLoad.has(p.path));
69
+ }
70
+ function isCoreSkill(skillName) {
71
+ return CORE_SKILLS.includes(skillName);
72
+ }
73
+ async function computeSha256(filePath) {
74
+ const content = await readFile(filePath);
75
+ return createHash("sha256").update(content).digest("hex");
76
+ }
77
+ async function validateExtractedPaths(baseDir) {
78
+ const realBase = await realpath(baseDir);
79
+ const entries = await readdir2(baseDir, { withFileTypes: true });
80
+ for (const entry of entries) {
81
+ const fullPath = join2(baseDir, entry.name);
82
+ const entryRealPath = await realpath(fullPath);
83
+ if (!entryRealPath.startsWith(realBase)) {
84
+ throw new Error(
85
+ `Security: Path traversal detected in extracted file: ${entry.name}`
86
+ );
87
+ }
88
+ if (entry.isDirectory()) {
89
+ await validateExtractedPaths(fullPath);
90
+ }
91
+ }
92
+ }
93
+ function getLoopliaPluginPath() {
94
+ return process.env.LOOPLIA_HOME ?? join2(homedir2(), ".looplia");
95
+ }
96
+ function getBundledPluginsPath() {
97
+ const currentFile = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
98
+ return join2(currentFile, "..", "..", "plugins");
99
+ }
100
+ async function parseMarketplace(marketplacePath) {
101
+ const content = await readFile(marketplacePath, "utf-8");
102
+ return JSON.parse(content);
103
+ }
104
+ async function getPluginNamesFromSource(bundledPath) {
105
+ const marketplacePath = join2(
106
+ bundledPath,
107
+ "..",
108
+ ".claude-plugin",
109
+ "marketplace.json"
110
+ );
111
+ if (await pathExists(marketplacePath)) {
112
+ const marketplace = await parseMarketplace(marketplacePath);
113
+ return marketplace.plugins.map((p) => {
114
+ const parts = p.source.split("/");
115
+ return parts.at(-1);
116
+ }).filter((name) => name !== void 0);
117
+ }
118
+ const entries = await readdir2(bundledPath, { withFileTypes: true });
119
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
120
+ }
121
+ function createDefaultProfile() {
122
+ return {
123
+ userId: "default",
124
+ topics: [],
125
+ style: {
126
+ tone: "intermediate",
127
+ targetWordCount: 1e3,
128
+ voice: "first-person"
129
+ }
130
+ };
131
+ }
132
+ async function extractWorkflows(targetDir, pluginsDir, pluginNames) {
133
+ const workflowsDir = join2(targetDir, "workflows");
134
+ await mkdir2(workflowsDir, { recursive: true });
135
+ for (const pluginName of pluginNames) {
136
+ const pluginWorkflowsPath = join2(pluginsDir, pluginName, "workflows");
137
+ if (!await pathExists(pluginWorkflowsPath)) {
138
+ continue;
139
+ }
140
+ const entries = await readdir2(pluginWorkflowsPath, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ if (entry.isFile() && entry.name.endsWith(".md")) {
143
+ await cp(
144
+ join2(pluginWorkflowsPath, entry.name),
145
+ join2(workflowsDir, entry.name)
146
+ );
147
+ }
148
+ }
149
+ await rm(pluginWorkflowsPath, { recursive: true, force: true });
150
+ }
151
+ }
152
+ async function copyPlugins(targetDir, sourcePath) {
153
+ const bundledPath = sourcePath ?? getBundledPluginsPath();
154
+ const pluginNames = await getPluginNamesFromSource(bundledPath);
155
+ if (pluginNames.length === 0) {
156
+ throw new Error(`No plugins found at ${bundledPath}`);
157
+ }
158
+ if (await pathExists(targetDir)) {
159
+ await rm(targetDir, { recursive: true, force: true });
160
+ }
161
+ await mkdir2(targetDir, { recursive: true });
162
+ const pluginsDir = join2(targetDir, "plugins");
163
+ await mkdir2(pluginsDir, { recursive: true });
164
+ for (const pluginName of pluginNames) {
165
+ const pluginPath = join2(bundledPath, pluginName);
166
+ if (await pathExists(pluginPath)) {
167
+ await cp(pluginPath, join2(pluginsDir, pluginName), { recursive: true });
168
+ }
169
+ }
170
+ await extractWorkflows(targetDir, pluginsDir, pluginNames);
171
+ await mkdir2(join2(targetDir, "sandbox"), { recursive: true });
172
+ await writeFile(
173
+ join2(targetDir, "user-profile.json"),
174
+ JSON.stringify(createDefaultProfile(), null, 2),
175
+ "utf-8"
176
+ );
177
+ const { initializeRegistry, compileRegistry } = await import("./compiler-QKB2ZYNK-CYEN6G5G.js");
178
+ await initializeRegistry();
179
+ const { syncRegistrySources } = await import("./sync-XZGFZXZF-6AWT77CE.js");
180
+ const syncResults = await syncRegistrySources({ showProgress: true });
181
+ for (const result of syncResults) {
182
+ if (result.status === "failed") {
183
+ console.warn(
184
+ `Warning: Failed to sync ${result.source.id}: ${result.error}`
185
+ );
186
+ }
187
+ }
188
+ await compileRegistry();
189
+ }
190
+ async function downloadRemotePlugins(version, targetDir) {
191
+ 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`;
192
+ console.log(`Downloading looplia plugins from ${releaseUrl}...`);
193
+ const tempDir = join2(tmpdir(), `looplia-download-${Date.now()}`);
194
+ await mkdir2(tempDir, { recursive: true });
195
+ try {
196
+ const response = await fetch(releaseUrl);
197
+ if (!response.ok) {
198
+ throw new Error(
199
+ `Failed to download plugins: ${response.status} ${response.statusText}`
200
+ );
201
+ }
202
+ const tarPath = join2(tempDir, "plugins.tar.gz");
203
+ await writeFile(tarPath, Buffer.from(await response.arrayBuffer()));
204
+ const checksumUrl = `${releaseUrl}.sha256`;
205
+ const checksumResponse = await fetch(checksumUrl);
206
+ if (checksumResponse.ok) {
207
+ const checksumText = await checksumResponse.text();
208
+ const checksumParts = checksumText.split(" ");
209
+ const expectedChecksum = checksumParts[0]?.trim();
210
+ if (!expectedChecksum) {
211
+ throw new Error("Invalid checksum file format");
212
+ }
213
+ const actualChecksum = await computeSha256(tarPath);
214
+ if (actualChecksum !== expectedChecksum) {
215
+ throw new Error(
216
+ `Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}`
217
+ );
218
+ }
219
+ console.log("\u2713 Checksum verified");
220
+ } else {
221
+ console.log(
222
+ "\u26A0 Checksum file not available (older release), skipping verification"
223
+ );
224
+ }
225
+ const { exec: exec2 } = await import("child_process");
226
+ const { promisify: promisify2 } = await import("util");
227
+ const execAsync2 = promisify2(exec2);
228
+ try {
229
+ await execAsync2("tar -xzf plugins.tar.gz", { cwd: tempDir });
230
+ } catch (error) {
231
+ const errorMessage = error instanceof Error ? error.message : String(error);
232
+ throw new Error(
233
+ `Failed to extract plugins tarball. Ensure 'tar' is available. Error: ${errorMessage}`
234
+ );
235
+ }
236
+ await rm(tarPath);
237
+ await validateExtractedPaths(tempDir);
238
+ await copyPlugins(targetDir, tempDir);
239
+ console.log(`Plugins downloaded and extracted to ${targetDir}`);
240
+ } finally {
241
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {
242
+ });
243
+ }
244
+ }
245
+ async function isLoopliaInitialized() {
246
+ const pluginPath = getLoopliaPluginPath();
247
+ const pluginsDir = join2(pluginPath, "plugins");
248
+ try {
249
+ const entries = await readdir2(pluginsDir, { withFileTypes: true });
250
+ return entries.some((e) => e.isDirectory() && !e.name.startsWith("."));
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+ function getDevPluginPaths(projectRoot) {
256
+ return [
257
+ { type: "local", path: join2(projectRoot, "plugins", "looplia-core") },
258
+ { type: "local", path: join2(projectRoot, "plugins", "looplia-writer") }
259
+ ];
260
+ }
261
+ async function getProdPluginPaths() {
262
+ const loopliaPath = getLoopliaPluginPath();
263
+ const pluginsDir = join2(loopliaPath, "plugins");
264
+ const results = [];
265
+ try {
266
+ const entries = await readdir2(pluginsDir, { withFileTypes: true });
267
+ for (const entry of entries) {
268
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
269
+ results.push({ type: "local", path: join2(pluginsDir, entry.name) });
270
+ }
271
+ }
272
+ } catch {
273
+ }
274
+ return results;
275
+ }
276
+ async function getPluginPaths() {
277
+ if (process.env.LOOPLIA_HOME) {
278
+ return await getProdPluginPaths();
279
+ }
280
+ if (process.env.LOOPLIA_DEV === "true") {
281
+ const devRoot = process.env.LOOPLIA_DEV_ROOT ?? process.cwd();
282
+ return getDevPluginPaths(devRoot);
283
+ }
284
+ return await getProdPluginPaths();
285
+ }
286
+
287
+ export {
288
+ CORE_SKILLS,
289
+ getSelectivePluginPaths,
290
+ isCoreSkill,
291
+ getLoopliaPluginPath,
292
+ copyPlugins,
293
+ downloadRemotePlugins,
294
+ isLoopliaInitialized,
295
+ getPluginPaths
296
+ };
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  pathExists
4
- } from "./chunk-VRBGWKZ6.js";
4
+ } from "./chunk-326UJHZM.js";
5
5
  import {
6
6
  init_esm_shims
7
7
  } from "./chunk-Y55L47HC.js";
8
8
 
9
- // ../../packages/provider/dist/chunk-QHAUYGWN.js
9
+ // ../../packages/provider/dist/chunk-3DYS5U5N.js
10
10
  init_esm_shims();
11
11
  import { exec } from "child_process";
12
12
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
@@ -354,13 +354,25 @@ async function getGitRemoteUrl(repoPath) {
354
354
  return;
355
355
  }
356
356
  }
357
+ async function getSkillSourceUrl(skillPath) {
358
+ const sourcePath = join(skillPath, "source.json");
359
+ try {
360
+ if (await pathExists(sourcePath)) {
361
+ const content = await readFile(sourcePath, "utf-8");
362
+ const sourceData = JSON.parse(content);
363
+ return sourceData.gitUrl;
364
+ }
365
+ } catch {
366
+ }
367
+ return;
368
+ }
357
369
  async function scanPluginDirectory(pluginPath, pluginName, sourceType) {
358
370
  const skills = [];
359
371
  const skillsPath = join(pluginPath, "skills");
360
372
  if (!await pathExists(skillsPath)) {
361
373
  return skills;
362
374
  }
363
- const gitUrl = sourceType === "thirdparty" ? await getGitRemoteUrl(pluginPath) : void 0;
375
+ const pluginGitUrl = sourceType === "thirdparty" ? await getGitRemoteUrl(pluginPath) : void 0;
364
376
  try {
365
377
  const skillEntries = await readdir(skillsPath, { withFileTypes: true });
366
378
  for (const skillEntry of skillEntries) {
@@ -370,6 +382,8 @@ async function scanPluginDirectory(pluginPath, pluginName, sourceType) {
370
382
  const skillPath = join(skillsPath, skillEntry.name);
371
383
  const metadata = await parseSkillMetadata(skillPath);
372
384
  if (metadata) {
385
+ const skillGitUrl = await getSkillSourceUrl(skillPath);
386
+ const gitUrl = skillGitUrl ?? pluginGitUrl;
373
387
  skills.push({
374
388
  name: metadata.name ?? skillEntry.name,
375
389
  title: metadata.title ?? formatTitle(skillEntry.name),
@@ -394,35 +408,20 @@ async function scanPluginDirectory(pluginPath, pluginName, sourceType) {
394
408
  }
395
409
  async function scanLocalPlugins(loopliaPath) {
396
410
  const skills = [];
397
- try {
398
- const entries = await readdir(loopliaPath, { withFileTypes: true });
399
- const builtinDirs = entries.filter(
400
- (e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "sandbox" && e.name !== "workflows" && e.name !== "registry" && e.name !== "plugins"
401
- );
402
- for (const pluginDir of builtinDirs) {
403
- const pluginPath = join(loopliaPath, pluginDir.name);
404
- const pluginSkills = await scanPluginDirectory(
405
- pluginPath,
406
- pluginDir.name,
407
- "builtin"
408
- );
409
- skills.push(...pluginSkills);
410
- }
411
- } catch {
412
- }
413
411
  const pluginsDir = join(loopliaPath, "plugins");
414
412
  if (await pathExists(pluginsDir)) {
415
413
  try {
416
414
  const entries = await readdir(pluginsDir, { withFileTypes: true });
417
- const thirdPartyDirs = entries.filter(
415
+ const pluginDirs = entries.filter(
418
416
  (e) => e.isDirectory() && !e.name.startsWith(".")
419
417
  );
420
- for (const pluginDir of thirdPartyDirs) {
418
+ for (const pluginDir of pluginDirs) {
421
419
  const pluginPath = join(pluginsDir, pluginDir.name);
420
+ const sourceType = pluginDir.name.startsWith("looplia-") ? "builtin" : "thirdparty";
422
421
  const pluginSkills = await scanPluginDirectory(
423
422
  pluginPath,
424
423
  pluginDir.name,
425
- "thirdparty"
424
+ sourceType
426
425
  );
427
426
  skills.push(...pluginSkills);
428
427
  }