smash-os-install 0.1.1 → 0.2.2

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.
Files changed (4) hide show
  1. package/install.mjs +671 -0
  2. package/package.json +6 -8
  3. package/README.md +0 -47
  4. package/index.js +0 -206
package/install.mjs ADDED
@@ -0,0 +1,671 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * smash-os-install — Install the SmashOS Claude Code harness into any repo.
4
+ *
5
+ * Usage (inside your repo root):
6
+ * npx smash-os-install — connected mode (requires SmashOS web app)
7
+ * npx smash-os-install --local — local-only mode (no web app, no API keys)
8
+ *
9
+ * What it does (connected):
10
+ * 1. Prompts for your SmashOS URL, Repo ID, and API key
11
+ * 2. Validates credentials against the SmashOS API
12
+ * 3. Fetches the generated harness files for your repo
13
+ * 4. Writes CLAUDE.md, .claude/hooks/, .claude/skills/, .env.smash-os
14
+ * 5. Merges hooks into existing .claude/settings.json (if present)
15
+ * 6. Adds .env.smash-os to .gitignore
16
+ *
17
+ * What it does (--local):
18
+ * 1. Prompts for project name and tech stack only
19
+ * 2. Writes CLAUDE.md + /ai/ skeleton + 2 skills locally
20
+ * 3. No web app connection required
21
+ */
22
+
23
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
24
+ import { join, dirname, basename } from 'path';
25
+ import { homedir } from 'os';
26
+ import { execSync } from 'child_process';
27
+ import prompts from 'prompts';
28
+ import chalk from 'chalk';
29
+
30
+ const cwd = process.cwd();
31
+ const isLocal = process.argv.includes('--local');
32
+
33
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
34
+
35
+ function writeFile(relPath, content) {
36
+ const abs = join(cwd, relPath);
37
+ mkdirSync(dirname(abs), { recursive: true });
38
+ writeFileSync(abs, content, 'utf8');
39
+ }
40
+
41
+ function mergeSettingsJson(newSettingsContent) {
42
+ const settingsPath = join(cwd, '.claude', 'settings.json');
43
+
44
+ if (!existsSync(settingsPath)) {
45
+ writeFile('.claude/settings.json', newSettingsContent);
46
+ return false; // not merged, freshly created
47
+ }
48
+
49
+ let existing;
50
+ try {
51
+ existing = JSON.parse(readFileSync(settingsPath, 'utf8'));
52
+ } catch {
53
+ // Unreadable — overwrite
54
+ writeFile('.claude/settings.json', newSettingsContent);
55
+ return false;
56
+ }
57
+
58
+ const incoming = JSON.parse(newSettingsContent);
59
+ if (!existing.hooks) existing.hooks = {};
60
+
61
+ for (const [event, hookList] of Object.entries(incoming.hooks ?? {})) {
62
+ if (!existing.hooks[event]) existing.hooks[event] = [];
63
+ for (const hookGroup of hookList) {
64
+ for (const hook of hookGroup.hooks ?? []) {
65
+ const alreadyPresent = existing.hooks[event].some((g) =>
66
+ g.hooks?.some((h) => h.command === hook.command)
67
+ );
68
+ if (!alreadyPresent) {
69
+ existing.hooks[event].push({ hooks: [hook] });
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ writeFile('.claude/settings.json', JSON.stringify(existing, null, 2));
76
+ return true; // merged into existing
77
+ }
78
+
79
+ function ensureGitignore(entry) {
80
+ const gitignorePath = join(cwd, '.gitignore');
81
+ if (existsSync(gitignorePath)) {
82
+ const content = readFileSync(gitignorePath, 'utf8');
83
+ if (content.includes(entry)) return;
84
+ writeFileSync(gitignorePath, content.trimEnd() + '\n' + entry + '\n', 'utf8');
85
+ } else {
86
+ writeFileSync(gitignorePath, entry + '\n', 'utf8');
87
+ }
88
+ }
89
+
90
+ // ─── Main ─────────────────────────────────────────────────────────────────────
91
+
92
+ console.log('');
93
+ console.log(chalk.bold(' SmashOS Harness Installer'));
94
+ console.log(chalk.dim(isLocal ? ' Local-only mode — no web app required' : ' Installs the Claude Code harness into your repo'));
95
+ console.log('');
96
+
97
+ // ─── Local-only mode ──────────────────────────────────────────────────────────
98
+
99
+ if (isLocal) {
100
+ const onCancel = () => { console.log(chalk.yellow('\n Cancelled.')); process.exit(0); };
101
+
102
+ const baseAnswers = await prompts([
103
+ {
104
+ type: 'text',
105
+ name: 'projectName',
106
+ message: 'Project name',
107
+ initial: basename(cwd),
108
+ validate: (v) => (v.trim().length > 0 ? true : 'Required'),
109
+ },
110
+ ], { onCancel });
111
+
112
+ const { projectName } = baseAnswers;
113
+
114
+ console.log('');
115
+ console.log(chalk.bold(' Tech Stack'));
116
+ console.log(chalk.dim(' Select your tools — choose "Other" to enter a custom value'));
117
+ console.log('');
118
+
119
+ const O = '__other__';
120
+ const o = (prev) => prev === O ? 'text' : null;
121
+ const req = (v) => v.trim().length > 0 ? true : 'Required';
122
+
123
+ const stackAnswers = await prompts([
124
+ // ── Frontend Framework ─────────────────────────────────────────────────
125
+ {
126
+ type: 'select',
127
+ name: 'frontend',
128
+ message: 'Frontend framework',
129
+ choices: [
130
+ { title: 'Next.js', value: 'Next.js' },
131
+ { title: 'React Router v7', value: 'React Router v7' },
132
+ { title: 'Remix', value: 'Remix' },
133
+ { title: 'Vite + React (SPA)', value: 'Vite + React' },
134
+ { title: 'SvelteKit', value: 'SvelteKit' },
135
+ { title: 'Nuxt (Vue 3)', value: 'Nuxt' },
136
+ { title: 'Astro', value: 'Astro' },
137
+ { title: 'Angular', value: 'Angular' },
138
+ { title: 'Solid Start', value: 'Solid Start' },
139
+ { title: 'Other…', value: O },
140
+ ],
141
+ },
142
+ { type: o, name: 'frontendCustom', message: 'Specify your frontend framework', validate: req },
143
+
144
+ // ── Backend / API ──────────────────────────────────────────────────────
145
+ {
146
+ type: 'select',
147
+ name: 'backend',
148
+ message: 'Backend / API layer',
149
+ choices: [
150
+ { title: 'Fullstack (handled by framework)', value: 'Fullstack (framework handles API)' },
151
+ { title: 'Node.js + Express', value: 'Node.js + Express' },
152
+ { title: 'Node.js + Hono', value: 'Node.js + Hono' },
153
+ { title: 'Node.js + Fastify', value: 'Node.js + Fastify' },
154
+ { title: 'Node.js + NestJS', value: 'Node.js + NestJS' },
155
+ { title: 'Python + FastAPI', value: 'Python + FastAPI' },
156
+ { title: 'Python + Django / Flask', value: 'Python + Django/Flask' },
157
+ { title: 'Go (Gin / Chi)', value: 'Go' },
158
+ { title: 'Bun + Elysia', value: 'Bun + Elysia' },
159
+ { title: 'Other…', value: O },
160
+ ],
161
+ },
162
+ { type: o, name: 'backendCustom', message: 'Specify your backend/API layer', validate: req },
163
+
164
+ // ── Database ───────────────────────────────────────────────────────────
165
+ {
166
+ type: 'select',
167
+ name: 'database',
168
+ message: 'Database',
169
+ choices: [
170
+ { title: 'Supabase (Postgres)', value: 'Supabase (Postgres)' },
171
+ { title: 'Neon (Postgres)', value: 'Neon (Postgres)' },
172
+ { title: 'MongoDB Atlas', value: 'MongoDB Atlas' },
173
+ { title: 'PlanetScale / Vitess (MySQL)', value: 'PlanetScale (MySQL)' },
174
+ { title: 'Firebase Firestore', value: 'Firebase Firestore' },
175
+ { title: 'Turso (SQLite / libSQL)', value: 'Turso (SQLite)' },
176
+ { title: 'Prisma + PostgreSQL', value: 'Prisma + PostgreSQL' },
177
+ { title: 'Drizzle + D1 (SQLite)', value: 'Drizzle + Cloudflare D1' },
178
+ { title: 'Upstash Redis', value: 'Upstash Redis' },
179
+ { title: 'Other…', value: O },
180
+ ],
181
+ },
182
+ { type: o, name: 'databaseCustom', message: 'Specify your database', validate: req },
183
+
184
+ // ── Authentication ─────────────────────────────────────────────────────
185
+ {
186
+ type: 'select',
187
+ name: 'auth',
188
+ message: 'Authentication',
189
+ choices: [
190
+ { title: 'None / Custom', value: 'None' },
191
+ { title: 'Clerk', value: 'Clerk' },
192
+ { title: 'Supabase Auth', value: 'Supabase Auth' },
193
+ { title: 'Auth.js (NextAuth.js)', value: 'Auth.js (NextAuth)' },
194
+ { title: 'Better Auth', value: 'Better Auth' },
195
+ { title: 'Firebase Auth', value: 'Firebase Auth' },
196
+ { title: 'Auth0', value: 'Auth0' },
197
+ { title: 'Lucia Auth', value: 'Lucia Auth' },
198
+ { title: 'Kinde', value: 'Kinde' },
199
+ { title: 'Other…', value: O },
200
+ ],
201
+ },
202
+ { type: o, name: 'authCustom', message: 'Specify your auth provider', validate: req },
203
+
204
+ // ── Deployment ─────────────────────────────────────────────────────────
205
+ {
206
+ type: 'select',
207
+ name: 'deployment',
208
+ message: 'Deployment target',
209
+ choices: [
210
+ { title: 'Vercel', value: 'Vercel' },
211
+ { title: 'Cloudflare Workers / Pages', value: 'Cloudflare Workers/Pages' },
212
+ { title: 'Netlify', value: 'Netlify' },
213
+ { title: 'Railway', value: 'Railway' },
214
+ { title: 'Fly.io', value: 'Fly.io' },
215
+ { title: 'Render', value: 'Render' },
216
+ { title: 'AWS (Lambda / ECS)', value: 'AWS' },
217
+ { title: 'Google Cloud Run', value: 'Google Cloud Run' },
218
+ { title: 'Self-hosted / VPS', value: 'Self-hosted / VPS' },
219
+ { title: 'Other…', value: O },
220
+ ],
221
+ },
222
+ { type: o, name: 'deploymentCustom', message: 'Specify your deployment target', validate: req },
223
+
224
+ // ── Styling / UI ───────────────────────────────────────────────────────
225
+ {
226
+ type: 'select',
227
+ name: 'styling',
228
+ message: 'Styling / UI library',
229
+ choices: [
230
+ { title: 'Tailwind CSS', value: 'Tailwind CSS' },
231
+ { title: 'shadcn/ui + Tailwind', value: 'shadcn/ui + Tailwind CSS' },
232
+ { title: 'CSS Modules', value: 'CSS Modules' },
233
+ { title: 'Material UI (MUI)', value: 'Material UI (MUI)' },
234
+ { title: 'Chakra UI', value: 'Chakra UI' },
235
+ { title: 'Styled Components / Emotion', value: 'Styled Components' },
236
+ { title: 'Mantine', value: 'Mantine' },
237
+ { title: 'Radix UI + Tailwind', value: 'Radix UI + Tailwind CSS' },
238
+ { title: 'UnoCSS', value: 'UnoCSS' },
239
+ { title: 'Other…', value: O },
240
+ ],
241
+ },
242
+ { type: o, name: 'stylingCustom', message: 'Specify your styling/UI library', validate: req },
243
+ ], { onCancel });
244
+
245
+ function pick(val, custom) { return val === O ? (custom || 'Other') : val; }
246
+
247
+ const techStack = [
248
+ `- Frontend: ${pick(stackAnswers.frontend, stackAnswers.frontendCustom)}`,
249
+ `- Backend: ${pick(stackAnswers.backend, stackAnswers.backendCustom)}`,
250
+ `- Database: ${pick(stackAnswers.database, stackAnswers.databaseCustom)}`,
251
+ `- Auth: ${pick(stackAnswers.auth, stackAnswers.authCustom)}`,
252
+ `- Deployment: ${pick(stackAnswers.deployment, stackAnswers.deploymentCustom)}`,
253
+ `- Styling: ${pick(stackAnswers.styling, stackAnswers.stylingCustom)}`,
254
+ ].join('\n');
255
+
256
+ console.log('');
257
+ const today = new Date().toISOString().split('T')[0];
258
+
259
+ const claudeMd = `# ${projectName} — SmashOS Harness Active
260
+
261
+ You are operating inside the SmashOS AI Workflow Harness.
262
+ You are not a single assistant. You are a coordinated AI engineering organisation.
263
+
264
+ ## On Session Start
265
+ 1. Read \`ai/context/product.md\` (if it exists)
266
+ 2. Read \`ai/context/architecture.md\` (if it exists)
267
+ 3. Read \`ai/context/coding-standards.md\` (if it exists)
268
+ 4. Read \`ai/memory/decisions.md\` — last 20 entries (if it exists)
269
+ 5. Adopt the role: Staff Engineer
270
+
271
+ ## Cognitive Mode Rules
272
+ THINKING → Staff Engineer, Product Manager (no file writes)
273
+ EXECUTION → Senior Developer, DevOps (no specs or decisions)
274
+ VALIDATION → Security Engineer, QA Engineer (no production code)
275
+
276
+ ## Golden Rules
277
+ - Never skip architecture review
278
+ - Never write code without an approved spec
279
+ - Never deploy without QA + Security validation
280
+ - Output structured results only
281
+ - Save decisions to memory after every significant choice
282
+
283
+ ## Slash Commands
284
+ - /smash-os:role [name] — switch cognitive mode + load role definition
285
+
286
+ ## Tech Stack
287
+ ${techStack}
288
+ `;
289
+
290
+ const aiFiles = {
291
+ 'ai/orchestrator.md': `# ${projectName} — SmashOS Orchestrator
292
+ **Generated:** ${today}
293
+ **Version:** 1.0
294
+
295
+ ---
296
+
297
+ ## Overview
298
+
299
+ You are operating inside the SmashOS AI Workflow Harness for **${projectName}**.
300
+ You are not a single assistant. You are a coordinated AI engineering organisation.
301
+
302
+ This orchestrator.md is your master boot document. Read it fully at the start of every session.
303
+
304
+ ---
305
+
306
+ ## On Session Start
307
+
308
+ 1. Read \`/ai/context/product.md\` — understand what you are building and for whom
309
+ 2. Read \`/ai/context/architecture.md\` — understand the technical system
310
+ 3. Read \`/ai/context/coding-standards.md\` — know the rules before touching any file
311
+ 4. Read \`/ai/memory/decisions.md\` (last 20 entries) — know what has been decided and why
312
+ 5. Adopt the role: Staff Engineer
313
+
314
+ ---
315
+
316
+ ## Cognitive Mode Rules
317
+
318
+ | Mode | Roles | Permitted | Forbidden |
319
+ |---|---|---|---|
320
+ | THINKING | Staff Engineer, Product Manager | Specs, decisions, architecture docs | File writes, commits |
321
+ | EXECUTION | Senior Developer, DevOps Engineer | Code, commits, branches, PRs | Specs, decisions |
322
+ | VALIDATION | Security Engineer, QA Engineer | Scores, test results, audit reports | Production code |
323
+
324
+ The orchestrator hard-blocks mode mixing. Never mix thinking and execution in the same phase.
325
+
326
+ ---
327
+
328
+ ## Golden Rules
329
+
330
+ 1. Never skip architecture review
331
+ 2. Never write code without an approved spec
332
+ 3. Never deploy without QA + Security validation
333
+ 4. Output structured results only — every phase output is a typed JSON object
334
+ 5. Save decisions to memory after every significant architectural choice
335
+ 6. Revenue risk outranks technical purity
336
+ 7. Block automation when stability drops below 40
337
+
338
+ ---
339
+
340
+ ## Active Role Definitions
341
+
342
+ | Role | Mode | Primary Skill |
343
+ |---|---|---|
344
+ | Staff Engineer | THINKING | architecture-review |
345
+ | Product Manager | THINKING | — |
346
+ | Senior Developer | EXECUTION | code-generation |
347
+ | Security Engineer | VALIDATION | security-audit |
348
+ | QA Engineer | VALIDATION | qa-validation |
349
+ | DevOps Engineer | EXECUTION | — |
350
+
351
+ Full role definitions: \`/ai/roles/{role-name}.md\`
352
+
353
+ ---
354
+
355
+ ## Selective Context Loading
356
+
357
+ Load only what your role requires. Never load the full context bundle.
358
+
359
+ | Role | Load |
360
+ |---|---|
361
+ | Product Manager | product.md + database.md + trigger payload |
362
+ | Staff Engineer | architecture.md + coding-standards.md + decisions.md + previous phase output |
363
+ | Senior Developer | architecture.md + coding-standards.md + approved spec |
364
+ | Security Engineer | database.md + auth section of architecture.md + file diff |
365
+ | QA Engineer | coding-standards.md + acceptance criteria + file diff |
366
+ | DevOps Engineer | architecture.md + PR metadata + score summary |
367
+
368
+ ---
369
+
370
+ ## Product Summary
371
+
372
+ *Context not yet generated. Fill in \`/ai/context/product.md\` with your project details.*
373
+
374
+ ---
375
+
376
+ ## Architecture Summary
377
+
378
+ *Context not yet generated. Fill in \`/ai/context/architecture.md\` with your architecture.*
379
+
380
+ ---
381
+
382
+ ## Coding Standards Summary
383
+
384
+ *Context not yet generated. Fill in \`/ai/context/coding-standards.md\` with your coding standards.*
385
+
386
+ ---
387
+
388
+ ## Database Summary
389
+
390
+ *Context not yet generated. Fill in \`/ai/context/database.md\` with your database details.*
391
+
392
+ ---
393
+
394
+ ## Memory
395
+
396
+ - Architecture decisions: \`/ai/memory/decisions.md\`
397
+ - Lessons learned: \`/ai/memory/lessons.md\`
398
+
399
+ After every completed session, append decisions and lessons to the relevant memory file.
400
+
401
+ ---
402
+
403
+ ## Workflow References
404
+
405
+ - Feature: \`/ai/workflows/feature.md\`
406
+ - Bug Fix: \`/ai/workflows/bug-fix.md\`
407
+ - Weekly Improvement: \`/ai/workflows/weekly-improvement.md\`
408
+
409
+ ---
410
+
411
+ *Generated by smash-os-install (local mode) — fill in the /ai/context/ files to activate full context.*
412
+ `,
413
+ 'ai/context/product.md': `# Product Context — ${projectName}\n\n## What is this project?\n<!-- Describe what this project does -->\n\n## Who uses it?\n<!-- Describe the users -->\n\n## Core goals\n<!-- List the main goals -->\n\n## Tech Stack\n${techStack}\n`,
414
+ 'ai/context/architecture.md': `# Architecture — ${projectName}\n\n## Overview\n<!-- Describe the high-level architecture -->\n\n## Key decisions\n<!-- List major architectural decisions and why they were made -->\n\n## Constraints\n<!-- List things that must not change -->\n`,
415
+ 'ai/context/coding-standards.md': `# Coding Standards — ${projectName}\n\n## Language & formatting\n<!-- ESLint config, prettier, etc. -->\n\n## Naming conventions\n<!-- Variables, files, functions -->\n\n## Patterns to follow\n<!-- e.g. always use server actions, never bypass RLS -->\n\n## Patterns to avoid\n<!-- e.g. no raw SQL, no any types -->\n`,
416
+ 'ai/memory/decisions.md': `# Decisions Log\n\n<!-- SmashOS writes here after each session. Format: -->\n<!-- ## YYYY-MM-DD — Decision title -->\n<!-- **Rationale:** why this was chosen -->\n<!-- **Outcome:** what changed -->\n`,
417
+ 'ai/memory/lessons.md': `# Lessons Learned\n\n<!-- SmashOS writes here after bugs and incidents. Format: -->\n<!-- ## YYYY-MM-DD — Lesson title -->\n<!-- **What happened:** -->\n<!-- **What to do differently:** -->\n`,
418
+ };
419
+
420
+ const localSkills = {
421
+ 'smash-os-role': `---\nname: smash-os-role\ndescription: Switch cognitive mode and load a SmashOS role definition. Use when the user says "act as X", "switch to X role", "be the X", or names a SmashOS role (Staff Engineer, Product Manager, Senior Developer, Security Engineer, QA Engineer, DevOps Engineer).\n---\n\n# /smash-os:role\n\nSwitch to the named role and adopt its cognitive mode.\n\n## Roles\n\n| Role | Mode | Allowed |\n|---|---|---|\n| Staff Engineer | THINKING | Architecture, decisions, reviews |\n| Product Manager | THINKING | Specs, user stories, acceptance criteria |\n| Senior Developer | EXECUTION | Writing code, editing files |\n| Security Engineer | VALIDATION | Security review only, no code changes |\n| QA Engineer | VALIDATION | Testing and verification only |\n| DevOps Engineer | EXECUTION | Infrastructure, deployment |\n\n## Steps\n1. Read the role name from the argument (default: Staff Engineer)\n2. Announce: "Switching to [Role] — [Mode] mode"\n3. Load \`ai/roles/<role-slug>.md\` if it exists\n4. Apply the cognitive mode restrictions for the rest of the session\n`,
422
+ 'smash-os-memory': `---\nname: smash-os-memory\ndescription: Show recent SmashOS decisions and lessons from the ai/memory/ folder. Use when the user says "show memory", "what decisions were made", "show lessons", or "smash-os memory".\n---\n\n# /smash-os:memory\n\nRead and display the local SmashOS memory files.\n\n## Steps\n1. Read \`ai/memory/decisions.md\` (last 20 entries)\n2. Read \`ai/memory/lessons.md\` (last 10 entries)\n3. Display them clearly — decisions first, then lessons\n4. If either file is empty or missing, say so\n`,
423
+ };
424
+
425
+ let written = 0;
426
+
427
+ writeFile('CLAUDE.md', claudeMd);
428
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white('CLAUDE.md'));
429
+ written++;
430
+
431
+ for (const [relPath, content] of Object.entries(aiFiles)) {
432
+ if (!existsSync(join(cwd, relPath))) {
433
+ writeFile(relPath, content);
434
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
435
+ written++;
436
+ } else {
437
+ console.log(' ' + chalk.dim('↷') + ' ' + chalk.dim(`${relPath} (already exists — skipped)`));
438
+ }
439
+ }
440
+
441
+ console.log('');
442
+ for (const [skillName, skillContent] of Object.entries(localSkills)) {
443
+ const skillDir = join(homedir(), '.claude', 'skills', skillName);
444
+ mkdirSync(skillDir, { recursive: true });
445
+ writeFileSync(join(skillDir, 'SKILL.md'), skillContent, 'utf8');
446
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(`/smash-os:${skillName.replace('smash-os-', '')}`) + chalk.dim(' → ~/.claude/skills/'));
447
+ written++;
448
+ }
449
+
450
+ console.log('');
451
+ console.log(chalk.bold.green(' SmashOS harness installed!') + chalk.dim(` (${written} files — local mode)`));
452
+ console.log('');
453
+ console.log(chalk.dim(' Open Claude Code in this directory and start working:'));
454
+ console.log(' ' + chalk.white(' claude .'));
455
+ console.log('');
456
+ console.log(chalk.dim(' Fill in the ai/context/ files with your project details,'));
457
+ console.log(chalk.dim(' then Claude Code will load them automatically every session.'));
458
+ console.log('');
459
+
460
+ // ─── MCP Installation ───────────────────────────────────────────────────────
461
+
462
+ const MCP_SERVERS = [
463
+ {
464
+ name: 'jcodemunch',
465
+ title: 'jcodemunch — code intelligence (search, navigate, refactor)',
466
+ cmd: 'claude mcp add jcodemunch --transport stdio -- uvx jcodemunch-mcp',
467
+ },
468
+ {
469
+ name: 'jdocmunch',
470
+ title: 'jdocmunch — documentation intelligence (index + search docs)',
471
+ cmd: 'claude mcp add jdocmunch --transport stdio -- uvx jdocmunch-mcp',
472
+ },
473
+ {
474
+ name: 'context7',
475
+ title: 'context7 — live library docs for any npm/pip package',
476
+ cmd: null, // built dynamically — requires API key prompt
477
+ promptKey: true,
478
+ },
479
+ {
480
+ name: 'chrome-devtools',
481
+ title: 'chrome-devtools — browser automation and debugging',
482
+ cmd: 'claude mcp add chrome-devtools --transport stdio -- npx -y chrome-devtools-mcp@latest',
483
+ },
484
+ ];
485
+
486
+ // Check which MCPs are already installed
487
+ let alreadyInstalled = new Set();
488
+ try {
489
+ const out = execSync('claude mcp list', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
490
+ for (const s of MCP_SERVERS) {
491
+ if (out.includes(s.name)) alreadyInstalled.add(s.name);
492
+ }
493
+ } catch { /* claude CLI not available — skip pre-check */ }
494
+
495
+ const available = MCP_SERVERS.filter(s => !alreadyInstalled.has(s.name));
496
+
497
+ if (alreadyInstalled.size > 0) {
498
+ console.log(chalk.dim(` Already installed: ${[...alreadyInstalled].join(', ')}`));
499
+ }
500
+
501
+ if (available.length > 0) {
502
+ console.log(chalk.bold(' MCP Servers'));
503
+ console.log(chalk.dim(' These extend Claude Code with code intelligence and browser tools.'));
504
+ console.log('');
505
+
506
+ const { mcpChoices } = await prompts({
507
+ type: 'multiselect',
508
+ name: 'mcpChoices',
509
+ message: 'Select MCP servers to install (space to toggle, enter to confirm)',
510
+ choices: available.map(s => ({ title: s.title, value: s.name, selected: true })),
511
+ hint: '- Space to select. Return to submit',
512
+ }, { onCancel });
513
+
514
+ if (mcpChoices && mcpChoices.length > 0) {
515
+ console.log('');
516
+
517
+ // Resolve context7 API key if selected
518
+ let context7Key = '';
519
+ if (mcpChoices.includes('context7')) {
520
+ const { key } = await prompts({
521
+ type: 'text',
522
+ name: 'key',
523
+ message: 'context7 API key (get one free at context7.com — leave blank to skip key)',
524
+ }, { onCancel });
525
+ context7Key = key || '';
526
+ }
527
+
528
+ for (const name of mcpChoices) {
529
+ const server = MCP_SERVERS.find(s => s.name === name);
530
+ let cmd = server.cmd;
531
+ if (name === 'context7') {
532
+ cmd = context7Key
533
+ ? `claude mcp add context7 --transport http https://mcp.context7.com/mcp -H "CONTEXT7_API_KEY: ${context7Key}"`
534
+ : 'claude mcp add context7 --transport http https://mcp.context7.com/mcp';
535
+ }
536
+ try {
537
+ execSync(cmd, { stdio: 'pipe' });
538
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(name) + chalk.dim(' installed'));
539
+ } catch (err) {
540
+ console.log(' ' + chalk.red('✗') + ' ' + chalk.white(name) + chalk.dim(` — ${err.message.split('\n')[0]}`));
541
+ console.log(' ' + chalk.dim(`Run manually: ${cmd}`));
542
+ }
543
+ }
544
+ console.log('');
545
+ }
546
+ }
547
+
548
+ process.exit(0);
549
+ }
550
+
551
+ // ─── Connected mode ────────────────────────────────────────────────────────────
552
+
553
+ const answers = await prompts(
554
+ [
555
+ {
556
+ type: 'text',
557
+ name: 'apiUrl',
558
+ message: 'SmashOS URL (where your SmashOS is deployed)',
559
+ initial: 'http://localhost:5173',
560
+ validate: (v) => (v.startsWith('http') ? true : 'Must be a valid URL'),
561
+ },
562
+ {
563
+ type: 'text',
564
+ name: 'repoId',
565
+ message: 'Repo ID (from the SmashOS dashboard URL: /dashboard/repos/<ID>)',
566
+ validate: (v) => (v.trim().length > 0 ? true : 'Required'),
567
+ },
568
+ {
569
+ type: 'password',
570
+ name: 'apiKey',
571
+ message: 'API Key (from SmashOS Settings → API Keys)',
572
+ validate: (v) => (v.trim().length > 0 ? true : 'Required'),
573
+ },
574
+ ],
575
+ {
576
+ onCancel: () => {
577
+ console.log(chalk.yellow('\n Cancelled.'));
578
+ process.exit(0);
579
+ },
580
+ }
581
+ );
582
+
583
+ const { apiUrl, repoId, apiKey } = answers;
584
+ const cleanUrl = apiUrl.replace(/\/$/, '');
585
+
586
+ // ─── Validate & fetch ─────────────────────────────────────────────────────────
587
+
588
+ process.stdout.write('\n ' + chalk.dim('Connecting to SmashOS…'));
589
+
590
+ let data;
591
+ try {
592
+ const resp = await fetch(`${cleanUrl}/api/repos/${repoId}/layer2-files`, {
593
+ headers: { Authorization: `Bearer ${apiKey}` },
594
+ });
595
+
596
+ if (resp.status === 401) {
597
+ console.log(chalk.red(' ✗'));
598
+ console.error(chalk.red('\n Invalid API key or Repo ID. Check SmashOS → Settings → API Keys.\n'));
599
+ process.exit(1);
600
+ }
601
+ if (resp.status === 404) {
602
+ console.log(chalk.red(' ✗'));
603
+ console.error(chalk.red('\n Repo not found. Check the Repo ID in your SmashOS dashboard URL.\n'));
604
+ process.exit(1);
605
+ }
606
+ if (!resp.ok) {
607
+ console.log(chalk.red(' ✗'));
608
+ console.error(chalk.red(`\n SmashOS returned ${resp.status}. Is the URL correct?\n`));
609
+ process.exit(1);
610
+ }
611
+
612
+ data = await resp.json();
613
+ } catch (err) {
614
+ console.log(chalk.red(' ✗'));
615
+ console.error(chalk.red(`\n Could not reach SmashOS at ${cleanUrl}\n ${err.message}\n`));
616
+ process.exit(1);
617
+ }
618
+
619
+ console.log(chalk.green(' ✓'));
620
+ console.log(' ' + chalk.dim(`Connected to SmashOS — repo: ${chalk.white(data.repo.name)}`));
621
+ console.log('');
622
+
623
+ // ─── Write files ──────────────────────────────────────────────────────────────
624
+
625
+ const { files } = data;
626
+ let written = 0;
627
+ let merged = false;
628
+
629
+ for (const [relPath, content] of Object.entries(files)) {
630
+ if (relPath === '.claude/settings.json') {
631
+ merged = mergeSettingsJson(content);
632
+ console.log(
633
+ ' ' + chalk.green('✓') + ' ' +
634
+ chalk.white('.claude/settings.json') +
635
+ chalk.dim(merged ? ' (merged with existing)' : ' (created)')
636
+ );
637
+ } else {
638
+ writeFile(relPath, content);
639
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
640
+ }
641
+ written++;
642
+ }
643
+
644
+ // ─── Write .env.smash-os (pre-filled) ────────────────────────────────────────
645
+
646
+ const envContent = [
647
+ '# SmashOS harness credentials — do not commit this file',
648
+ `SMASH_OS_API_URL=${cleanUrl}`,
649
+ `SMASH_OS_REPO_ID=${repoId}`,
650
+ `SMASH_OS_API_KEY=${apiKey}`,
651
+ ].join('\n') + '\n';
652
+
653
+ writeFile('.env.smash-os', envContent);
654
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white('.env.smash-os') + chalk.dim(' (pre-filled)'));
655
+
656
+ // ─── Gitignore ────────────────────────────────────────────────────────────────
657
+
658
+ ensureGitignore('.env.smash-os');
659
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.dim('.env.smash-os added to .gitignore'));
660
+
661
+ // ─── Done ─────────────────────────────────────────────────────────────────────
662
+
663
+ console.log('');
664
+ console.log(chalk.bold.green(' SmashOS harness installed!') + chalk.dim(` (${written} files)`));
665
+ console.log('');
666
+ console.log(chalk.dim(' Next steps:'));
667
+ console.log(chalk.dim(' 1. Open a Claude Code session in this directory'));
668
+ console.log(chalk.dim(' 2. The harness activates automatically on session start'));
669
+ console.log(chalk.dim(` 3. Use ${chalk.white('/smash-os:run')} to trigger a pipeline`));
670
+ console.log(chalk.dim(` 4. View results at: ${chalk.white(cleanUrl + '/dashboard/repos/' + repoId)}`));
671
+ console.log('');
package/package.json CHANGED
@@ -1,21 +1,19 @@
1
1
  {
2
2
  "name": "smash-os-install",
3
- "version": "0.1.1",
4
- "description": "Install the SmashOS Claude Code harness into any repository",
3
+ "version": "0.2.2",
4
+ "description": "Install the SmashOS Claude Code harness into any existing repo",
5
5
  "type": "module",
6
6
  "bin": {
7
- "smash-os-install": "./index.js"
7
+ "smash-os-install": "install.mjs"
8
8
  },
9
9
  "files": [
10
- "index.js",
11
- "README.md"
10
+ "install.mjs"
12
11
  ],
13
12
  "keywords": [
14
13
  "smash-os",
15
- "claude-code",
16
14
  "ai-workflow",
17
- "harness",
18
- "install"
15
+ "claude-code",
16
+ "harness"
19
17
  ],
20
18
  "license": "MIT",
21
19
  "engines": {
package/README.md DELETED
@@ -1,47 +0,0 @@
1
- # smash-os-install
2
-
3
- Install the [SmashOS](https://github.com/MrShifu01/SmashOS) Claude Code harness into any repository.
4
-
5
- ## Prerequisites
6
-
7
- 1. A running SmashOS instance (self-hosted or deployed)
8
- 2. Your repo registered in the SmashOS dashboard
9
- 3. An API key generated from SmashOS → Settings → API Keys
10
-
11
- ## Usage
12
-
13
- Run inside your repo root:
14
-
15
- ```bash
16
- npx smash-os-install
17
- ```
18
-
19
- The installer will prompt for:
20
- - **SmashOS URL** — where your SmashOS is deployed (e.g. `https://your-smash-os.vercel.app`)
21
- - **Repo ID** — from the SmashOS dashboard URL: `/dashboard/repos/<ID>`
22
- - **API Key** — from SmashOS → Settings → API Keys
23
-
24
- ## What gets installed
25
-
26
- ```
27
- CLAUDE.md ← harness instructions, loaded every Claude session
28
- .env.smash-os ← your credentials (gitignored automatically)
29
- .claude/settings.json ← SessionStart + Stop hooks
30
- .claude/hooks/smash-os-boot.mjs ← fetches live state from SmashOS on session start
31
- .claude/hooks/smash-os-sync.mjs ← posts session summary to SmashOS on session end
32
- .claude/skills/smash-os-status.md ← /smash-os:status command
33
- .claude/skills/smash-os-run.md ← /smash-os:run command
34
- .claude/skills/smash-os-memory.md ← /smash-os:memory command
35
- .claude/skills/smash-os-audit.md ← /smash-os:audit command
36
- .claude/skills/smash-os-role.md ← /smash-os:role command
37
- .claude/skills/smash-os-sync.md ← /smash-os:sync command
38
- ```
39
-
40
- If `.claude/settings.json` already exists, the hooks are merged — your existing config is preserved.
41
-
42
- ## After installation
43
-
44
- 1. Open a Claude Code session in this directory
45
- 2. The harness activates automatically on session start
46
- 3. Use `/smash-os:run feature` to trigger a pipeline
47
- 4. View results in the SmashOS dashboard
package/index.js DELETED
@@ -1,206 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * smash-os-install — Install the SmashOS Claude Code harness into any repo.
4
- *
5
- * Usage (inside your repo root):
6
- * npx smash-os-install
7
- *
8
- * What it does:
9
- * 1. Prompts for your SmashOS URL, Repo ID, and API key
10
- * 2. Validates credentials against the SmashOS API
11
- * 3. Fetches the generated harness files for your repo
12
- * 4. Writes CLAUDE.md, .claude/hooks/, .claude/skills/, .env.smash-os
13
- * 5. Merges hooks into existing .claude/settings.json (if present)
14
- * 6. Adds .env.smash-os to .gitignore
15
- */
16
-
17
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
18
- import { join, dirname } from 'path';
19
- import prompts from 'prompts';
20
- import chalk from 'chalk';
21
-
22
- const cwd = process.cwd();
23
-
24
- // ─── Helpers ──────────────────────────────────────────────────────────────────
25
-
26
- function writeFile(relPath, content) {
27
- const abs = join(cwd, relPath);
28
- mkdirSync(dirname(abs), { recursive: true });
29
- writeFileSync(abs, content, 'utf8');
30
- }
31
-
32
- function mergeSettingsJson(newSettingsContent) {
33
- const settingsPath = join(cwd, '.claude', 'settings.json');
34
-
35
- if (!existsSync(settingsPath)) {
36
- writeFile('.claude/settings.json', newSettingsContent);
37
- return false; // not merged, freshly created
38
- }
39
-
40
- let existing;
41
- try {
42
- existing = JSON.parse(readFileSync(settingsPath, 'utf8'));
43
- } catch {
44
- // Unreadable — overwrite
45
- writeFile('.claude/settings.json', newSettingsContent);
46
- return false;
47
- }
48
-
49
- const incoming = JSON.parse(newSettingsContent);
50
- if (!existing.hooks) existing.hooks = {};
51
-
52
- for (const [event, hookList] of Object.entries(incoming.hooks ?? {})) {
53
- if (!existing.hooks[event]) existing.hooks[event] = [];
54
- for (const hookGroup of hookList) {
55
- for (const hook of hookGroup.hooks ?? []) {
56
- const alreadyPresent = existing.hooks[event].some((g) =>
57
- g.hooks?.some((h) => h.command === hook.command)
58
- );
59
- if (!alreadyPresent) {
60
- existing.hooks[event].push({ hooks: [hook] });
61
- }
62
- }
63
- }
64
- }
65
-
66
- writeFile('.claude/settings.json', JSON.stringify(existing, null, 2));
67
- return true; // merged into existing
68
- }
69
-
70
- function ensureGitignore(entry) {
71
- const gitignorePath = join(cwd, '.gitignore');
72
- if (existsSync(gitignorePath)) {
73
- const content = readFileSync(gitignorePath, 'utf8');
74
- if (content.includes(entry)) return;
75
- writeFileSync(gitignorePath, content.trimEnd() + '\n' + entry + '\n', 'utf8');
76
- } else {
77
- writeFileSync(gitignorePath, entry + '\n', 'utf8');
78
- }
79
- }
80
-
81
- // ─── Main ─────────────────────────────────────────────────────────────────────
82
-
83
- console.log('');
84
- console.log(chalk.bold(' SmashOS Harness Installer'));
85
- console.log(chalk.dim(' Installs the Claude Code harness into your repo'));
86
- console.log('');
87
-
88
- const answers = await prompts(
89
- [
90
- {
91
- type: 'text',
92
- name: 'apiUrl',
93
- message: 'SmashOS URL (where your SmashOS is deployed)',
94
- initial: 'http://localhost:5173',
95
- validate: (v) => (v.startsWith('http') ? true : 'Must be a valid URL'),
96
- },
97
- {
98
- type: 'text',
99
- name: 'repoId',
100
- message: 'Repo ID (from the SmashOS dashboard URL: /dashboard/repos/<ID>)',
101
- validate: (v) => (v.trim().length > 0 ? true : 'Required'),
102
- },
103
- {
104
- type: 'text',
105
- name: 'apiKey',
106
- message: 'API Key (from SmashOS Settings → API Keys)',
107
- validate: (v) => (v.trim().length > 0 ? true : 'Required'),
108
- },
109
- ],
110
- {
111
- onCancel: () => {
112
- console.log(chalk.yellow('\n Cancelled.'));
113
- process.exit(0);
114
- },
115
- }
116
- );
117
-
118
- const { apiUrl, repoId, apiKey } = answers;
119
- const cleanUrl = apiUrl.replace(/\/$/, '');
120
-
121
- // ─── Validate & fetch ─────────────────────────────────────────────────────────
122
-
123
- process.stdout.write('\n ' + chalk.dim('Connecting to SmashOS…'));
124
-
125
- let data;
126
- try {
127
- const resp = await fetch(`${cleanUrl}/api/repos/${repoId}/layer2-files`, {
128
- headers: { Authorization: `Bearer ${apiKey}` },
129
- });
130
-
131
- if (resp.status === 401) {
132
- console.log(chalk.red(' ✗'));
133
- console.error(chalk.red('\n Invalid API key or Repo ID. Check SmashOS → Settings → API Keys.\n'));
134
- process.exit(1);
135
- }
136
- if (resp.status === 404) {
137
- console.log(chalk.red(' ✗'));
138
- console.error(chalk.red('\n Repo not found. Check the Repo ID in your SmashOS dashboard URL.\n'));
139
- process.exit(1);
140
- }
141
- if (!resp.ok) {
142
- console.log(chalk.red(' ✗'));
143
- console.error(chalk.red(`\n SmashOS returned ${resp.status}. Is the URL correct?\n`));
144
- process.exit(1);
145
- }
146
-
147
- data = await resp.json();
148
- } catch (err) {
149
- console.log(chalk.red(' ✗'));
150
- console.error(chalk.red(`\n Could not reach SmashOS at ${cleanUrl}\n ${err.message}\n`));
151
- process.exit(1);
152
- }
153
-
154
- console.log(chalk.green(' ✓'));
155
- console.log(' ' + chalk.dim(`Connected to SmashOS — repo: ${chalk.white(data.repo.name)}`));
156
- console.log('');
157
-
158
- // ─── Write files ──────────────────────────────────────────────────────────────
159
-
160
- const { files } = data;
161
- let written = 0;
162
- let merged = false;
163
-
164
- for (const [relPath, content] of Object.entries(files)) {
165
- if (relPath === '.claude/settings.json') {
166
- merged = mergeSettingsJson(content);
167
- console.log(
168
- ' ' + chalk.green('✓') + ' ' +
169
- chalk.white('.claude/settings.json') +
170
- chalk.dim(merged ? ' (merged with existing)' : ' (created)')
171
- );
172
- } else {
173
- writeFile(relPath, content);
174
- console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
175
- }
176
- written++;
177
- }
178
-
179
- // ─── Write .env.smash-os (pre-filled) ────────────────────────────────────────
180
-
181
- const envContent = [
182
- '# SmashOS harness credentials — do not commit this file',
183
- `SMASH_OS_API_URL=${cleanUrl}`,
184
- `SMASH_OS_REPO_ID=${repoId}`,
185
- `SMASH_OS_API_KEY=${apiKey}`,
186
- ].join('\n') + '\n';
187
-
188
- writeFile('.env.smash-os', envContent);
189
- console.log(' ' + chalk.green('✓') + ' ' + chalk.white('.env.smash-os') + chalk.dim(' (pre-filled)'));
190
-
191
- // ─── Gitignore ────────────────────────────────────────────────────────────────
192
-
193
- ensureGitignore('.env.smash-os');
194
- console.log(' ' + chalk.green('✓') + ' ' + chalk.dim('.env.smash-os added to .gitignore'));
195
-
196
- // ─── Done ─────────────────────────────────────────────────────────────────────
197
-
198
- console.log('');
199
- console.log(chalk.bold.green(' SmashOS harness installed!') + chalk.dim(` (${written} files)`));
200
- console.log('');
201
- console.log(chalk.dim(' Next steps:'));
202
- console.log(chalk.dim(' 1. Open a Claude Code session in this directory'));
203
- console.log(chalk.dim(' 2. The harness activates automatically on session start'));
204
- console.log(chalk.dim(` 3. Use ${chalk.white('/smash-os:run')} to trigger a pipeline`));
205
- console.log(chalk.dim(` 4. View results at: ${chalk.white(cleanUrl + '/dashboard/repos/' + repoId)}`));
206
- console.log('');