@folpe/loom 0.4.0 → 1.1.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.
- package/README.md +20 -0
- package/dist/index.js +332 -41
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -59,6 +59,26 @@ loom add skill tailwind-patterns
|
|
|
59
59
|
|
|
60
60
|
Files are written to `.claude/agents/` and `.claude/skills/` in your current directory.
|
|
61
61
|
|
|
62
|
+
If the resource isn't found in the bundled library, Loom will automatically check your local library (`~/.loom/library/`) for marketplace-installed resources.
|
|
63
|
+
|
|
64
|
+
### Browse the marketplace
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
loom marketplace search # list all public resources
|
|
68
|
+
loom marketplace search tailwind # search by keyword
|
|
69
|
+
loom marketplace search --type skill # filter by type
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Alias: `loom mp search`
|
|
73
|
+
|
|
74
|
+
### Install from the marketplace
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
loom marketplace install ticket-craft
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This downloads the resource to your local library (`~/.loom/library/`). You can then use it with `loom add` in any project.
|
|
81
|
+
|
|
62
82
|
## What's included
|
|
63
83
|
|
|
64
84
|
### Agents
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,34 @@ function listFiles(dir) {
|
|
|
23
23
|
if (!fs.existsSync(dir)) return [];
|
|
24
24
|
return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile()).map((d) => d.name).sort();
|
|
25
25
|
}
|
|
26
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
27
|
+
".md",
|
|
28
|
+
".ts",
|
|
29
|
+
".js",
|
|
30
|
+
".sh",
|
|
31
|
+
".dot",
|
|
32
|
+
".yaml",
|
|
33
|
+
".yml",
|
|
34
|
+
".json",
|
|
35
|
+
".css",
|
|
36
|
+
".html"
|
|
37
|
+
]);
|
|
38
|
+
function walkDir(dir, base = "") {
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
results.push(...walkDir(path.join(dir, entry.name), rel));
|
|
45
|
+
} else if (entry.isFile()) {
|
|
46
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
47
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
48
|
+
results.push(rel);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
26
54
|
async function listAgents() {
|
|
27
55
|
const agentsDir = path.join(DATA_DIR, "agents");
|
|
28
56
|
const slugs = listSubDirs(agentsDir);
|
|
@@ -94,10 +122,16 @@ async function getAgent(slug) {
|
|
|
94
122
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
95
123
|
return { slug, rawContent: raw };
|
|
96
124
|
}
|
|
97
|
-
async function
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
125
|
+
async function getSkillWithFiles(slug) {
|
|
126
|
+
const skillDir = path.join(DATA_DIR, "skills", slug);
|
|
127
|
+
const mainPath = path.join(skillDir, "SKILL.md");
|
|
128
|
+
const mainContent = fs.readFileSync(mainPath, "utf-8");
|
|
129
|
+
const relativePaths = walkDir(skillDir);
|
|
130
|
+
const files = relativePaths.map((relativePath) => ({
|
|
131
|
+
relativePath,
|
|
132
|
+
content: fs.readFileSync(path.join(skillDir, relativePath), "utf-8")
|
|
133
|
+
}));
|
|
134
|
+
return { slug, mainContent, files };
|
|
101
135
|
}
|
|
102
136
|
async function getPreset(slug) {
|
|
103
137
|
const filePath = path.join(DATA_DIR, "presets", `${slug}.yaml`);
|
|
@@ -106,6 +140,116 @@ async function getPreset(slug) {
|
|
|
106
140
|
return { ...data, slug };
|
|
107
141
|
}
|
|
108
142
|
|
|
143
|
+
// src/lib/local-library.ts
|
|
144
|
+
import fs2 from "fs";
|
|
145
|
+
import path2 from "path";
|
|
146
|
+
import os from "os";
|
|
147
|
+
var LIBRARY_DIR = path2.join(os.homedir(), ".loom", "library");
|
|
148
|
+
function ensureDir(dir) {
|
|
149
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
function saveLocalAgent(slug, content) {
|
|
152
|
+
const dir = path2.join(LIBRARY_DIR, "agents", slug);
|
|
153
|
+
ensureDir(dir);
|
|
154
|
+
const filePath = path2.join(dir, "AGENT.md");
|
|
155
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
156
|
+
return filePath;
|
|
157
|
+
}
|
|
158
|
+
function saveLocalSkill(slug, files) {
|
|
159
|
+
const dir = path2.join(LIBRARY_DIR, "skills", slug);
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const filePath = path2.join(dir, file.relativePath);
|
|
162
|
+
ensureDir(path2.dirname(filePath));
|
|
163
|
+
fs2.writeFileSync(filePath, file.content, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
return dir;
|
|
166
|
+
}
|
|
167
|
+
function saveLocalPreset(slug, content) {
|
|
168
|
+
const dir = path2.join(LIBRARY_DIR, "presets");
|
|
169
|
+
ensureDir(dir);
|
|
170
|
+
const filePath = path2.join(dir, `${slug}.yaml`);
|
|
171
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
172
|
+
return filePath;
|
|
173
|
+
}
|
|
174
|
+
function getLocalAgent(slug) {
|
|
175
|
+
const filePath = path2.join(LIBRARY_DIR, "agents", slug, "AGENT.md");
|
|
176
|
+
try {
|
|
177
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
178
|
+
return { slug, rawContent: raw };
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
184
|
+
".md",
|
|
185
|
+
".ts",
|
|
186
|
+
".js",
|
|
187
|
+
".sh",
|
|
188
|
+
".dot",
|
|
189
|
+
".yaml",
|
|
190
|
+
".yml",
|
|
191
|
+
".json",
|
|
192
|
+
".css",
|
|
193
|
+
".html"
|
|
194
|
+
]);
|
|
195
|
+
function walkDir2(dir, base = "") {
|
|
196
|
+
let entries;
|
|
197
|
+
try {
|
|
198
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const results = [];
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
results.push(...walkDir2(path2.join(dir, entry.name), rel));
|
|
207
|
+
} else if (entry.isFile()) {
|
|
208
|
+
const ext = path2.extname(entry.name).toLowerCase();
|
|
209
|
+
if (TEXT_EXTENSIONS2.has(ext)) {
|
|
210
|
+
results.push(rel);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
216
|
+
function getLocalSkillWithFiles(slug) {
|
|
217
|
+
const dir = path2.join(LIBRARY_DIR, "skills", slug);
|
|
218
|
+
if (!fs2.existsSync(dir)) return null;
|
|
219
|
+
const relativePaths = walkDir2(dir);
|
|
220
|
+
if (relativePaths.length === 0) return null;
|
|
221
|
+
const files = relativePaths.map((relativePath) => ({
|
|
222
|
+
relativePath,
|
|
223
|
+
content: fs2.readFileSync(path2.join(dir, relativePath), "utf-8")
|
|
224
|
+
}));
|
|
225
|
+
return { slug, files };
|
|
226
|
+
}
|
|
227
|
+
function listSubDirs2(dir) {
|
|
228
|
+
try {
|
|
229
|
+
return fs2.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function listLocalResources() {
|
|
235
|
+
const items = [];
|
|
236
|
+
for (const slug of listSubDirs2(path2.join(LIBRARY_DIR, "agents"))) {
|
|
237
|
+
items.push({ slug, type: "agent" });
|
|
238
|
+
}
|
|
239
|
+
for (const slug of listSubDirs2(path2.join(LIBRARY_DIR, "skills"))) {
|
|
240
|
+
items.push({ slug, type: "skill" });
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const presetsDir = path2.join(LIBRARY_DIR, "presets");
|
|
244
|
+
const files = fs2.readdirSync(presetsDir).filter((f) => f.endsWith(".yaml"));
|
|
245
|
+
for (const f of files) {
|
|
246
|
+
items.push({ slug: f.replace(/\.yaml$/, ""), type: "preset" });
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
return items;
|
|
251
|
+
}
|
|
252
|
+
|
|
109
253
|
// src/commands/list.ts
|
|
110
254
|
function truncate(str, max) {
|
|
111
255
|
if (str.length <= max) return str;
|
|
@@ -116,6 +260,7 @@ function padEnd(str, len) {
|
|
|
116
260
|
}
|
|
117
261
|
async function listCommand(type) {
|
|
118
262
|
try {
|
|
263
|
+
const bundledSlugs = /* @__PURE__ */ new Set();
|
|
119
264
|
if (!type || type === "agents") {
|
|
120
265
|
const agents = await listAgents();
|
|
121
266
|
console.log(pc.bold(pc.cyan("\n Agents")));
|
|
@@ -124,6 +269,7 @@ async function listCommand(type) {
|
|
|
124
269
|
console.log(pc.dim(" No agents found."));
|
|
125
270
|
}
|
|
126
271
|
for (const a of agents) {
|
|
272
|
+
bundledSlugs.add(`agent:${a.slug}`);
|
|
127
273
|
console.log(
|
|
128
274
|
` ${padEnd(pc.green(a.slug), 30)} ${padEnd(a.name, 25)} ${pc.dim(truncate(a.description, 40))}`
|
|
129
275
|
);
|
|
@@ -137,6 +283,7 @@ async function listCommand(type) {
|
|
|
137
283
|
console.log(pc.dim(" No skills found."));
|
|
138
284
|
}
|
|
139
285
|
for (const s of skills) {
|
|
286
|
+
bundledSlugs.add(`skill:${s.slug}`);
|
|
140
287
|
console.log(
|
|
141
288
|
` ${padEnd(pc.green(s.slug), 30)} ${padEnd(s.name, 25)} ${pc.dim(truncate(s.description, 40))}`
|
|
142
289
|
);
|
|
@@ -150,12 +297,25 @@ async function listCommand(type) {
|
|
|
150
297
|
console.log(pc.dim(" No presets found."));
|
|
151
298
|
}
|
|
152
299
|
for (const p2 of presets) {
|
|
300
|
+
bundledSlugs.add(`preset:${p2.slug}`);
|
|
153
301
|
const meta = pc.dim(`(${p2.agentCount} agents, ${p2.skillCount} skills)`);
|
|
154
302
|
console.log(
|
|
155
303
|
` ${padEnd(pc.green(p2.slug), 30)} ${padEnd(p2.name, 25)} ${meta}`
|
|
156
304
|
);
|
|
157
305
|
}
|
|
158
306
|
}
|
|
307
|
+
const localItems = listLocalResources().filter(
|
|
308
|
+
(item) => !bundledSlugs.has(`${item.type}:${item.slug}`)
|
|
309
|
+
);
|
|
310
|
+
if (localItems.length > 0) {
|
|
311
|
+
console.log(pc.bold(pc.magenta("\n Installed (marketplace)")));
|
|
312
|
+
console.log(pc.dim(" " + "\u2500".repeat(60)));
|
|
313
|
+
for (const item of localItems) {
|
|
314
|
+
console.log(
|
|
315
|
+
` ${padEnd(pc.green(item.slug), 30)} ${pc.dim(`[${item.type}]`)}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
159
319
|
console.log();
|
|
160
320
|
} catch (error) {
|
|
161
321
|
handleError(error);
|
|
@@ -176,34 +336,36 @@ function handleError(error) {
|
|
|
176
336
|
import pc2 from "picocolors";
|
|
177
337
|
|
|
178
338
|
// src/lib/writer.ts
|
|
179
|
-
import
|
|
180
|
-
import
|
|
181
|
-
function
|
|
182
|
-
|
|
339
|
+
import fs3 from "fs";
|
|
340
|
+
import path3 from "path";
|
|
341
|
+
function ensureDir2(dirPath) {
|
|
342
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
183
343
|
}
|
|
184
344
|
function writeAgent(target, slug, content, cwd = process.cwd()) {
|
|
185
|
-
const dir =
|
|
186
|
-
|
|
187
|
-
const filePath =
|
|
188
|
-
|
|
345
|
+
const dir = path3.join(cwd, target.dir, target.agentsSubdir, slug);
|
|
346
|
+
ensureDir2(dir);
|
|
347
|
+
const filePath = path3.join(dir, "AGENT.md");
|
|
348
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
189
349
|
return filePath;
|
|
190
350
|
}
|
|
191
|
-
function
|
|
192
|
-
const dir =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
351
|
+
function writeSkillDir(target, slug, files, cwd = process.cwd()) {
|
|
352
|
+
const dir = path3.join(cwd, target.dir, target.skillsSubdir, slug);
|
|
353
|
+
for (const file of files) {
|
|
354
|
+
const filePath = path3.join(dir, file.relativePath);
|
|
355
|
+
ensureDir2(path3.dirname(filePath));
|
|
356
|
+
fs3.writeFileSync(filePath, file.content, "utf-8");
|
|
357
|
+
}
|
|
358
|
+
return dir;
|
|
197
359
|
}
|
|
198
360
|
function writeOrchestrator(target, content, cwd = process.cwd()) {
|
|
199
|
-
const filePath =
|
|
200
|
-
|
|
201
|
-
|
|
361
|
+
const filePath = path3.join(cwd, target.dir, target.orchestratorFile);
|
|
362
|
+
ensureDir2(path3.dirname(filePath));
|
|
363
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
202
364
|
return filePath;
|
|
203
365
|
}
|
|
204
366
|
function writeContextFile(target, content, cwd = process.cwd()) {
|
|
205
|
-
const filePath =
|
|
206
|
-
|
|
367
|
+
const filePath = path3.join(cwd, target.contextFile);
|
|
368
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
207
369
|
return filePath;
|
|
208
370
|
}
|
|
209
371
|
|
|
@@ -223,20 +385,39 @@ async function addCommand(type, slug, target) {
|
|
|
223
385
|
\u2713 Agent "${slug}" written to ${filePath}
|
|
224
386
|
`));
|
|
225
387
|
} else {
|
|
226
|
-
const skill = await
|
|
227
|
-
const
|
|
388
|
+
const skill = await getSkillWithFiles(slug);
|
|
389
|
+
const dirPath = writeSkillDir(target, slug, skill.files);
|
|
390
|
+
const fileCount = skill.files.length;
|
|
228
391
|
console.log(pc2.green(`
|
|
229
|
-
\u2713 Skill "${slug}" written to ${
|
|
392
|
+
\u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""})
|
|
230
393
|
`));
|
|
231
394
|
}
|
|
232
|
-
} catch
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
395
|
+
} catch {
|
|
396
|
+
if (type === "agent") {
|
|
397
|
+
const local = getLocalAgent(slug);
|
|
398
|
+
if (local) {
|
|
399
|
+
const filePath = writeAgent(target, slug, local.rawContent);
|
|
400
|
+
console.log(pc2.green(`
|
|
401
|
+
\u2713 Agent "${slug}" written to ${filePath} ${pc2.dim("(from ~/.loom/library)")}
|
|
236
402
|
`));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
237
405
|
} else {
|
|
238
|
-
|
|
406
|
+
const local = getLocalSkillWithFiles(slug);
|
|
407
|
+
if (local) {
|
|
408
|
+
const dirPath = writeSkillDir(target, slug, local.files);
|
|
409
|
+
const fileCount = local.files.length;
|
|
410
|
+
console.log(pc2.green(`
|
|
411
|
+
\u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""}) ${pc2.dim("(from ~/.loom/library)")}
|
|
412
|
+
`));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
239
415
|
}
|
|
416
|
+
console.error(pc2.red(`
|
|
417
|
+
Error: ${type} "${slug}" not found.
|
|
418
|
+
`));
|
|
419
|
+
console.log(pc2.dim(` Try: loom marketplace search ${slug}
|
|
420
|
+
`));
|
|
240
421
|
process.exit(1);
|
|
241
422
|
}
|
|
242
423
|
}
|
|
@@ -392,8 +573,8 @@ function resolveTarget(targetName, customDir, customContextFile) {
|
|
|
392
573
|
}
|
|
393
574
|
|
|
394
575
|
// src/lib/config.ts
|
|
395
|
-
import
|
|
396
|
-
import
|
|
576
|
+
import fs4 from "fs";
|
|
577
|
+
import path4 from "path";
|
|
397
578
|
var CONFIG_FILE = "loom.config.json";
|
|
398
579
|
function saveConfig(target, cwd = process.cwd()) {
|
|
399
580
|
const config = {
|
|
@@ -401,14 +582,14 @@ function saveConfig(target, cwd = process.cwd()) {
|
|
|
401
582
|
targetDir: target.dir,
|
|
402
583
|
contextFile: target.contextFile
|
|
403
584
|
};
|
|
404
|
-
const filePath =
|
|
405
|
-
|
|
585
|
+
const filePath = path4.join(cwd, CONFIG_FILE);
|
|
586
|
+
fs4.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
406
587
|
}
|
|
407
588
|
function loadConfig(cwd = process.cwd()) {
|
|
408
|
-
const filePath =
|
|
409
|
-
if (!
|
|
589
|
+
const filePath = path4.join(cwd, CONFIG_FILE);
|
|
590
|
+
if (!fs4.existsSync(filePath)) return null;
|
|
410
591
|
try {
|
|
411
|
-
const raw =
|
|
592
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
412
593
|
const config = JSON.parse(raw);
|
|
413
594
|
return resolveTarget(config.target, config.targetDir, config.contextFile);
|
|
414
595
|
} catch {
|
|
@@ -611,7 +792,7 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
|
|
|
611
792
|
agentSlugs.map((slug) => getAgent(slug))
|
|
612
793
|
);
|
|
613
794
|
const skillResults = await Promise.allSettled(
|
|
614
|
-
skillSlugs.map((slug) =>
|
|
795
|
+
skillSlugs.map((slug) => getSkillWithFiles(slug))
|
|
615
796
|
);
|
|
616
797
|
const agentInfos = [];
|
|
617
798
|
const agentsWithSkills = [];
|
|
@@ -657,8 +838,9 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
|
|
|
657
838
|
const slug = skillSlugs[i];
|
|
658
839
|
const result = skillResults[i];
|
|
659
840
|
if (result.status === "fulfilled") {
|
|
660
|
-
|
|
661
|
-
|
|
841
|
+
writeSkillDir(target, slug, result.value.files);
|
|
842
|
+
const fileCount = result.value.files.length;
|
|
843
|
+
console.log(pc3.green(` \u2713 Skill: ${slug} (${fileCount} file${fileCount !== 1 ? "s" : ""})`));
|
|
662
844
|
} else {
|
|
663
845
|
console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
|
|
664
846
|
}
|
|
@@ -685,6 +867,106 @@ function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillS
|
|
|
685
867
|
});
|
|
686
868
|
}
|
|
687
869
|
|
|
870
|
+
// src/commands/marketplace.ts
|
|
871
|
+
import pc4 from "picocolors";
|
|
872
|
+
var DEFAULT_API_URL = "https://loom.voidcorp.io";
|
|
873
|
+
function padEnd2(str, len) {
|
|
874
|
+
return str + " ".repeat(Math.max(0, len - str.length));
|
|
875
|
+
}
|
|
876
|
+
function truncate2(str, max) {
|
|
877
|
+
if (str.length <= max) return str;
|
|
878
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
879
|
+
}
|
|
880
|
+
function getApiUrl() {
|
|
881
|
+
return process.env.LOOM_API_URL ?? DEFAULT_API_URL;
|
|
882
|
+
}
|
|
883
|
+
async function marketplaceSearchCommand(query, opts) {
|
|
884
|
+
try {
|
|
885
|
+
const url = new URL("/api/cli/marketplace", getApiUrl());
|
|
886
|
+
if (query) url.searchParams.set("q", query);
|
|
887
|
+
if (opts?.type) url.searchParams.set("type", opts.type);
|
|
888
|
+
if (opts?.sort) url.searchParams.set("sort", opts.sort);
|
|
889
|
+
const res = await fetch(url);
|
|
890
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
891
|
+
const data = await res.json();
|
|
892
|
+
if (data.items.length === 0) {
|
|
893
|
+
console.log(pc4.dim("\n No results found.\n"));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
console.log(
|
|
897
|
+
pc4.bold(pc4.cyan(`
|
|
898
|
+
Marketplace${query ? ` \u2014 "${query}"` : ""}`))
|
|
899
|
+
);
|
|
900
|
+
console.log(pc4.dim(" " + "\u2500".repeat(70)));
|
|
901
|
+
for (const item of data.items) {
|
|
902
|
+
const type = pc4.dim(`[${item.type}]`);
|
|
903
|
+
const installs = pc4.dim(`\u2193${item.installCount}`);
|
|
904
|
+
const author = item.authorName ? pc4.dim(`by ${item.authorName}`) : "";
|
|
905
|
+
console.log(
|
|
906
|
+
` ${padEnd2(pc4.green(item.slug), 25)} ${padEnd2(type, 14)} ${padEnd2(truncate2(item.title, 25), 27)} ${installs} ${author}`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
console.log(
|
|
910
|
+
pc4.dim(
|
|
911
|
+
`
|
|
912
|
+
Install with: ${pc4.reset("loom marketplace install <slug>")}
|
|
913
|
+
`
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (error instanceof Error) {
|
|
918
|
+
console.error(pc4.red(`
|
|
919
|
+
\u2717 ${error.message}
|
|
920
|
+
`));
|
|
921
|
+
} else {
|
|
922
|
+
console.error(pc4.red("\n \u2717 Could not reach the marketplace.\n"));
|
|
923
|
+
}
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function marketplaceInstallCommand(slug) {
|
|
928
|
+
try {
|
|
929
|
+
const url = new URL("/api/cli/marketplace/install", getApiUrl());
|
|
930
|
+
const res = await fetch(url, {
|
|
931
|
+
method: "POST",
|
|
932
|
+
headers: { "Content-Type": "application/json" },
|
|
933
|
+
body: JSON.stringify({ slug })
|
|
934
|
+
});
|
|
935
|
+
if (!res.ok) {
|
|
936
|
+
const body = await res.json().catch(() => ({}));
|
|
937
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
938
|
+
}
|
|
939
|
+
const data = await res.json();
|
|
940
|
+
const r = data.resource;
|
|
941
|
+
if (r.type === "agent") {
|
|
942
|
+
saveLocalAgent(r.slug, r.content);
|
|
943
|
+
} else if (r.type === "skill") {
|
|
944
|
+
const files = r.files?.length ? r.files.map((f) => ({ relativePath: f.relativePath, content: f.content })) : [{ relativePath: "SKILL.md", content: r.content }];
|
|
945
|
+
saveLocalSkill(r.slug, files);
|
|
946
|
+
} else if (r.type === "preset") {
|
|
947
|
+
saveLocalPreset(r.slug, r.content);
|
|
948
|
+
}
|
|
949
|
+
console.log(
|
|
950
|
+
pc4.green(`
|
|
951
|
+
\u2713 Installed "${r.title}" (${r.type}) to ~/.loom/library/
|
|
952
|
+
`)
|
|
953
|
+
);
|
|
954
|
+
console.log(
|
|
955
|
+
pc4.dim(` Use it: loom add ${r.type} ${r.slug}
|
|
956
|
+
`)
|
|
957
|
+
);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (error instanceof Error) {
|
|
960
|
+
console.error(pc4.red(`
|
|
961
|
+
\u2717 ${error.message}
|
|
962
|
+
`));
|
|
963
|
+
} else {
|
|
964
|
+
console.error(pc4.red("\n \u2717 Installation failed.\n"));
|
|
965
|
+
}
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
688
970
|
// src/index.ts
|
|
689
971
|
var require2 = createRequire(import.meta.url);
|
|
690
972
|
var { version } = require2("../package.json");
|
|
@@ -726,4 +1008,13 @@ program.command("init").description("Initialize a project with a preset (agents
|
|
|
726
1008
|
targetExplicit
|
|
727
1009
|
});
|
|
728
1010
|
});
|
|
1011
|
+
var mp = program.command("marketplace").alias("mp").description("Browse and install community resources");
|
|
1012
|
+
mp.command("search").description("Search the marketplace").argument("[query]", "Search query").option("--type <type>", "Filter by type: agent, skill, preset").option("--sort <sort>", "Sort: popular, recent", "popular").action(
|
|
1013
|
+
async (query, opts) => {
|
|
1014
|
+
await marketplaceSearchCommand(query, opts);
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
mp.command("install").description("Install a resource from the marketplace").argument("<slug>", "Resource slug to install").action(async (slug) => {
|
|
1018
|
+
await marketplaceInstallCommand(slug);
|
|
1019
|
+
});
|
|
729
1020
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@folpe/loom",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI to scaffold Claude Code projects with curated agents, skills, and presets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"dist",
|
|
30
30
|
"data"
|
|
31
31
|
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"prebuild": "node scripts/sync-library.js",
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch"
|
|
36
|
+
},
|
|
32
37
|
"dependencies": {
|
|
33
38
|
"@clack/prompts": "^1.0.1",
|
|
34
39
|
"commander": "^13.1.0",
|
|
@@ -43,10 +48,5 @@
|
|
|
43
48
|
"@types/node": "^20",
|
|
44
49
|
"tsup": "^8.4.0",
|
|
45
50
|
"typescript": "^5"
|
|
46
|
-
},
|
|
47
|
-
"scripts": {
|
|
48
|
-
"prebuild": "node scripts/sync-library.js",
|
|
49
|
-
"build": "tsup",
|
|
50
|
-
"dev": "tsup --watch"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|