@folpe/loom 0.1.0 → 0.3.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 +82 -16
- package/data/agents/backend/AGENT.md +12 -0
- package/data/agents/database/AGENT.md +3 -0
- package/data/agents/frontend/AGENT.md +10 -0
- package/data/agents/marketing/AGENT.md +3 -0
- package/data/agents/orchestrator/AGENT.md +1 -15
- package/data/agents/security/AGENT.md +3 -0
- package/data/agents/tests/AGENT.md +2 -0
- package/data/agents/ux-ui/AGENT.md +5 -0
- package/data/presets/api-backend.yaml +37 -0
- package/data/presets/chrome-extension.yaml +36 -0
- package/data/presets/cli-tool.yaml +31 -0
- package/data/presets/e-commerce.yaml +49 -0
- package/data/presets/expo-mobile.yaml +41 -0
- package/data/presets/fullstack-auth.yaml +45 -0
- package/data/presets/landing-page.yaml +38 -0
- package/data/presets/mvp-lean.yaml +35 -0
- package/data/presets/saas-default.yaml +7 -11
- package/data/presets/saas-full.yaml +71 -0
- package/data/skills/api-design/SKILL.md +149 -0
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +105 -0
- package/data/skills/cli-development/SKILL.md +147 -0
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +87 -0
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +106 -0
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +126 -0
- package/data/skills/stripe-integration/SKILL.md +96 -0
- package/data/skills/supabase-patterns/SKILL.md +110 -0
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +179 -0
- package/dist/index.js +254 -100
- package/package.json +2 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-ux-guidelines
|
|
3
|
+
description: "UI/UX design rules for accessibility, interaction, typography, color, and animation. Use when building user-facing interfaces, reviewing designs, checking accessibility compliance, or choosing fonts and color palettes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# UI/UX Design Guidelines
|
|
7
|
+
|
|
8
|
+
Comprehensive design guide covering accessibility, interaction, layout, typography, color, and animation — prioritized by impact.
|
|
9
|
+
|
|
10
|
+
## Critical Rules
|
|
11
|
+
|
|
12
|
+
- **Color contrast**: minimum 4.5:1 for normal text, 3:1 for large text.
|
|
13
|
+
- **Touch targets**: minimum 44x44px on mobile.
|
|
14
|
+
- **No `h-screen`** — use `h-dvh` for correct mobile viewport.
|
|
15
|
+
- **No animation unless requested** — respect `prefers-reduced-motion`.
|
|
16
|
+
- **Empty states must have one clear next action** — never blank screens.
|
|
17
|
+
- **All icon-only buttons must have `aria-label`**.
|
|
18
|
+
|
|
19
|
+
## Priority 1 — Accessibility (CRITICAL)
|
|
20
|
+
|
|
21
|
+
- **Color is not enough**: never convey information by color alone — add icons or text.
|
|
22
|
+
- **Alt text**: descriptive alt for all meaningful images. Use `alt=""` only for decorative images.
|
|
23
|
+
- **Heading hierarchy**: sequential h1 → h2 → h3, one `<h1>` per page.
|
|
24
|
+
- **Keyboard navigation**: tab order matches visual order, no keyboard traps.
|
|
25
|
+
- **Focus states**: visible focus rings on all interactive elements — never `outline-none` without replacement.
|
|
26
|
+
- **Form labels**: every input must have a visible `<label>` — placeholder alone is never enough.
|
|
27
|
+
- **Error announcements**: use `aria-live="polite"` or `role="alert"` for dynamic errors.
|
|
28
|
+
- **Skip links**: provide "Skip to main content" on navigation-heavy pages.
|
|
29
|
+
- **Motion sensitivity**: always respect `prefers-reduced-motion` — disable parallax and scroll-jacking.
|
|
30
|
+
|
|
31
|
+
## Priority 2 — Touch & Interaction (CRITICAL)
|
|
32
|
+
|
|
33
|
+
- **Touch spacing**: minimum 8px gap between adjacent touch targets.
|
|
34
|
+
- **Hover vs tap**: never rely on hover for primary interactions — use click/tap.
|
|
35
|
+
- **Cursor pointer**: add `cursor-pointer` to all clickable elements.
|
|
36
|
+
- **Focus states**: `focus:ring-2 focus:ring-primary` on interactive elements.
|
|
37
|
+
- **Hover states**: `hover:bg-accent cursor-pointer` for visual feedback.
|
|
38
|
+
- **Active states**: `active:scale-95` for press feedback.
|
|
39
|
+
- **Disabled states**: `opacity-50 cursor-not-allowed pointer-events-none`.
|
|
40
|
+
- **Loading buttons**: disable button and show spinner during async actions — prevent double submission.
|
|
41
|
+
- **Error feedback**: show clear error message near the problem — never silent failures.
|
|
42
|
+
- **Success feedback**: always confirm completed actions with toast or visual change.
|
|
43
|
+
- **Confirmation dialogs**: require confirmation before any destructive/irreversible action.
|
|
44
|
+
|
|
45
|
+
## Priority 3 — Layout & Responsive (HIGH)
|
|
46
|
+
|
|
47
|
+
- **Mobile first**: base styles for mobile, `md:` and `lg:` for larger screens.
|
|
48
|
+
- **Viewport**: always set `<meta name="viewport" content="width=device-width, initial-scale=1">`.
|
|
49
|
+
- **No horizontal scroll**: ensure all content fits viewport — `max-w-full overflow-x-hidden`.
|
|
50
|
+
- **Viewport units**: use `min-h-dvh` not `h-screen` — mobile browser chrome breaks `100vh`.
|
|
51
|
+
- **Container width**: limit text to 65-75 characters per line — `max-w-prose`.
|
|
52
|
+
- **Content jumping**: reserve space for async content — use `aspect-ratio` or fixed dimensions.
|
|
53
|
+
- **Z-index scale**: use a fixed scale (10, 20, 30, 50) — never `z-[9999]`.
|
|
54
|
+
- **Fixed elements**: account for safe areas, never stack multiple fixed elements carelessly.
|
|
55
|
+
- **Breakpoint testing**: always test at 375px, 768px, 1024px, 1440px.
|
|
56
|
+
- **Image scaling**: `max-w-full h-auto` on all images.
|
|
57
|
+
- **Table handling**: wrap tables in `overflow-x-auto` for mobile.
|
|
58
|
+
|
|
59
|
+
## Priority 4 — Typography (MEDIUM)
|
|
60
|
+
|
|
61
|
+
- **Body font size**: minimum 16px (`text-base`) on mobile — never `text-xs` for body.
|
|
62
|
+
- **Line height**: 1.5–1.75 for body text — `leading-relaxed`.
|
|
63
|
+
- **Line length**: 65-75 characters max — `max-w-prose` or `max-w-3xl`.
|
|
64
|
+
- **Heading hierarchy**: clear size/weight difference from body text.
|
|
65
|
+
- **Font loading**: use `font-display: swap` with similar fallback to prevent layout shift.
|
|
66
|
+
- **Numeric data**: use `tabular-nums` for aligned numbers in tables and dashboards.
|
|
67
|
+
- **Text wrapping**: use `text-balance` for headings, `text-pretty` for paragraphs.
|
|
68
|
+
- **Truncation**: `truncate` or `line-clamp-2` with expand option for long content.
|
|
69
|
+
|
|
70
|
+
### Recommended Font Pairings
|
|
71
|
+
|
|
72
|
+
| Style | Heading | Body | Best For |
|
|
73
|
+
|-------|---------|------|----------|
|
|
74
|
+
| Modern Professional | Poppins | Open Sans | SaaS, corporate, startups |
|
|
75
|
+
| Tech Startup | Space Grotesk | DM Sans | Tech, developer tools, AI |
|
|
76
|
+
| Minimal Swiss | Inter | Inter | Dashboards, admin panels, docs |
|
|
77
|
+
| Friendly SaaS | Plus Jakarta Sans | Plus Jakarta Sans | Web apps, productivity tools |
|
|
78
|
+
| Classic Elegant | Playfair Display | Inter | Luxury, fashion, editorial |
|
|
79
|
+
| Bold Statement | Bebas Neue | Source Sans 3 | Marketing, portfolios, agencies |
|
|
80
|
+
| Developer Mono | JetBrains Mono | IBM Plex Sans | Dev tools, documentation, CLI |
|
|
81
|
+
| Playful Creative | Fredoka | Nunito | Children's apps, gaming, education |
|
|
82
|
+
| Corporate Trust | Lexend | Source Sans 3 | Enterprise, government, healthcare |
|
|
83
|
+
|
|
84
|
+
## Priority 5 — Color Palettes by Product Type (MEDIUM)
|
|
85
|
+
|
|
86
|
+
| Product | Primary | CTA | Background | Text |
|
|
87
|
+
|---------|---------|-----|------------|------|
|
|
88
|
+
| SaaS (General) | `#2563EB` blue | `#F97316` orange | `#F8FAFC` | `#1E293B` |
|
|
89
|
+
| Micro SaaS | `#6366F1` indigo | `#10B981` emerald | `#F5F3FF` | `#1E1B4B` |
|
|
90
|
+
| E-commerce | `#059669` green | `#F97316` orange | `#ECFDF5` | `#064E3B` |
|
|
91
|
+
| E-commerce Luxury | `#1C1917` black | `#CA8A04` gold | `#FAFAF9` | `#0C0A09` |
|
|
92
|
+
| Landing Page | `#0EA5E9` sky | `#F97316` orange | `#F0F9FF` | `#0C4A6E` |
|
|
93
|
+
| Financial Dashboard | `#0F172A` navy | `#22C55E` green | `#020617` | `#F8FAFC` |
|
|
94
|
+
| Healthcare | `#0891B2` cyan | `#059669` green | `#ECFEFF` | `#164E63` |
|
|
95
|
+
| Education | `#4F46E5` indigo | `#F97316` orange | `#EEF2FF` | `#1E1B4B` |
|
|
96
|
+
| Creative Agency | `#EC4899` pink | `#06B6D4` cyan | `#FDF2F8` | `#831843` |
|
|
97
|
+
| Portfolio | `#18181B` zinc | `#2563EB` blue | `#FAFAFA` | `#09090B` |
|
|
98
|
+
| AI / Chatbot | `#7C3AED` purple | `#06B6D4` cyan | `#FAF5FF` | `#1E1B4B` |
|
|
99
|
+
| Productivity | `#0D9488` teal | `#F97316` orange | `#F0FDFA` | `#134E4A` |
|
|
100
|
+
|
|
101
|
+
> Use these as starting points. Always map to your design system's semantic tokens (`bg-primary`, `text-foreground`, etc.) rather than hardcoding hex values.
|
|
102
|
+
|
|
103
|
+
## Priority 6 — Animation (MEDIUM)
|
|
104
|
+
|
|
105
|
+
- **Only when needed**: never add animation unless explicitly requested.
|
|
106
|
+
- **Timing**: 150-300ms for micro-interactions — never exceed 500ms for UI.
|
|
107
|
+
- **Performance**: animate only `transform` and `opacity` — never `width`, `height`, `top`, `left`, `margin`, `padding`.
|
|
108
|
+
- **Easing**: `ease-out` for entering, `ease-in` for exiting — never `linear` for UI.
|
|
109
|
+
- **Continuous animation**: only for loading indicators — never for decorative elements.
|
|
110
|
+
- **Reduced motion**: always check `prefers-reduced-motion` and disable non-essential animation.
|
|
111
|
+
- **Maximum scope**: animate 1-2 key elements per view — never everything.
|
|
112
|
+
- **No layout shift**: hover states must not cause layout shift — use `transform` not size changes.
|
|
113
|
+
|
|
114
|
+
## Priority 7 — Forms (MEDIUM)
|
|
115
|
+
|
|
116
|
+
- **Labels**: always visible above or beside input — never placeholder-only.
|
|
117
|
+
- **Error placement**: show error directly below the related input, not at form top.
|
|
118
|
+
- **Inline validation**: validate on blur for most fields, not only on submit.
|
|
119
|
+
- **Input types**: use `email`, `tel`, `number`, `url` — not `text` for everything.
|
|
120
|
+
- **Autofill**: use `autocomplete` attribute properly — never `autocomplete="off"` everywhere.
|
|
121
|
+
- **Required fields**: mark clearly with asterisk `*` or "(required)" text.
|
|
122
|
+
- **Password visibility**: always provide a show/hide toggle.
|
|
123
|
+
- **Mobile keyboards**: use `inputmode="numeric"` for number-only inputs.
|
|
124
|
+
- **Submit feedback**: show loading state → success/error — never no feedback.
|
|
125
|
+
|
|
126
|
+
## Priority 8 — Feedback & Empty States (LOW-MEDIUM)
|
|
127
|
+
|
|
128
|
+
- **Loading indicators**: show spinner/skeleton for operations > 300ms — never frozen UI.
|
|
129
|
+
- **Empty states**: show helpful message + one clear action — never blank screen.
|
|
130
|
+
- **Error recovery**: provide clear next steps — "Try again" button + help link.
|
|
131
|
+
- **Progress indicators**: show "Step 2 of 4" for multi-step processes.
|
|
132
|
+
- **Toast notifications**: auto-dismiss after 3-5 seconds — never persistent.
|
|
133
|
+
- **Truncation**: handle long content gracefully with `line-clamp` + expand.
|
|
134
|
+
- **No results**: show suggestions when search yields nothing — never just "0 results".
|
|
135
|
+
|
|
136
|
+
## Pre-Delivery Checklist
|
|
137
|
+
|
|
138
|
+
### Visual Quality
|
|
139
|
+
- [ ] No emoji icons — use Lucide, Heroicons, or SF Symbols (SVG)
|
|
140
|
+
- [ ] Consistent icon set throughout the interface
|
|
141
|
+
- [ ] Hover states without layout shift
|
|
142
|
+
- [ ] Semantic color tokens used (not hardcoded hex)
|
|
143
|
+
- [ ] No purple/multicolor gradients unless explicitly requested
|
|
144
|
+
- [ ] No glow effects as primary affordances
|
|
145
|
+
- [ ] Empty states have one clear next action
|
|
146
|
+
|
|
147
|
+
### Interaction
|
|
148
|
+
- [ ] All clickable elements have `cursor-pointer`
|
|
149
|
+
- [ ] Clear visual hover feedback on interactive elements
|
|
150
|
+
- [ ] Smooth transitions (150-300ms)
|
|
151
|
+
- [ ] Visible keyboard focus states
|
|
152
|
+
- [ ] Destructive actions require `AlertDialog` confirmation
|
|
153
|
+
- [ ] Loading buttons disabled during async actions
|
|
154
|
+
|
|
155
|
+
### Light/Dark Mode
|
|
156
|
+
- [ ] Light text contrast: 4.5:1 minimum
|
|
157
|
+
- [ ] Borders visible in both modes
|
|
158
|
+
- [ ] Test both themes before delivery
|
|
159
|
+
|
|
160
|
+
### Layout
|
|
161
|
+
- [ ] No content hidden behind fixed navbars
|
|
162
|
+
- [ ] Responsive: 375px, 768px, 1024px, 1440px
|
|
163
|
+
- [ ] No horizontal scroll on mobile
|
|
164
|
+
- [ ] `min-h-dvh` used instead of `h-screen`
|
|
165
|
+
- [ ] `safe-area-inset` respected for fixed elements
|
|
166
|
+
|
|
167
|
+
### Accessibility
|
|
168
|
+
- [ ] All images have alt text
|
|
169
|
+
- [ ] All form inputs have labels
|
|
170
|
+
- [ ] Color is not the sole indicator of state
|
|
171
|
+
- [ ] `prefers-reduced-motion` respected
|
|
172
|
+
- [ ] Icon-only buttons have `aria-label`
|
|
173
|
+
- [ ] Skip link present on nav-heavy pages
|
|
174
|
+
|
|
175
|
+
### Performance
|
|
176
|
+
- [ ] Images optimized (WebP/AVIF, `srcset`, lazy loading)
|
|
177
|
+
- [ ] No `will-change` outside active animations
|
|
178
|
+
- [ ] No large `blur()` or `backdrop-filter` surfaces animated
|
|
179
|
+
- [ ] No `useEffect` for what can be render logic
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/commands/list.ts
|
|
@@ -31,14 +32,16 @@ async function listAgents() {
|
|
|
31
32
|
try {
|
|
32
33
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
33
34
|
const { data } = matter(raw);
|
|
35
|
+
const fm = data;
|
|
34
36
|
agents.push({
|
|
35
37
|
slug,
|
|
36
|
-
name:
|
|
37
|
-
description:
|
|
38
|
-
role:
|
|
38
|
+
name: fm.name || slug,
|
|
39
|
+
description: fm.description || "",
|
|
40
|
+
role: fm.role || "",
|
|
41
|
+
skills: Array.isArray(fm.skills) ? fm.skills : []
|
|
39
42
|
});
|
|
40
43
|
} catch {
|
|
41
|
-
agents.push({ slug, name: slug, description: "", role: "" });
|
|
44
|
+
agents.push({ slug, name: slug, description: "", role: "", skills: [] });
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
return agents;
|
|
@@ -146,10 +149,10 @@ async function listCommand(type) {
|
|
|
146
149
|
if (presets.length === 0) {
|
|
147
150
|
console.log(pc.dim(" No presets found."));
|
|
148
151
|
}
|
|
149
|
-
for (const
|
|
150
|
-
const meta = pc.dim(`(${
|
|
152
|
+
for (const p2 of presets) {
|
|
153
|
+
const meta = pc.dim(`(${p2.agentCount} agents, ${p2.skillCount} skills)`);
|
|
151
154
|
console.log(
|
|
152
|
-
` ${padEnd(pc.green(
|
|
155
|
+
` ${padEnd(pc.green(p2.slug), 30)} ${padEnd(p2.name, 25)} ${meta}`
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
}
|
|
@@ -193,6 +196,12 @@ function writeSkill(slug, content, cwd = process.cwd()) {
|
|
|
193
196
|
fs2.writeFileSync(filePath, content, "utf-8");
|
|
194
197
|
return filePath;
|
|
195
198
|
}
|
|
199
|
+
function writeOrchestrator(content, cwd = process.cwd()) {
|
|
200
|
+
const filePath = path2.join(cwd, CLAUDE_DIR, "orchestrator.md");
|
|
201
|
+
ensureDir(path2.dirname(filePath));
|
|
202
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
203
|
+
return filePath;
|
|
204
|
+
}
|
|
196
205
|
function writeClaudeMd(content, cwd = process.cwd()) {
|
|
197
206
|
const filePath = path2.join(cwd, "CLAUDE.md");
|
|
198
207
|
fs2.writeFileSync(filePath, content, "utf-8");
|
|
@@ -235,9 +244,11 @@ async function addCommand(type, slug) {
|
|
|
235
244
|
|
|
236
245
|
// src/commands/init.ts
|
|
237
246
|
import pc3 from "picocolors";
|
|
238
|
-
import
|
|
247
|
+
import * as p from "@clack/prompts";
|
|
248
|
+
import matter3 from "gray-matter";
|
|
239
249
|
|
|
240
250
|
// src/lib/generator.ts
|
|
251
|
+
import matter2 from "gray-matter";
|
|
241
252
|
function generateClaudeMd(preset, agents) {
|
|
242
253
|
const lines = [];
|
|
243
254
|
lines.push(`# ${preset.name}`);
|
|
@@ -246,8 +257,8 @@ function generateClaudeMd(preset, agents) {
|
|
|
246
257
|
lines.push("");
|
|
247
258
|
if (preset.constitution.principles.length > 0) {
|
|
248
259
|
lines.push("## Principles");
|
|
249
|
-
for (const
|
|
250
|
-
lines.push(`- ${
|
|
260
|
+
for (const p2 of preset.constitution.principles) {
|
|
261
|
+
lines.push(`- ${p2}`);
|
|
251
262
|
}
|
|
252
263
|
lines.push("");
|
|
253
264
|
}
|
|
@@ -279,100 +290,44 @@ function generateClaudeMd(preset, agents) {
|
|
|
279
290
|
}
|
|
280
291
|
lines.push("");
|
|
281
292
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
293
|
+
lines.push("## Orchestrator");
|
|
294
|
+
lines.push("");
|
|
295
|
+
lines.push("Use the orchestrator agent (`.claude/orchestrator.md`) as the main coordinator. It will analyze tasks, break them into subtasks, and delegate to the appropriate specialized agents listed above.");
|
|
296
|
+
lines.push("");
|
|
287
297
|
return lines.join("\n");
|
|
288
298
|
}
|
|
299
|
+
function generateOrchestrator(templateContent, agents, presetSkills) {
|
|
300
|
+
const { data: frontmatter, content } = matter2(templateContent);
|
|
301
|
+
const rules = [];
|
|
302
|
+
const delegatesTo = [];
|
|
303
|
+
for (const agent of agents) {
|
|
304
|
+
if (agent.slug === "orchestrator") continue;
|
|
305
|
+
delegatesTo.push(agent.slug);
|
|
306
|
+
const relevantSkills = agent.skills.filter((s) => presetSkills.includes(s));
|
|
307
|
+
let line = `- **${agent.slug}**: ${agent.description}`;
|
|
308
|
+
if (relevantSkills.length > 0) {
|
|
309
|
+
line += `. Skills: ${relevantSkills.join(", ")}`;
|
|
310
|
+
}
|
|
311
|
+
rules.push(line);
|
|
312
|
+
}
|
|
313
|
+
const newFrontmatter = { ...frontmatter, "delegates-to": delegatesTo };
|
|
314
|
+
const newContent = content.replace("{{DELEGATION_RULES}}", rules.join("\n"));
|
|
315
|
+
return matter2.stringify(newContent, newFrontmatter);
|
|
316
|
+
}
|
|
289
317
|
|
|
290
318
|
// src/commands/init.ts
|
|
291
|
-
async function initCommand(presetSlug) {
|
|
319
|
+
async function initCommand(presetSlug, opts = {}) {
|
|
292
320
|
try {
|
|
293
|
-
|
|
294
|
-
if (!presetSlug) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
console.error(pc3.red("\n No presets available.\n"));
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
console.log(pc3.bold(pc3.cyan("\n Available presets:\n")));
|
|
301
|
-
presets.forEach((p, i) => {
|
|
302
|
-
console.log(` ${pc3.bold(String(i + 1))}. ${pc3.green(p.slug)} \u2014 ${p.description}`);
|
|
303
|
-
});
|
|
304
|
-
const { createInterface } = await import("readline");
|
|
305
|
-
const rl = createInterface({
|
|
306
|
-
input: process.stdin,
|
|
307
|
-
output: process.stdout
|
|
308
|
-
});
|
|
309
|
-
const answer = await new Promise((resolve) => {
|
|
310
|
-
rl.question(pc3.cyan("\n Choose a preset (number): "), (ans) => {
|
|
311
|
-
rl.close();
|
|
312
|
-
resolve(ans.trim());
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
const index = parseInt(answer, 10) - 1;
|
|
316
|
-
if (isNaN(index) || index < 0 || index >= presets.length) {
|
|
317
|
-
console.error(pc3.red("\n Invalid selection.\n"));
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
presetSlug = presets[index].slug;
|
|
321
|
-
preset = await getPreset(presetSlug);
|
|
322
|
-
} else {
|
|
323
|
-
preset = await getPreset(presetSlug);
|
|
324
|
-
}
|
|
325
|
-
console.log(pc3.bold(pc3.cyan(`
|
|
326
|
-
Initializing preset "${preset.name}"...
|
|
327
|
-
`)));
|
|
328
|
-
const agentResults = await Promise.allSettled(
|
|
329
|
-
preset.agents.map((slug) => getAgent(slug))
|
|
330
|
-
);
|
|
331
|
-
const skillResults = await Promise.allSettled(
|
|
332
|
-
preset.skills.map((slug) => getSkill(slug))
|
|
333
|
-
);
|
|
334
|
-
const agentInfos = [];
|
|
335
|
-
for (let i = 0; i < preset.agents.length; i++) {
|
|
336
|
-
const slug = preset.agents[i];
|
|
337
|
-
const result = agentResults[i];
|
|
338
|
-
if (result.status === "fulfilled") {
|
|
339
|
-
writeAgent(slug, result.value.rawContent);
|
|
340
|
-
const { data } = matter2(result.value.rawContent);
|
|
341
|
-
const fm = data;
|
|
342
|
-
agentInfos.push({
|
|
343
|
-
slug,
|
|
344
|
-
name: fm.name || slug,
|
|
345
|
-
role: fm.role || ""
|
|
346
|
-
});
|
|
347
|
-
console.log(pc3.green(` \u2713 Agent: ${slug}`));
|
|
348
|
-
} else {
|
|
349
|
-
console.log(pc3.yellow(` \u26A0 Agent "${slug}" skipped: ${result.reason}`));
|
|
350
|
-
}
|
|
321
|
+
const hasFlags = !!(opts.addAgent || opts.removeAgent || opts.addSkill || opts.removeSkill);
|
|
322
|
+
if (!presetSlug && hasFlags) {
|
|
323
|
+
console.error(pc3.red("\n Error: flags require a preset argument. Usage: loom init <preset> [flags]\n"));
|
|
324
|
+
process.exit(1);
|
|
351
325
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
writeSkill(slug, result.value.rawContent);
|
|
357
|
-
console.log(pc3.green(` \u2713 Skill: ${slug}`));
|
|
358
|
-
} else {
|
|
359
|
-
console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
|
|
360
|
-
}
|
|
326
|
+
if (!presetSlug && !hasFlags) {
|
|
327
|
+
await interactiveInit();
|
|
328
|
+
} else {
|
|
329
|
+
await nonInteractiveInit(presetSlug, opts);
|
|
361
330
|
}
|
|
362
|
-
const claudeContent = generateClaudeMd(preset, agentInfos);
|
|
363
|
-
writeClaudeMd(claudeContent);
|
|
364
|
-
console.log(pc3.green(` \u2713 CLAUDE.md generated`));
|
|
365
|
-
const agentOk = agentResults.filter((r) => r.status === "fulfilled").length;
|
|
366
|
-
const skillOk = skillResults.filter((r) => r.status === "fulfilled").length;
|
|
367
|
-
console.log(
|
|
368
|
-
pc3.bold(
|
|
369
|
-
pc3.cyan(
|
|
370
|
-
`
|
|
371
|
-
Done! ${agentOk} agent(s), ${skillOk} skill(s), CLAUDE.md ready.
|
|
372
|
-
`
|
|
373
|
-
)
|
|
374
|
-
)
|
|
375
|
-
);
|
|
376
331
|
} catch (error) {
|
|
377
332
|
if (error instanceof Error) {
|
|
378
333
|
console.error(pc3.red(`
|
|
@@ -384,17 +339,216 @@ async function initCommand(presetSlug) {
|
|
|
384
339
|
process.exit(1);
|
|
385
340
|
}
|
|
386
341
|
}
|
|
342
|
+
async function interactiveInit() {
|
|
343
|
+
p.intro(pc3.bgCyan(pc3.black(" loom init ")));
|
|
344
|
+
const presets = await listPresets();
|
|
345
|
+
if (presets.length === 0) {
|
|
346
|
+
p.cancel("No presets available.");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const presetSlug = await p.select({
|
|
350
|
+
message: "Choose a preset",
|
|
351
|
+
options: presets.map((pr) => ({
|
|
352
|
+
value: pr.slug,
|
|
353
|
+
label: pr.name,
|
|
354
|
+
hint: `${pr.agentCount} agents, ${pr.skillCount} skills`
|
|
355
|
+
}))
|
|
356
|
+
});
|
|
357
|
+
if (p.isCancel(presetSlug)) {
|
|
358
|
+
p.cancel("Operation cancelled.");
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
const preset = await getPreset(presetSlug);
|
|
362
|
+
const allAgents = await listAgents();
|
|
363
|
+
const allSkillSlugs = (await listSkills()).map((s2) => s2.slug);
|
|
364
|
+
const nonOrchestratorAgents = allAgents.filter((a) => a.slug !== "orchestrator");
|
|
365
|
+
const presetAgentSet = new Set(preset.agents);
|
|
366
|
+
const selectedAgents = await p.multiselect({
|
|
367
|
+
message: "Select agents",
|
|
368
|
+
options: nonOrchestratorAgents.map((a) => ({
|
|
369
|
+
value: a.slug,
|
|
370
|
+
label: a.name,
|
|
371
|
+
hint: a.description
|
|
372
|
+
})),
|
|
373
|
+
initialValues: nonOrchestratorAgents.filter((a) => presetAgentSet.has(a.slug)).map((a) => a.slug),
|
|
374
|
+
required: true
|
|
375
|
+
});
|
|
376
|
+
if (p.isCancel(selectedAgents)) {
|
|
377
|
+
p.cancel("Operation cancelled.");
|
|
378
|
+
process.exit(0);
|
|
379
|
+
}
|
|
380
|
+
const agentSlugs = ["orchestrator", ...selectedAgents];
|
|
381
|
+
const skillOptions = computeAvailableSkills(preset, selectedAgents, allAgents, allSkillSlugs);
|
|
382
|
+
const selectedSkills = await p.multiselect({
|
|
383
|
+
message: "Select skills",
|
|
384
|
+
options: skillOptions.map((s2) => ({
|
|
385
|
+
value: s2.slug,
|
|
386
|
+
label: s2.slug,
|
|
387
|
+
hint: s2.preSelected ? "recommended" : void 0
|
|
388
|
+
})),
|
|
389
|
+
initialValues: skillOptions.filter((s2) => s2.preSelected).map((s2) => s2.slug),
|
|
390
|
+
required: false
|
|
391
|
+
});
|
|
392
|
+
if (p.isCancel(selectedSkills)) {
|
|
393
|
+
p.cancel("Operation cancelled.");
|
|
394
|
+
process.exit(0);
|
|
395
|
+
}
|
|
396
|
+
const skillSlugs = selectedSkills;
|
|
397
|
+
const confirmed = await p.confirm({
|
|
398
|
+
message: `Scaffold with ${agentSlugs.length} agents and ${skillSlugs.length} skills?`
|
|
399
|
+
});
|
|
400
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
401
|
+
p.cancel("Operation cancelled.");
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
const s = p.spinner();
|
|
405
|
+
s.start("Generating project files...");
|
|
406
|
+
await generateAndWrite(preset, agentSlugs, skillSlugs);
|
|
407
|
+
s.stop("Project files generated.");
|
|
408
|
+
p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), CLAUDE.md ready.`));
|
|
409
|
+
}
|
|
410
|
+
async function nonInteractiveInit(presetSlug, opts) {
|
|
411
|
+
const preset = await getPreset(presetSlug);
|
|
412
|
+
const allAgents = await listAgents();
|
|
413
|
+
let agentSlugs = [...preset.agents];
|
|
414
|
+
if (opts.addAgent) {
|
|
415
|
+
for (const slug of opts.addAgent) {
|
|
416
|
+
if (!agentSlugs.includes(slug)) agentSlugs.push(slug);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (opts.removeAgent) {
|
|
420
|
+
agentSlugs = agentSlugs.filter(
|
|
421
|
+
(s) => s === "orchestrator" || !opts.removeAgent.includes(s)
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const selectedNonOrch = agentSlugs.filter((s) => s !== "orchestrator");
|
|
425
|
+
const linkedToSelected = /* @__PURE__ */ new Set();
|
|
426
|
+
const linkedToRemoved = /* @__PURE__ */ new Set();
|
|
427
|
+
for (const agent of allAgents) {
|
|
428
|
+
if (selectedNonOrch.includes(agent.slug)) {
|
|
429
|
+
for (const sk of agent.skills) linkedToSelected.add(sk);
|
|
430
|
+
}
|
|
431
|
+
if (opts.removeAgent?.includes(agent.slug)) {
|
|
432
|
+
for (const sk of agent.skills) linkedToRemoved.add(sk);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const orphanSkills = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const sk of linkedToRemoved) {
|
|
437
|
+
if (!linkedToSelected.has(sk)) orphanSkills.add(sk);
|
|
438
|
+
}
|
|
439
|
+
let skillSlugs = preset.skills.filter((s) => !orphanSkills.has(s));
|
|
440
|
+
if (opts.addSkill) {
|
|
441
|
+
for (const slug of opts.addSkill) {
|
|
442
|
+
if (!skillSlugs.includes(slug)) skillSlugs.push(slug);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (opts.removeSkill) {
|
|
446
|
+
skillSlugs = skillSlugs.filter((s) => !opts.removeSkill.includes(s));
|
|
447
|
+
}
|
|
448
|
+
console.log(pc3.bold(pc3.cyan(`
|
|
449
|
+
Initializing preset "${preset.name}"...
|
|
450
|
+
`)));
|
|
451
|
+
await generateAndWrite(preset, agentSlugs, skillSlugs);
|
|
452
|
+
console.log(
|
|
453
|
+
pc3.bold(
|
|
454
|
+
pc3.cyan(
|
|
455
|
+
`
|
|
456
|
+
Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), CLAUDE.md ready.
|
|
457
|
+
`
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
async function generateAndWrite(preset, agentSlugs, skillSlugs) {
|
|
463
|
+
const agentResults = await Promise.allSettled(
|
|
464
|
+
agentSlugs.map((slug) => getAgent(slug))
|
|
465
|
+
);
|
|
466
|
+
const skillResults = await Promise.allSettled(
|
|
467
|
+
skillSlugs.map((slug) => getSkill(slug))
|
|
468
|
+
);
|
|
469
|
+
const agentInfos = [];
|
|
470
|
+
const agentsWithSkills = [];
|
|
471
|
+
let orchestratorTemplate = null;
|
|
472
|
+
for (let i = 0; i < agentSlugs.length; i++) {
|
|
473
|
+
const slug = agentSlugs[i];
|
|
474
|
+
const result = agentResults[i];
|
|
475
|
+
if (result.status === "fulfilled") {
|
|
476
|
+
const { data } = matter3(result.value.rawContent);
|
|
477
|
+
const fm = data;
|
|
478
|
+
if (slug === "orchestrator") {
|
|
479
|
+
orchestratorTemplate = result.value.rawContent;
|
|
480
|
+
} else {
|
|
481
|
+
writeAgent(slug, result.value.rawContent);
|
|
482
|
+
console.log(pc3.green(` \u2713 Agent: ${slug}`));
|
|
483
|
+
}
|
|
484
|
+
agentInfos.push({
|
|
485
|
+
slug,
|
|
486
|
+
name: fm.name || slug,
|
|
487
|
+
role: fm.role || ""
|
|
488
|
+
});
|
|
489
|
+
agentsWithSkills.push({
|
|
490
|
+
slug,
|
|
491
|
+
name: fm.name || slug,
|
|
492
|
+
description: fm.description || "",
|
|
493
|
+
skills: Array.isArray(fm.skills) ? fm.skills : []
|
|
494
|
+
});
|
|
495
|
+
} else {
|
|
496
|
+
console.log(pc3.yellow(` \u26A0 Agent "${slug}" skipped: ${result.reason}`));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (orchestratorTemplate) {
|
|
500
|
+
const orchestratorContent = generateOrchestrator(
|
|
501
|
+
orchestratorTemplate,
|
|
502
|
+
agentsWithSkills,
|
|
503
|
+
skillSlugs
|
|
504
|
+
);
|
|
505
|
+
writeOrchestrator(orchestratorContent);
|
|
506
|
+
console.log(pc3.green(` \u2713 orchestrator.md generated`));
|
|
507
|
+
}
|
|
508
|
+
for (let i = 0; i < skillSlugs.length; i++) {
|
|
509
|
+
const slug = skillSlugs[i];
|
|
510
|
+
const result = skillResults[i];
|
|
511
|
+
if (result.status === "fulfilled") {
|
|
512
|
+
writeSkill(slug, result.value.rawContent);
|
|
513
|
+
console.log(pc3.green(` \u2713 Skill: ${slug}`));
|
|
514
|
+
} else {
|
|
515
|
+
console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const claudeContent = generateClaudeMd(preset, agentInfos);
|
|
519
|
+
writeClaudeMd(claudeContent);
|
|
520
|
+
console.log(pc3.green(` \u2713 CLAUDE.md generated`));
|
|
521
|
+
}
|
|
522
|
+
function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillSlugs) {
|
|
523
|
+
const linkedToSelected = /* @__PURE__ */ new Set();
|
|
524
|
+
for (const agent of allAgents) {
|
|
525
|
+
if (selectedAgentSlugs.includes(agent.slug)) {
|
|
526
|
+
for (const sk of agent.skills) linkedToSelected.add(sk);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const linkedToAny = /* @__PURE__ */ new Set();
|
|
530
|
+
for (const agent of allAgents) {
|
|
531
|
+
for (const sk of agent.skills) linkedToAny.add(sk);
|
|
532
|
+
}
|
|
533
|
+
const presetSkillSet = new Set(preset.skills);
|
|
534
|
+
return allSkillSlugs.map((slug) => {
|
|
535
|
+
const preSelected = linkedToSelected.has(slug) || presetSkillSet.has(slug) && !linkedToAny.has(slug);
|
|
536
|
+
return { slug, preSelected };
|
|
537
|
+
});
|
|
538
|
+
}
|
|
387
539
|
|
|
388
540
|
// src/index.ts
|
|
541
|
+
var require2 = createRequire(import.meta.url);
|
|
542
|
+
var { version } = require2("../package.json");
|
|
389
543
|
var program = new Command();
|
|
390
|
-
program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version(
|
|
544
|
+
program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version(version);
|
|
391
545
|
program.command("list").description("List available agents, skills, and presets").argument("[type]", "Filter by type: agents, skills, or presets").action(async (type) => {
|
|
392
546
|
await listCommand(type);
|
|
393
547
|
});
|
|
394
548
|
program.command("add").description("Download an agent or skill from the library").argument("<type>", "Type: agent or skill").argument("<slug>", "Slug of the agent or skill").action(async (type, slug) => {
|
|
395
549
|
await addCommand(type, slug);
|
|
396
550
|
});
|
|
397
|
-
program.command("init").description("Initialize a project with a preset (agents + skills + CLAUDE.md)").argument("[preset]", "Preset slug (interactive if omitted)").action(async (preset) => {
|
|
398
|
-
await initCommand(preset);
|
|
551
|
+
program.command("init").description("Initialize a project with a preset (agents + skills + CLAUDE.md)").argument("[preset]", "Preset slug (interactive if omitted)").option("--add-agent <slugs...>", "Add extra agents").option("--remove-agent <slugs...>", "Remove agents from preset").option("--add-skill <slugs...>", "Add extra skills").option("--remove-skill <slugs...>", "Remove skills from preset").action(async (preset, opts) => {
|
|
552
|
+
await initCommand(preset, opts);
|
|
399
553
|
});
|
|
400
554
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@folpe/loom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI to scaffold Claude Code projects with curated agents, skills, and presets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"data"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@clack/prompts": "^1.0.1",
|
|
33
34
|
"commander": "^13.1.0",
|
|
34
35
|
"gray-matter": "^4.0.3",
|
|
35
36
|
"picocolors": "^1.1.1",
|