create-smash-os 0.1.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.
- package/README.md +76 -0
- package/index.mjs +287 -0
- package/install.mjs +206 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# create-smash-os
|
|
2
|
+
|
|
3
|
+
> Scaffold a [SmashOS](https://github.com/SmashBurgerBar/smash-os) AI workflow platform instance into any directory.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-smash-os my-smash-os
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or without a name (interactive):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx create-smash-os
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
1. **Clones** the latest SmashOS source from GitHub
|
|
20
|
+
2. **Prompts** for your environment variables (Supabase, GitHub App)
|
|
21
|
+
3. **Creates** `.env.local` with your values
|
|
22
|
+
4. **Runs** `npm install`
|
|
23
|
+
5. **Prints** next steps for Supabase schema push, GitHub App setup, and Vercel deployment
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- Node.js 18+
|
|
28
|
+
- A [Supabase](https://supabase.com) project (free tier works)
|
|
29
|
+
- A [Vercel](https://vercel.com) account (for deployment)
|
|
30
|
+
- A [GitHub App](https://github.com/settings/apps) (created during setup)
|
|
31
|
+
|
|
32
|
+
## After scaffolding
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd my-smash-os
|
|
36
|
+
|
|
37
|
+
# 1. Push the database schema
|
|
38
|
+
npx supabase db push
|
|
39
|
+
|
|
40
|
+
# 2. Fill in GitHub App credentials in .env.local
|
|
41
|
+
# (created during the wizard — GitHub App ID, Client ID, Client Secret, Private Key)
|
|
42
|
+
|
|
43
|
+
# 3. Start locally
|
|
44
|
+
npm run dev
|
|
45
|
+
|
|
46
|
+
# 4. Deploy
|
|
47
|
+
npx vercel --prod
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## GitHub App setup
|
|
51
|
+
|
|
52
|
+
During the wizard you'll be prompted for GitHub App credentials.
|
|
53
|
+
Create your app at: **github.com/settings/apps → New GitHub App**
|
|
54
|
+
|
|
55
|
+
Required settings:
|
|
56
|
+
| Field | Value |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Callback URL | `https://your-domain.vercel.app/auth/github-app/callback` |
|
|
59
|
+
| Post-install redirect | `https://your-domain.vercel.app/dashboard/repos/new?installed=true` |
|
|
60
|
+
| Webhook | Disabled (enabled in later setup) |
|
|
61
|
+
| Permissions | Contents (Read), Metadata (Read), Pull requests (Read & Write), Commit statuses (Read & Write) |
|
|
62
|
+
|
|
63
|
+
## Environment variables
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `SUPABASE_URL` | ✅ | Your Supabase project URL |
|
|
68
|
+
| `SUPABASE_ANON_KEY` | ✅ | Supabase anon/public key |
|
|
69
|
+
| `GITHUB_APP_ID` | ⚙️ | GitHub App numeric ID |
|
|
70
|
+
| `GITHUB_APP_SLUG` | ⚙️ | GitHub App URL slug (e.g. `smash-os`) |
|
|
71
|
+
| `GITHUB_CLIENT_ID` | ⚙️ | GitHub App OAuth Client ID |
|
|
72
|
+
| `GITHUB_CLIENT_SECRET` | ⚙️ | GitHub App OAuth Client Secret |
|
|
73
|
+
| `GITHUB_APP_PRIVATE_KEY` | ⚙️ | GitHub App PEM private key (newlines as `\n`) |
|
|
74
|
+
| `GITHUB_WEBHOOK_SECRET` | ⚙️ | Webhook verification secret |
|
|
75
|
+
|
|
76
|
+
⚙️ = Required for GitHub App integration (repo registration + context engine)
|
package/index.mjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-smash-os
|
|
4
|
+
* Usage: npx create-smash-os [project-name]
|
|
5
|
+
*
|
|
6
|
+
* Scaffolds the SmashOS AI workflow platform into a new directory.
|
|
7
|
+
* Clones from GitHub, runs interactive env setup, and installs dependencies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
12
|
+
import { join, resolve } from "path";
|
|
13
|
+
import { argv, exit, cwd } from "process";
|
|
14
|
+
import { createInterface } from "readline";
|
|
15
|
+
|
|
16
|
+
// ── Lazy-load optional deps (installed via the package's own node_modules) ──
|
|
17
|
+
async function loadDeps() {
|
|
18
|
+
try {
|
|
19
|
+
const [chalkMod, promptsMod, tigedMod] = await Promise.all([
|
|
20
|
+
import("chalk"),
|
|
21
|
+
import("prompts"),
|
|
22
|
+
import("tiged"),
|
|
23
|
+
]);
|
|
24
|
+
return {
|
|
25
|
+
chalk: chalkMod.default,
|
|
26
|
+
prompts: promptsMod.default,
|
|
27
|
+
tiged: tigedMod.default,
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
// Fallback if deps aren't installed — happens on first npx run before cache
|
|
31
|
+
console.error(
|
|
32
|
+
"Missing dependencies. Run: npm install chalk prompts tiged\n" +
|
|
33
|
+
"Or use: npx create-smash-os (which auto-installs via npx)"
|
|
34
|
+
);
|
|
35
|
+
exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── GitHub source — update this once the repo has a public remote ──────────
|
|
40
|
+
const GITHUB_REPO = "SmashBurgerBar/smash-os";
|
|
41
|
+
// Files/dirs to exclude from the clone (CLI package itself, dev artifacts)
|
|
42
|
+
const EXCLUDE_PATTERN = "create-smash-os/**,.env,.env.local";
|
|
43
|
+
|
|
44
|
+
// ── Environment variables the user must configure ──────────────────────────
|
|
45
|
+
const ENV_VARS = [
|
|
46
|
+
{
|
|
47
|
+
name: "SUPABASE_URL",
|
|
48
|
+
message: "Supabase Project URL",
|
|
49
|
+
hint: "https://your-project.supabase.co",
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "SUPABASE_ANON_KEY",
|
|
54
|
+
message: "Supabase Anon Key",
|
|
55
|
+
hint: "Find it in Project Settings → API",
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "GITHUB_APP_ID",
|
|
60
|
+
message: "GitHub App ID",
|
|
61
|
+
hint: "Create at github.com/settings/apps — leave blank to skip",
|
|
62
|
+
required: false,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "GITHUB_APP_SLUG",
|
|
66
|
+
message: "GitHub App Slug (lowercase name from GitHub App URL)",
|
|
67
|
+
hint: "e.g. smash-os",
|
|
68
|
+
required: false,
|
|
69
|
+
default: "smash-os",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "GITHUB_CLIENT_ID",
|
|
73
|
+
message: "GitHub App Client ID",
|
|
74
|
+
required: false,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "GITHUB_CLIENT_SECRET",
|
|
78
|
+
message: "GitHub App Client Secret",
|
|
79
|
+
required: false,
|
|
80
|
+
secret: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "GITHUB_APP_PRIVATE_KEY",
|
|
84
|
+
message: "GitHub App Private Key (PEM, paste with \\n for newlines)",
|
|
85
|
+
hint: "Download from GitHub App settings as .pem file",
|
|
86
|
+
required: false,
|
|
87
|
+
secret: true,
|
|
88
|
+
long: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "GITHUB_WEBHOOK_SECRET",
|
|
92
|
+
message: "GitHub Webhook Secret",
|
|
93
|
+
hint: "Generate with: openssl rand -hex 32",
|
|
94
|
+
required: false,
|
|
95
|
+
secret: true,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
100
|
+
async function main() {
|
|
101
|
+
const { chalk, prompts, tiged } = await loadDeps();
|
|
102
|
+
|
|
103
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log(chalk.bold.white(" ╔══════════════════════════════╗"));
|
|
106
|
+
console.log(chalk.bold.white(" ║ SmashOS Installer ║"));
|
|
107
|
+
console.log(chalk.bold.white(" ║ AI Workflow Platform ║"));
|
|
108
|
+
console.log(chalk.bold.white(" ╚══════════════════════════════╝"));
|
|
109
|
+
console.log("");
|
|
110
|
+
|
|
111
|
+
// ── Project name ────────────────────────────────────────────────────────
|
|
112
|
+
let projectName = argv[2];
|
|
113
|
+
if (!projectName) {
|
|
114
|
+
const { name } = await prompts({
|
|
115
|
+
type: "text",
|
|
116
|
+
name: "name",
|
|
117
|
+
message: "Project name",
|
|
118
|
+
initial: "my-smash-os",
|
|
119
|
+
validate: (v) =>
|
|
120
|
+
/^[a-z0-9-_]+$/.test(v) || "Use lowercase letters, numbers, hyphens only",
|
|
121
|
+
});
|
|
122
|
+
if (!name) {
|
|
123
|
+
console.log(chalk.yellow(" Cancelled."));
|
|
124
|
+
exit(0);
|
|
125
|
+
}
|
|
126
|
+
projectName = name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const targetDir = resolve(cwd(), projectName);
|
|
130
|
+
|
|
131
|
+
if (existsSync(targetDir)) {
|
|
132
|
+
const { overwrite } = await prompts({
|
|
133
|
+
type: "confirm",
|
|
134
|
+
name: "overwrite",
|
|
135
|
+
message: `Directory "${projectName}" already exists. Continue anyway?`,
|
|
136
|
+
initial: false,
|
|
137
|
+
});
|
|
138
|
+
if (!overwrite) {
|
|
139
|
+
console.log(chalk.yellow(" Cancelled."));
|
|
140
|
+
exit(0);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
mkdirSync(targetDir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Clone from GitHub ────────────────────────────────────────────────────
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(chalk.cyan(` Cloning SmashOS from github.com/${GITHUB_REPO}…`));
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const emitter = tiged(GITHUB_REPO, {
|
|
152
|
+
cache: false,
|
|
153
|
+
force: true,
|
|
154
|
+
verbose: false,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await emitter.clone(targetDir);
|
|
158
|
+
console.log(chalk.green(" ✓ Source cloned"));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error("");
|
|
161
|
+
console.error(chalk.red(" ✗ Clone failed:"), err.message);
|
|
162
|
+
console.error("");
|
|
163
|
+
console.error(
|
|
164
|
+
chalk.yellow(" The GitHub repo may not be public yet, or the slug may need updating.")
|
|
165
|
+
);
|
|
166
|
+
console.error(chalk.yellow(` Expected: https://github.com/${GITHUB_REPO}`));
|
|
167
|
+
console.error("");
|
|
168
|
+
console.error(chalk.dim(" Tip: To scaffold from a local copy instead, run:"));
|
|
169
|
+
console.error(chalk.dim(` cp -r /path/to/smash-os ${projectName}`));
|
|
170
|
+
exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Remove the create-smash-os package from the clone ───────────────────
|
|
174
|
+
try {
|
|
175
|
+
execSync(`rm -rf "${join(targetDir, "create-smash-os")}"`, { stdio: "ignore" });
|
|
176
|
+
} catch {
|
|
177
|
+
// Non-critical — directory may not exist
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Env setup ────────────────────────────────────────────────────────────
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log(chalk.bold(" Environment Setup"));
|
|
183
|
+
console.log(chalk.dim(" Configure your SmashOS instance. Press Enter to skip optional vars."));
|
|
184
|
+
console.log("");
|
|
185
|
+
|
|
186
|
+
const envValues = {};
|
|
187
|
+
|
|
188
|
+
for (const envVar of ENV_VARS) {
|
|
189
|
+
const hint = envVar.hint ? chalk.dim(` (${envVar.hint})`) : "";
|
|
190
|
+
const required = envVar.required ? chalk.red(" *required") : chalk.dim(" optional");
|
|
191
|
+
|
|
192
|
+
const { value } = await prompts({
|
|
193
|
+
type: envVar.long ? "text" : "text",
|
|
194
|
+
name: "value",
|
|
195
|
+
message: `${envVar.message}${required}${hint}`,
|
|
196
|
+
initial: envVar.default ?? "",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (value) {
|
|
200
|
+
envValues[envVar.name] = value;
|
|
201
|
+
} else if (envVar.required) {
|
|
202
|
+
console.log(chalk.yellow(` ⚠ ${envVar.name} is required — you'll need to add it to .env.local manually.`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Write .env.local ─────────────────────────────────────────────────────
|
|
207
|
+
const envLines = [
|
|
208
|
+
"# SmashOS — generated by create-smash-os",
|
|
209
|
+
"# Edit this file with your actual credentials before deploying.",
|
|
210
|
+
"",
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const envVar of ENV_VARS) {
|
|
214
|
+
const val = envValues[envVar.name] ?? "";
|
|
215
|
+
if (envVar.message) {
|
|
216
|
+
envLines.push(`# ${envVar.message}`);
|
|
217
|
+
}
|
|
218
|
+
envLines.push(`${envVar.name}=${val}`);
|
|
219
|
+
envLines.push("");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const envPath = join(targetDir, ".env.local");
|
|
223
|
+
writeFileSync(envPath, envLines.join("\n"), "utf-8");
|
|
224
|
+
console.log("");
|
|
225
|
+
console.log(chalk.green(" ✓ .env.local created"));
|
|
226
|
+
|
|
227
|
+
// ── npm install ──────────────────────────────────────────────────────────
|
|
228
|
+
console.log(chalk.cyan(" Installing dependencies…"));
|
|
229
|
+
try {
|
|
230
|
+
execSync("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
231
|
+
console.log(chalk.green(" ✓ Dependencies installed"));
|
|
232
|
+
} catch {
|
|
233
|
+
console.log(chalk.yellow(" ⚠ npm install failed — run it manually inside the project."));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Next steps ───────────────────────────────────────────────────────────
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(chalk.bold.green(" SmashOS scaffolded successfully!"));
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(chalk.bold(" Next steps:"));
|
|
241
|
+
console.log("");
|
|
242
|
+
console.log(
|
|
243
|
+
chalk.white(" 1."),
|
|
244
|
+
chalk.dim("Push the Supabase schema:"),
|
|
245
|
+
chalk.cyan(`cd ${projectName} && npx supabase db push`)
|
|
246
|
+
);
|
|
247
|
+
console.log(
|
|
248
|
+
chalk.white(" 2."),
|
|
249
|
+
chalk.dim("Create a GitHub App at:"),
|
|
250
|
+
chalk.cyan("github.com/settings/apps → New GitHub App")
|
|
251
|
+
);
|
|
252
|
+
console.log(
|
|
253
|
+
chalk.white(" "),
|
|
254
|
+
chalk.dim("Callback URL:"),
|
|
255
|
+
chalk.white(`https://your-domain.vercel.app/auth/github-app/callback`)
|
|
256
|
+
);
|
|
257
|
+
console.log(
|
|
258
|
+
chalk.white(" "),
|
|
259
|
+
chalk.dim("Post-install URL:"),
|
|
260
|
+
chalk.white(`https://your-domain.vercel.app/dashboard/repos/new?installed=true`)
|
|
261
|
+
);
|
|
262
|
+
console.log(
|
|
263
|
+
chalk.white(" 3."),
|
|
264
|
+
chalk.dim("Update .env.local with your GitHub App credentials")
|
|
265
|
+
);
|
|
266
|
+
console.log(
|
|
267
|
+
chalk.white(" 4."),
|
|
268
|
+
chalk.dim("Deploy to Vercel:"),
|
|
269
|
+
chalk.cyan(`cd ${projectName} && npx vercel --prod`)
|
|
270
|
+
);
|
|
271
|
+
console.log(
|
|
272
|
+
chalk.white(" 5."),
|
|
273
|
+
chalk.dim("Start locally:"),
|
|
274
|
+
chalk.cyan(`cd ${projectName} && npm run dev`)
|
|
275
|
+
);
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(
|
|
278
|
+
chalk.dim(" Full setup guide: "),
|
|
279
|
+
chalk.underline.cyan(`https://github.com/${GITHUB_REPO}#setup`)
|
|
280
|
+
);
|
|
281
|
+
console.log("");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
main().catch((err) => {
|
|
285
|
+
console.error("Unexpected error:", err);
|
|
286
|
+
exit(1);
|
|
287
|
+
});
|
package/install.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
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: 'password',
|
|
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('');
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-smash-os",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Scaffold a SmashOS AI workflow platform into any directory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-smash-os": "./index.mjs",
|
|
8
|
+
"smash-os-install": "./install.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"install.mjs",
|
|
13
|
+
"steps.mjs",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"smash-os",
|
|
18
|
+
"ai-workflow",
|
|
19
|
+
"scaffold",
|
|
20
|
+
"create"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.4.1",
|
|
28
|
+
"prompts": "^2.4.2",
|
|
29
|
+
"tiged": "^2.12.7"
|
|
30
|
+
}
|
|
31
|
+
}
|