@weirdfingers/baseboards 0.2.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 +191 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +887 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/templates/README.md +120 -0
- package/templates/api/.env.example +62 -0
- package/templates/api/Dockerfile +32 -0
- package/templates/api/README.md +132 -0
- package/templates/api/alembic/env.py +106 -0
- package/templates/api/alembic/script.py.mako +28 -0
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
- package/templates/api/alembic.ini +36 -0
- package/templates/api/config/generators.yaml +25 -0
- package/templates/api/config/storage_config.yaml +26 -0
- package/templates/api/docs/ADDING_GENERATORS.md +409 -0
- package/templates/api/docs/GENERATORS_API.md +502 -0
- package/templates/api/docs/MIGRATIONS.md +472 -0
- package/templates/api/docs/storage_providers.md +337 -0
- package/templates/api/pyproject.toml +165 -0
- package/templates/api/src/boards/__init__.py +10 -0
- package/templates/api/src/boards/api/app.py +171 -0
- package/templates/api/src/boards/api/auth.py +75 -0
- package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
- package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
- package/templates/api/src/boards/api/endpoints/setup.py +505 -0
- package/templates/api/src/boards/api/endpoints/sse.py +129 -0
- package/templates/api/src/boards/api/endpoints/storage.py +74 -0
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
- package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
- package/templates/api/src/boards/auth/__init__.py +15 -0
- package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
- package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
- package/templates/api/src/boards/auth/adapters/base.py +73 -0
- package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
- package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
- package/templates/api/src/boards/auth/adapters/none.py +102 -0
- package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
- package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
- package/templates/api/src/boards/auth/context.py +35 -0
- package/templates/api/src/boards/auth/factory.py +115 -0
- package/templates/api/src/boards/auth/middleware.py +221 -0
- package/templates/api/src/boards/auth/provisioning.py +129 -0
- package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
- package/templates/api/src/boards/cli.py +354 -0
- package/templates/api/src/boards/config.py +116 -0
- package/templates/api/src/boards/database/__init__.py +7 -0
- package/templates/api/src/boards/database/cli.py +110 -0
- package/templates/api/src/boards/database/connection.py +252 -0
- package/templates/api/src/boards/database/models.py +19 -0
- package/templates/api/src/boards/database/seed_data.py +182 -0
- package/templates/api/src/boards/dbmodels/__init__.py +455 -0
- package/templates/api/src/boards/generators/__init__.py +57 -0
- package/templates/api/src/boards/generators/artifacts.py +53 -0
- package/templates/api/src/boards/generators/base.py +140 -0
- package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
- package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
- package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
- package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
- package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
- package/templates/api/src/boards/generators/loader.py +253 -0
- package/templates/api/src/boards/generators/registry.py +114 -0
- package/templates/api/src/boards/generators/resolution.py +515 -0
- package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
- package/templates/api/src/boards/graphql/__init__.py +7 -0
- package/templates/api/src/boards/graphql/access_control.py +136 -0
- package/templates/api/src/boards/graphql/mutations/root.py +136 -0
- package/templates/api/src/boards/graphql/queries/root.py +116 -0
- package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
- package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
- package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
- package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
- package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
- package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
- package/templates/api/src/boards/graphql/schema.py +81 -0
- package/templates/api/src/boards/graphql/types/board.py +102 -0
- package/templates/api/src/boards/graphql/types/generation.py +130 -0
- package/templates/api/src/boards/graphql/types/generator.py +17 -0
- package/templates/api/src/boards/graphql/types/user.py +47 -0
- package/templates/api/src/boards/jobs/repository.py +104 -0
- package/templates/api/src/boards/logging.py +195 -0
- package/templates/api/src/boards/middleware.py +339 -0
- package/templates/api/src/boards/progress/__init__.py +4 -0
- package/templates/api/src/boards/progress/models.py +25 -0
- package/templates/api/src/boards/progress/publisher.py +64 -0
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +118 -0
- package/templates/api/src/boards/storage/__init__.py +52 -0
- package/templates/api/src/boards/storage/base.py +363 -0
- package/templates/api/src/boards/storage/config.py +187 -0
- package/templates/api/src/boards/storage/factory.py +278 -0
- package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
- package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
- package/templates/api/src/boards/storage/implementations/local.py +201 -0
- package/templates/api/src/boards/storage/implementations/s3.py +294 -0
- package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
- package/templates/api/src/boards/tenant_isolation.py +446 -0
- package/templates/api/src/boards/validation.py +262 -0
- package/templates/api/src/boards/workers/__init__.py +1 -0
- package/templates/api/src/boards/workers/actors.py +201 -0
- package/templates/api/src/boards/workers/cli.py +125 -0
- package/templates/api/src/boards/workers/context.py +188 -0
- package/templates/api/src/boards/workers/middleware.py +58 -0
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.dev.yaml +39 -0
- package/templates/compose.yaml +109 -0
- package/templates/docker/env.example +23 -0
- package/templates/web/.env.example +28 -0
- package/templates/web/Dockerfile +51 -0
- package/templates/web/components.json +22 -0
- package/templates/web/imageLoader.js +18 -0
- package/templates/web/next-env.d.ts +5 -0
- package/templates/web/next.config.js +36 -0
- package/templates/web/package.json +37 -0
- package/templates/web/postcss.config.mjs +7 -0
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
- package/templates/web/src/app/globals.css +120 -0
- package/templates/web/src/app/layout.tsx +21 -0
- package/templates/web/src/app/page.tsx +35 -0
- package/templates/web/src/app/providers.tsx +18 -0
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
- package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
- package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
- package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
- package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
- package/templates/web/src/components/header.tsx +30 -0
- package/templates/web/src/components/ui/button.tsx +58 -0
- package/templates/web/src/components/ui/card.tsx +92 -0
- package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
- package/templates/web/src/lib/utils.ts +6 -0
- package/templates/web/tsconfig.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import chalk9 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/commands/up.ts
|
|
11
|
+
import { execa as execa2 } from "execa";
|
|
12
|
+
import fs2 from "fs-extra";
|
|
13
|
+
import path2 from "path";
|
|
14
|
+
import chalk2 from "chalk";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import prompts from "prompts";
|
|
17
|
+
|
|
18
|
+
// src/utils.ts
|
|
19
|
+
import { execa } from "execa";
|
|
20
|
+
import fs from "fs-extra";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
import which from "which";
|
|
24
|
+
import chalk from "chalk";
|
|
25
|
+
import crypto from "crypto";
|
|
26
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
var __dirname = path.dirname(__filename);
|
|
28
|
+
function getTemplatesDir() {
|
|
29
|
+
return path.join(__dirname, "../templates");
|
|
30
|
+
}
|
|
31
|
+
async function checkPrerequisites() {
|
|
32
|
+
const prereqs = {
|
|
33
|
+
docker: { installed: false },
|
|
34
|
+
node: { installed: true, satisfies: false },
|
|
35
|
+
platform: { name: process.platform }
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const dockerPath = await which("docker");
|
|
39
|
+
prereqs.docker.installed = !!dockerPath;
|
|
40
|
+
if (dockerPath) {
|
|
41
|
+
const { stdout } = await execa("docker", ["--version"]);
|
|
42
|
+
const match = stdout.match(/Docker version ([\d.]+)/);
|
|
43
|
+
if (match) {
|
|
44
|
+
prereqs.docker.version = match[1];
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const { stdout: composeStdout } = await execa("docker", [
|
|
48
|
+
"compose",
|
|
49
|
+
"version"
|
|
50
|
+
]);
|
|
51
|
+
const composeMatch = composeStdout.match(/version v?([\d.]+)/);
|
|
52
|
+
if (composeMatch) {
|
|
53
|
+
prereqs.docker.composeVersion = composeMatch[1];
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
}
|
|
60
|
+
const nodeVersion = process.version.replace("v", "");
|
|
61
|
+
prereqs.node.version = nodeVersion;
|
|
62
|
+
const majorVersion = parseInt(nodeVersion.split(".")[0], 10);
|
|
63
|
+
prereqs.node.satisfies = majorVersion >= 20;
|
|
64
|
+
if (process.platform === "linux") {
|
|
65
|
+
try {
|
|
66
|
+
const { stdout } = await execa("uname", ["-r"]);
|
|
67
|
+
if (stdout.toLowerCase().includes("microsoft") || stdout.toLowerCase().includes("wsl")) {
|
|
68
|
+
prereqs.platform.isWSL = true;
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return prereqs;
|
|
74
|
+
}
|
|
75
|
+
async function assertPrerequisites() {
|
|
76
|
+
const prereqs = await checkPrerequisites();
|
|
77
|
+
const errors = [];
|
|
78
|
+
if (!prereqs.docker.installed) {
|
|
79
|
+
errors.push(
|
|
80
|
+
"\u{1F433} Docker is not installed.",
|
|
81
|
+
" Install from: https://docs.docker.com/get-docker/"
|
|
82
|
+
);
|
|
83
|
+
} else if (!prereqs.docker.composeVersion) {
|
|
84
|
+
errors.push(
|
|
85
|
+
"\u{1F433} Docker Compose (v2) is not available.",
|
|
86
|
+
" Update Docker to get Compose v2: https://docs.docker.com/compose/install/"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!prereqs.node.satisfies) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`\u26A0\uFE0F Node.js ${prereqs.node.version} is too old (need v20+).`,
|
|
92
|
+
" Install from: https://nodejs.org/"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (errors.length > 0) {
|
|
96
|
+
console.error(chalk.red("\n\u274C Prerequisites not met:\n"));
|
|
97
|
+
errors.forEach((err) => console.error(chalk.yellow(err)));
|
|
98
|
+
console.error("");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function isScaffolded(dir) {
|
|
103
|
+
const keyFiles = [
|
|
104
|
+
"compose.yaml",
|
|
105
|
+
"web/package.json",
|
|
106
|
+
"api/pyproject.toml"
|
|
107
|
+
];
|
|
108
|
+
return keyFiles.every((file) => fs.existsSync(path.join(dir, file)));
|
|
109
|
+
}
|
|
110
|
+
async function findAvailablePort(preferred, maxAttempts = 50) {
|
|
111
|
+
for (let port = preferred; port < preferred + maxAttempts; port++) {
|
|
112
|
+
if (await isPortAvailable(port)) {
|
|
113
|
+
return port;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`No available port found near ${preferred}`);
|
|
117
|
+
}
|
|
118
|
+
async function isPortAvailable(port) {
|
|
119
|
+
const { createServer } = await import("net");
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const server = createServer();
|
|
122
|
+
server.once("error", () => resolve(false));
|
|
123
|
+
server.once("listening", () => {
|
|
124
|
+
server.close();
|
|
125
|
+
resolve(true);
|
|
126
|
+
});
|
|
127
|
+
server.listen(port);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function generateSecret(length = 32) {
|
|
131
|
+
return crypto.randomBytes(length).toString("hex");
|
|
132
|
+
}
|
|
133
|
+
function generatePassword(length = 24) {
|
|
134
|
+
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
|
135
|
+
let password = "";
|
|
136
|
+
const bytes = crypto.randomBytes(length);
|
|
137
|
+
for (let i = 0; i < length; i++) {
|
|
138
|
+
password += charset[bytes[i] % charset.length];
|
|
139
|
+
}
|
|
140
|
+
return password;
|
|
141
|
+
}
|
|
142
|
+
function parsePortsOption(portsStr) {
|
|
143
|
+
const ports = {};
|
|
144
|
+
const pairs = portsStr.split(/\s+/);
|
|
145
|
+
for (const pair of pairs) {
|
|
146
|
+
const [service, port] = pair.split("=");
|
|
147
|
+
const portNum = parseInt(port, 10);
|
|
148
|
+
if (service && ["web", "api", "db", "redis"].includes(service) && !isNaN(portNum)) {
|
|
149
|
+
ports[service] = portNum;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return ports;
|
|
153
|
+
}
|
|
154
|
+
function getCliVersion() {
|
|
155
|
+
const packagePath = path.join(__dirname, "../package.json");
|
|
156
|
+
const packageJson2 = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
157
|
+
return packageJson2.version;
|
|
158
|
+
}
|
|
159
|
+
function detectMissingProviderKeys(envPath) {
|
|
160
|
+
if (!fs.existsSync(envPath)) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
164
|
+
const providerKeys = [
|
|
165
|
+
"REPLICATE_API_KEY",
|
|
166
|
+
"FAL_KEY",
|
|
167
|
+
"OPENAI_API_KEY",
|
|
168
|
+
"GOOGLE_API_KEY"
|
|
169
|
+
];
|
|
170
|
+
const missingKeys = [];
|
|
171
|
+
for (const key of providerKeys) {
|
|
172
|
+
const regex = new RegExp(`^${key}=(.*)$`, "m");
|
|
173
|
+
const match = envContent.match(regex);
|
|
174
|
+
if (!match || !match[1] || match[1].trim() === "") {
|
|
175
|
+
missingKeys.push(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (missingKeys.length === providerKeys.length) {
|
|
179
|
+
return missingKeys;
|
|
180
|
+
}
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
async function waitFor(condition, options) {
|
|
184
|
+
const { timeoutMs, intervalMs = 1e3, onProgress } = options;
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
187
|
+
if (await condition()) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (onProgress) {
|
|
191
|
+
onProgress(Date.now() - startTime);
|
|
192
|
+
}
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/commands/up.ts
|
|
199
|
+
async function up(directory, options) {
|
|
200
|
+
console.log(chalk2.blue.bold("\n\u{1F3A8} Baseboards CLI\n"));
|
|
201
|
+
const spinner = ora("Checking prerequisites...").start();
|
|
202
|
+
await assertPrerequisites();
|
|
203
|
+
spinner.succeed("Prerequisites OK");
|
|
204
|
+
const dir = path2.resolve(process.cwd(), directory);
|
|
205
|
+
const name = path2.basename(dir);
|
|
206
|
+
const version = getCliVersion();
|
|
207
|
+
const mode = options.prod ? "prod" : "dev";
|
|
208
|
+
let customPorts = {};
|
|
209
|
+
if (options.ports) {
|
|
210
|
+
customPorts = parsePortsOption(options.ports);
|
|
211
|
+
}
|
|
212
|
+
const defaultPorts = {
|
|
213
|
+
web: 3300,
|
|
214
|
+
api: 8800,
|
|
215
|
+
db: 5432,
|
|
216
|
+
redis: 6379,
|
|
217
|
+
...customPorts
|
|
218
|
+
};
|
|
219
|
+
const ctx = {
|
|
220
|
+
dir,
|
|
221
|
+
name,
|
|
222
|
+
isScaffolded: isScaffolded(dir),
|
|
223
|
+
ports: defaultPorts,
|
|
224
|
+
mode,
|
|
225
|
+
version
|
|
226
|
+
};
|
|
227
|
+
const isFreshScaffold = !ctx.isScaffolded;
|
|
228
|
+
if (!ctx.isScaffolded) {
|
|
229
|
+
console.log(
|
|
230
|
+
chalk2.cyan(`
|
|
231
|
+
\u{1F4E6} Scaffolding new project: ${chalk2.bold(name)}`)
|
|
232
|
+
);
|
|
233
|
+
await scaffoldProject(ctx);
|
|
234
|
+
} else {
|
|
235
|
+
console.log(
|
|
236
|
+
chalk2.green(`
|
|
237
|
+
\u2705 Project already scaffolded: ${chalk2.bold(name)}`)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
spinner.start("Checking port availability...");
|
|
241
|
+
ctx.ports.web = await findAvailablePort(ctx.ports.web);
|
|
242
|
+
ctx.ports.api = await findAvailablePort(ctx.ports.api);
|
|
243
|
+
spinner.succeed(
|
|
244
|
+
`Ports available: web=${ctx.ports.web}, api=${ctx.ports.api}`
|
|
245
|
+
);
|
|
246
|
+
await ensureEnvFiles(ctx);
|
|
247
|
+
if (isFreshScaffold) {
|
|
248
|
+
await promptForApiKeys(ctx);
|
|
249
|
+
}
|
|
250
|
+
const apiEnvPath = path2.join(ctx.dir, "api/.env");
|
|
251
|
+
const missingKeys = detectMissingProviderKeys(apiEnvPath);
|
|
252
|
+
if (missingKeys.length > 0) {
|
|
253
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F Provider API keys not configured!"));
|
|
254
|
+
console.log(chalk2.gray(" Add at least one API key to api/.env:"));
|
|
255
|
+
console.log(
|
|
256
|
+
chalk2.cyan(" \u2022 REPLICATE_API_KEY") + chalk2.gray(" - https://replicate.com/account/api-tokens")
|
|
257
|
+
);
|
|
258
|
+
console.log(
|
|
259
|
+
chalk2.cyan(" \u2022 FAL_KEY") + chalk2.gray(" - https://fal.ai/dashboard/keys")
|
|
260
|
+
);
|
|
261
|
+
console.log(
|
|
262
|
+
chalk2.cyan(" \u2022 OPENAI_API_KEY") + chalk2.gray(" - https://platform.openai.com/api-keys")
|
|
263
|
+
);
|
|
264
|
+
console.log(
|
|
265
|
+
chalk2.cyan(" \u2022 GOOGLE_API_KEY") + chalk2.gray(" - https://makersuite.google.com/app/apikey")
|
|
266
|
+
);
|
|
267
|
+
console.log(
|
|
268
|
+
chalk2.gray(
|
|
269
|
+
"\n The app will start, but image generation won't work without keys.\n"
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
await startDockerCompose(ctx, true);
|
|
274
|
+
await waitForHealthy(ctx);
|
|
275
|
+
await runMigrations(ctx);
|
|
276
|
+
printSuccessMessage(ctx, options.detached || false, missingKeys.length > 0);
|
|
277
|
+
if (!options.detached) {
|
|
278
|
+
try {
|
|
279
|
+
await attachToLogs(ctx);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error.signal === "SIGINT" || error.exitCode === 130) {
|
|
282
|
+
console.log(chalk2.yellow("\n\n\u26A0\uFE0F Interrupted - services stopped"));
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function scaffoldProject(ctx) {
|
|
290
|
+
const templatesDir = getTemplatesDir();
|
|
291
|
+
const spinner = ora("Copying templates...").start();
|
|
292
|
+
fs2.ensureDirSync(ctx.dir);
|
|
293
|
+
fs2.copySync(path2.join(templatesDir, "web"), path2.join(ctx.dir, "web"));
|
|
294
|
+
fs2.copySync(path2.join(templatesDir, "api"), path2.join(ctx.dir, "api"));
|
|
295
|
+
const rootFiles = [
|
|
296
|
+
"compose.yaml",
|
|
297
|
+
"compose.dev.yaml",
|
|
298
|
+
"README.md",
|
|
299
|
+
".gitignore"
|
|
300
|
+
];
|
|
301
|
+
for (const file of rootFiles) {
|
|
302
|
+
const src = path2.join(templatesDir, file);
|
|
303
|
+
const dest = path2.join(ctx.dir, file);
|
|
304
|
+
if (fs2.existsSync(src)) {
|
|
305
|
+
fs2.copySync(src, dest);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
fs2.copySync(path2.join(templatesDir, "docker"), path2.join(ctx.dir, "docker"));
|
|
309
|
+
spinner.succeed("Templates copied");
|
|
310
|
+
spinner.start("Creating data directories...");
|
|
311
|
+
fs2.ensureDirSync(path2.join(ctx.dir, "data/storage"));
|
|
312
|
+
spinner.succeed("Data directories created");
|
|
313
|
+
console.log(chalk2.green(" \u2728 Project scaffolded successfully!"));
|
|
314
|
+
}
|
|
315
|
+
async function ensureEnvFiles(ctx) {
|
|
316
|
+
const spinner = ora("Configuring environment...").start();
|
|
317
|
+
const webEnvPath = path2.join(ctx.dir, "web/.env");
|
|
318
|
+
const webEnvExamplePath = path2.join(ctx.dir, "web/.env.example");
|
|
319
|
+
if (!fs2.existsSync(webEnvPath) && fs2.existsSync(webEnvExamplePath)) {
|
|
320
|
+
let webEnv = fs2.readFileSync(webEnvExamplePath, "utf-8");
|
|
321
|
+
webEnv = webEnv.replace(
|
|
322
|
+
"http://localhost:8800",
|
|
323
|
+
`http://localhost:${ctx.ports.api}`
|
|
324
|
+
);
|
|
325
|
+
fs2.writeFileSync(webEnvPath, webEnv);
|
|
326
|
+
}
|
|
327
|
+
const apiEnvPath = path2.join(ctx.dir, "api/.env");
|
|
328
|
+
const apiEnvExamplePath = path2.join(ctx.dir, "api/.env.example");
|
|
329
|
+
if (!fs2.existsSync(apiEnvPath) && fs2.existsSync(apiEnvExamplePath)) {
|
|
330
|
+
let apiEnv = fs2.readFileSync(apiEnvExamplePath, "utf-8");
|
|
331
|
+
if (apiEnv.includes("BOARDS_JWT_SECRET=\n") || apiEnv.includes("BOARDS_JWT_SECRET=\r\n")) {
|
|
332
|
+
const jwtSecret = generateSecret(32);
|
|
333
|
+
apiEnv = apiEnv.replace(
|
|
334
|
+
/BOARDS_JWT_SECRET=.*$/m,
|
|
335
|
+
`BOARDS_JWT_SECRET=${jwtSecret}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
fs2.writeFileSync(apiEnvPath, apiEnv);
|
|
339
|
+
}
|
|
340
|
+
const dockerEnvPath = path2.join(ctx.dir, "docker/.env");
|
|
341
|
+
const dockerEnvExamplePath = path2.join(ctx.dir, "docker/env.example");
|
|
342
|
+
if (!fs2.existsSync(dockerEnvPath) && fs2.existsSync(dockerEnvExamplePath)) {
|
|
343
|
+
let dockerEnv = fs2.readFileSync(dockerEnvExamplePath, "utf-8");
|
|
344
|
+
const dbPassword = generatePassword(24);
|
|
345
|
+
const dbPasswordEncoded = encodeURIComponent(dbPassword);
|
|
346
|
+
dockerEnv = dockerEnv.replace(
|
|
347
|
+
/POSTGRES_PASSWORD=.*/g,
|
|
348
|
+
`POSTGRES_PASSWORD=${dbPassword}`
|
|
349
|
+
);
|
|
350
|
+
dockerEnv = dockerEnv.replace(
|
|
351
|
+
/REPLACE_WITH_GENERATED_PASSWORD/g,
|
|
352
|
+
dbPasswordEncoded
|
|
353
|
+
);
|
|
354
|
+
dockerEnv = dockerEnv.replace(/WEB_PORT=.*/g, `WEB_PORT=${ctx.ports.web}`);
|
|
355
|
+
dockerEnv = dockerEnv.replace(/API_PORT=.*/g, `API_PORT=${ctx.ports.api}`);
|
|
356
|
+
dockerEnv = dockerEnv.replace(/VERSION=.*/g, `VERSION=${ctx.version}`);
|
|
357
|
+
dockerEnv = dockerEnv.replace(
|
|
358
|
+
/PROJECT_NAME=.*/g,
|
|
359
|
+
`PROJECT_NAME=${ctx.name}`
|
|
360
|
+
);
|
|
361
|
+
fs2.writeFileSync(dockerEnvPath, dockerEnv);
|
|
362
|
+
}
|
|
363
|
+
spinner.succeed("Environment configured");
|
|
364
|
+
}
|
|
365
|
+
async function promptForApiKeys(ctx) {
|
|
366
|
+
console.log(chalk2.cyan("\n\u{1F511} API Key Configuration"));
|
|
367
|
+
console.log(chalk2.gray("Add API keys to enable image generation providers"));
|
|
368
|
+
console.log(chalk2.gray("Press Enter to skip any key\n"));
|
|
369
|
+
const response = await prompts([
|
|
370
|
+
{
|
|
371
|
+
type: "text",
|
|
372
|
+
name: "REPLICATE_API_KEY",
|
|
373
|
+
message: "Replicate API Key (https://replicate.com/account/api-tokens):",
|
|
374
|
+
initial: ""
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: "text",
|
|
378
|
+
name: "OPENAI_API_KEY",
|
|
379
|
+
message: "OpenAI API Key (https://platform.openai.com/api-keys):",
|
|
380
|
+
initial: ""
|
|
381
|
+
}
|
|
382
|
+
]);
|
|
383
|
+
const apiKeys = {};
|
|
384
|
+
if (response.REPLICATE_API_KEY && response.REPLICATE_API_KEY.trim()) {
|
|
385
|
+
apiKeys.REPLICATE_API_KEY = response.REPLICATE_API_KEY.trim();
|
|
386
|
+
}
|
|
387
|
+
if (response.OPENAI_API_KEY && response.OPENAI_API_KEY.trim()) {
|
|
388
|
+
apiKeys.OPENAI_API_KEY = response.OPENAI_API_KEY.trim();
|
|
389
|
+
}
|
|
390
|
+
if (Object.keys(apiKeys).length > 0) {
|
|
391
|
+
const apiEnvPath = path2.join(ctx.dir, "api/.env");
|
|
392
|
+
let apiEnv = fs2.readFileSync(apiEnvPath, "utf-8");
|
|
393
|
+
const jsonKeys = JSON.stringify(apiKeys);
|
|
394
|
+
if (apiEnv.includes("BOARDS_GENERATOR_API_KEYS=")) {
|
|
395
|
+
apiEnv = apiEnv.replace(
|
|
396
|
+
/BOARDS_GENERATOR_API_KEYS=.*$/m,
|
|
397
|
+
`BOARDS_GENERATOR_API_KEYS=${jsonKeys}`
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
apiEnv = apiEnv.replace(
|
|
401
|
+
/(BOARDS_JWT_SECRET=.*\n)/,
|
|
402
|
+
`$1
|
|
403
|
+
# Generator API Keys (JSON format)
|
|
404
|
+
BOARDS_GENERATOR_API_KEYS=${jsonKeys}
|
|
405
|
+
`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
fs2.writeFileSync(apiEnvPath, apiEnv);
|
|
409
|
+
console.log(chalk2.green("\n\u2705 API keys saved to api/.env"));
|
|
410
|
+
console.log(
|
|
411
|
+
chalk2.gray(" You can edit this file anytime to add/update keys\n")
|
|
412
|
+
);
|
|
413
|
+
} else {
|
|
414
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F No API keys provided"));
|
|
415
|
+
console.log(chalk2.gray(" You can add them later by editing api/.env\n"));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function startDockerCompose(ctx, detached) {
|
|
419
|
+
const spinner = ora("Starting Docker Compose...").start();
|
|
420
|
+
const composeFiles = ["compose.yaml"];
|
|
421
|
+
if (ctx.mode === "dev") {
|
|
422
|
+
composeFiles.push("compose.dev.yaml");
|
|
423
|
+
}
|
|
424
|
+
const composeArgs = [
|
|
425
|
+
"compose",
|
|
426
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
427
|
+
"up",
|
|
428
|
+
"-d",
|
|
429
|
+
"--remove-orphans"
|
|
430
|
+
];
|
|
431
|
+
try {
|
|
432
|
+
await execa2("docker", composeArgs, {
|
|
433
|
+
cwd: ctx.dir,
|
|
434
|
+
stdio: "inherit"
|
|
435
|
+
});
|
|
436
|
+
spinner.succeed("Docker Compose started");
|
|
437
|
+
} catch (error) {
|
|
438
|
+
spinner.fail("Failed to start Docker Compose");
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function attachToLogs(ctx) {
|
|
443
|
+
console.log(
|
|
444
|
+
chalk2.gray("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")
|
|
445
|
+
);
|
|
446
|
+
console.log(chalk2.gray("Streaming logs... (Press Ctrl+C to stop)\n"));
|
|
447
|
+
const composeFiles = ["compose.yaml"];
|
|
448
|
+
if (ctx.mode === "dev") {
|
|
449
|
+
composeFiles.push("compose.dev.yaml");
|
|
450
|
+
}
|
|
451
|
+
const composeArgs = [
|
|
452
|
+
"compose",
|
|
453
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
454
|
+
"logs",
|
|
455
|
+
"-f"
|
|
456
|
+
];
|
|
457
|
+
await execa2("docker", composeArgs, {
|
|
458
|
+
cwd: ctx.dir,
|
|
459
|
+
stdio: "inherit"
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async function waitForHealthy(ctx) {
|
|
463
|
+
const spinner = ora("Waiting for services to be healthy...").start();
|
|
464
|
+
const services = ["db", "cache", "api", "worker", "web"];
|
|
465
|
+
const maxWaitMs = 12e4;
|
|
466
|
+
const checkHealth = async () => {
|
|
467
|
+
try {
|
|
468
|
+
const { stdout } = await execa2(
|
|
469
|
+
"docker",
|
|
470
|
+
["compose", "ps", "--format", "json"],
|
|
471
|
+
{
|
|
472
|
+
cwd: ctx.dir
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
const containers = stdout.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
476
|
+
const allHealthy = services.every((service) => {
|
|
477
|
+
const container = containers.find((c) => c.Service === service);
|
|
478
|
+
return container && (container.Health === "healthy" || container.State === "running");
|
|
479
|
+
});
|
|
480
|
+
return allHealthy;
|
|
481
|
+
} catch (e) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const success = await waitFor(checkHealth, {
|
|
486
|
+
timeoutMs: maxWaitMs,
|
|
487
|
+
intervalMs: 2e3,
|
|
488
|
+
onProgress: (elapsed) => {
|
|
489
|
+
const seconds = Math.floor(elapsed / 1e3);
|
|
490
|
+
spinner.text = `Waiting for services to be healthy... (${seconds}s)`;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
if (success) {
|
|
494
|
+
spinner.succeed("All services healthy");
|
|
495
|
+
} else {
|
|
496
|
+
spinner.warn("Services taking longer than expected...");
|
|
497
|
+
console.log(
|
|
498
|
+
chalk2.yellow(
|
|
499
|
+
"\n\u26A0\uFE0F Health check timeout. Services may still be starting."
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
console.log(
|
|
503
|
+
chalk2.gray(" Run"),
|
|
504
|
+
chalk2.cyan("baseboards logs"),
|
|
505
|
+
chalk2.gray("to check progress.")
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function runMigrations(ctx) {
|
|
510
|
+
const spinner = ora("Running database migrations...").start();
|
|
511
|
+
try {
|
|
512
|
+
await execa2(
|
|
513
|
+
"docker",
|
|
514
|
+
["compose", "exec", "-T", "api", "alembic", "upgrade", "head"],
|
|
515
|
+
{
|
|
516
|
+
cwd: ctx.dir
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
spinner.succeed("Database migrations complete");
|
|
520
|
+
} catch (error) {
|
|
521
|
+
spinner.fail("Database migrations failed");
|
|
522
|
+
console.log(
|
|
523
|
+
chalk2.yellow(
|
|
524
|
+
"\n\u26A0\uFE0F Database migrations failed. You may need to run them manually:"
|
|
525
|
+
)
|
|
526
|
+
);
|
|
527
|
+
console.log(error);
|
|
528
|
+
console.log(chalk2.cyan(" docker compose exec api alembic upgrade head"));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function printSuccessMessage(ctx, detached, hasKeyWarning) {
|
|
532
|
+
console.log(chalk2.green.bold("\n\u2728 Baseboards is running!\n"));
|
|
533
|
+
console.log(
|
|
534
|
+
chalk2.cyan(" \u{1F310} Web:"),
|
|
535
|
+
chalk2.underline(`http://localhost:${ctx.ports.web}`)
|
|
536
|
+
);
|
|
537
|
+
console.log(
|
|
538
|
+
chalk2.cyan(" \u{1F50C} API:"),
|
|
539
|
+
chalk2.underline(`http://localhost:${ctx.ports.api}`)
|
|
540
|
+
);
|
|
541
|
+
console.log(
|
|
542
|
+
chalk2.cyan(" \u{1F4CA} GraphQL:"),
|
|
543
|
+
chalk2.underline(`http://localhost:${ctx.ports.api}/graphql`)
|
|
544
|
+
);
|
|
545
|
+
if (hasKeyWarning) {
|
|
546
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F Remember to configure provider API keys!"));
|
|
547
|
+
console.log(chalk2.gray(" Edit:"), chalk2.cyan("api/.env"));
|
|
548
|
+
console.log(
|
|
549
|
+
chalk2.gray(" Docs:"),
|
|
550
|
+
chalk2.cyan("https://baseboards.dev/docs/setup")
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
console.log(chalk2.gray("\n\u{1F4D6} Commands:"));
|
|
554
|
+
console.log(chalk2.gray(" Stop:"), chalk2.cyan("baseboards down"));
|
|
555
|
+
console.log(chalk2.gray(" Logs:"), chalk2.cyan("baseboards logs"));
|
|
556
|
+
console.log(chalk2.gray(" Status:"), chalk2.cyan("baseboards status"));
|
|
557
|
+
console.log();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/commands/down.ts
|
|
561
|
+
import { execa as execa3 } from "execa";
|
|
562
|
+
import path3 from "path";
|
|
563
|
+
import chalk3 from "chalk";
|
|
564
|
+
import ora2 from "ora";
|
|
565
|
+
async function down(directory, options) {
|
|
566
|
+
const dir = path3.resolve(process.cwd(), directory);
|
|
567
|
+
if (!isScaffolded(dir)) {
|
|
568
|
+
console.error(chalk3.red("\n\u274C Error: Not a Baseboards project"));
|
|
569
|
+
console.log(chalk3.gray(" Run"), chalk3.cyan("baseboards up"), chalk3.gray("to scaffold a project first."));
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
const spinner = ora2("Stopping services...").start();
|
|
573
|
+
const args = ["compose", "down"];
|
|
574
|
+
if (options.volumes) {
|
|
575
|
+
args.push("--volumes");
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
await execa3("docker", args, {
|
|
579
|
+
cwd: dir
|
|
580
|
+
});
|
|
581
|
+
spinner.succeed("Services stopped");
|
|
582
|
+
if (options.volumes) {
|
|
583
|
+
console.log(chalk3.yellow("\u26A0\uFE0F Volumes removed (database data deleted)"));
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
spinner.fail("Failed to stop services");
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/commands/logs.ts
|
|
592
|
+
import { execa as execa4 } from "execa";
|
|
593
|
+
import path4 from "path";
|
|
594
|
+
import chalk4 from "chalk";
|
|
595
|
+
async function logs(directory, services, options) {
|
|
596
|
+
const dir = path4.resolve(process.cwd(), directory);
|
|
597
|
+
if (!isScaffolded(dir)) {
|
|
598
|
+
console.error(chalk4.red("\n\u274C Error: Not a Baseboards project"));
|
|
599
|
+
console.log(chalk4.gray(" Run"), chalk4.cyan("baseboards up"), chalk4.gray("to scaffold a project first."));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
const args = ["compose", "logs"];
|
|
603
|
+
if (options.follow) {
|
|
604
|
+
args.push("--follow");
|
|
605
|
+
}
|
|
606
|
+
if (options.since) {
|
|
607
|
+
args.push("--since", options.since);
|
|
608
|
+
}
|
|
609
|
+
if (options.tail) {
|
|
610
|
+
args.push("--tail", options.tail);
|
|
611
|
+
}
|
|
612
|
+
if (services.length > 0) {
|
|
613
|
+
args.push(...services);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
await execa4("docker", args, {
|
|
617
|
+
cwd: dir,
|
|
618
|
+
stdio: "inherit"
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error.signal !== "SIGINT") {
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/commands/status.ts
|
|
628
|
+
import { execa as execa5 } from "execa";
|
|
629
|
+
import path5 from "path";
|
|
630
|
+
import chalk5 from "chalk";
|
|
631
|
+
async function status(directory) {
|
|
632
|
+
const dir = path5.resolve(process.cwd(), directory);
|
|
633
|
+
if (!isScaffolded(dir)) {
|
|
634
|
+
console.error(chalk5.red("\n\u274C Error: Not a Baseboards project"));
|
|
635
|
+
console.log(chalk5.gray(" Run"), chalk5.cyan("baseboards up"), chalk5.gray("to scaffold a project first."));
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
console.log(chalk5.blue.bold("\n\u{1F4CA} Service Status\n"));
|
|
639
|
+
try {
|
|
640
|
+
await execa5("docker", ["compose", "ps"], {
|
|
641
|
+
cwd: dir,
|
|
642
|
+
stdio: "inherit"
|
|
643
|
+
});
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error(chalk5.red("\n\u274C Failed to get status"));
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/commands/clean.ts
|
|
651
|
+
import { execa as execa6 } from "execa";
|
|
652
|
+
import path6 from "path";
|
|
653
|
+
import chalk6 from "chalk";
|
|
654
|
+
import ora3 from "ora";
|
|
655
|
+
import prompts2 from "prompts";
|
|
656
|
+
async function clean(directory, options) {
|
|
657
|
+
const dir = path6.resolve(process.cwd(), directory);
|
|
658
|
+
if (!isScaffolded(dir)) {
|
|
659
|
+
console.error(chalk6.red("\n\u274C Error: Not a Baseboards project"));
|
|
660
|
+
console.log(chalk6.gray(" Run"), chalk6.cyan("baseboards up"), chalk6.gray("to scaffold a project first."));
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
if (options.hard) {
|
|
664
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F WARNING: This will delete:"));
|
|
665
|
+
console.log(chalk6.yellow(" \u2022 All containers"));
|
|
666
|
+
console.log(chalk6.yellow(" \u2022 All volumes (database data will be lost)"));
|
|
667
|
+
console.log(chalk6.yellow(" \u2022 All images"));
|
|
668
|
+
const response = await prompts2({
|
|
669
|
+
type: "confirm",
|
|
670
|
+
name: "confirmed",
|
|
671
|
+
message: "Are you sure?",
|
|
672
|
+
initial: false
|
|
673
|
+
});
|
|
674
|
+
if (!response.confirmed) {
|
|
675
|
+
console.log(chalk6.gray("\nCancelled"));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const spinner = ora3("Cleaning up...").start();
|
|
680
|
+
try {
|
|
681
|
+
await execa6("docker", ["compose", "down", "--volumes", "--remove-orphans"], {
|
|
682
|
+
cwd: dir
|
|
683
|
+
});
|
|
684
|
+
if (options.hard) {
|
|
685
|
+
try {
|
|
686
|
+
const { stdout } = await execa6("docker", ["compose", "images", "-q"], {
|
|
687
|
+
cwd: dir
|
|
688
|
+
});
|
|
689
|
+
const imageIds = stdout.split("\n").filter(Boolean);
|
|
690
|
+
if (imageIds.length > 0) {
|
|
691
|
+
await execa6("docker", ["rmi", ...imageIds]);
|
|
692
|
+
}
|
|
693
|
+
} catch (e) {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
spinner.succeed("Cleanup complete");
|
|
697
|
+
if (options.hard) {
|
|
698
|
+
console.log(chalk6.green("\n\u2728 All Docker resources removed"));
|
|
699
|
+
console.log(chalk6.gray(" Run"), chalk6.cyan("baseboards up"), chalk6.gray("to start fresh."));
|
|
700
|
+
} else {
|
|
701
|
+
console.log(chalk6.green("\n\u2728 Containers and volumes removed"));
|
|
702
|
+
}
|
|
703
|
+
} catch (error) {
|
|
704
|
+
spinner.fail("Cleanup failed");
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/commands/update.ts
|
|
710
|
+
import path7 from "path";
|
|
711
|
+
import chalk7 from "chalk";
|
|
712
|
+
async function update(directory, options) {
|
|
713
|
+
const dir = path7.resolve(process.cwd(), directory);
|
|
714
|
+
if (!isScaffolded(dir)) {
|
|
715
|
+
console.error(chalk7.red("\n\u274C Error: Not a Baseboards project"));
|
|
716
|
+
console.log(chalk7.gray(" Run"), chalk7.cyan("baseboards up"), chalk7.gray("to scaffold a project first."));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
console.log(chalk7.blue.bold("\n\u{1F504} Update Command\n"));
|
|
720
|
+
console.log(chalk7.yellow("\u26A0\uFE0F This feature is coming soon!"));
|
|
721
|
+
console.log(chalk7.gray("\nFor now, to update:"));
|
|
722
|
+
console.log(chalk7.gray("1. Update the CLI:"), chalk7.cyan("npm install -g @weirdfingers/baseboards@latest"));
|
|
723
|
+
console.log(chalk7.gray("2. Pull new images:"), chalk7.cyan("docker compose pull"));
|
|
724
|
+
console.log(chalk7.gray("3. Restart:"), chalk7.cyan("baseboards down && baseboards up"));
|
|
725
|
+
console.log();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/commands/doctor.ts
|
|
729
|
+
import path8 from "path";
|
|
730
|
+
import fs3 from "fs-extra";
|
|
731
|
+
import chalk8 from "chalk";
|
|
732
|
+
async function doctor(directory) {
|
|
733
|
+
const dir = path8.resolve(process.cwd(), directory);
|
|
734
|
+
console.log(chalk8.blue.bold("\n\u{1FA7A} Baseboards Diagnostics\n"));
|
|
735
|
+
console.log(chalk8.cyan("CLI Version:"), getCliVersion());
|
|
736
|
+
console.log(chalk8.cyan("\n\u{1F4CB} Prerequisites:"));
|
|
737
|
+
const prereqs = await checkPrerequisites();
|
|
738
|
+
console.log(
|
|
739
|
+
chalk8.gray(" Node.js:"),
|
|
740
|
+
prereqs.node.installed ? prereqs.node.satisfies ? chalk8.green(`\u2713 v${prereqs.node.version}`) : chalk8.yellow(`\u26A0\uFE0F v${prereqs.node.version} (need v20+)`) : chalk8.red("\u2717 Not installed")
|
|
741
|
+
);
|
|
742
|
+
console.log(
|
|
743
|
+
chalk8.gray(" Docker:"),
|
|
744
|
+
prereqs.docker.installed ? chalk8.green(`\u2713 v${prereqs.docker.version}`) : chalk8.red("\u2717 Not installed")
|
|
745
|
+
);
|
|
746
|
+
if (prereqs.docker.composeVersion) {
|
|
747
|
+
console.log(
|
|
748
|
+
chalk8.gray(" Docker Compose:"),
|
|
749
|
+
chalk8.green(`\u2713 v${prereqs.docker.composeVersion}`)
|
|
750
|
+
);
|
|
751
|
+
} else if (prereqs.docker.installed) {
|
|
752
|
+
console.log(chalk8.gray(" Docker Compose:"), chalk8.red("\u2717 Not available"));
|
|
753
|
+
}
|
|
754
|
+
console.log(chalk8.gray(" Platform:"), prereqs.platform.name);
|
|
755
|
+
if (prereqs.platform.isWSL) {
|
|
756
|
+
console.log(chalk8.gray(" WSL:"), chalk8.blue("\u2713 Detected"));
|
|
757
|
+
}
|
|
758
|
+
console.log(chalk8.cyan("\n\u{1F4C2} Project:"));
|
|
759
|
+
const scaffolded = isScaffolded(dir);
|
|
760
|
+
console.log(
|
|
761
|
+
chalk8.gray(" Scaffolded:"),
|
|
762
|
+
scaffolded ? chalk8.green("\u2713 Yes") : chalk8.yellow("\u2717 No")
|
|
763
|
+
);
|
|
764
|
+
if (scaffolded) {
|
|
765
|
+
console.log(chalk8.gray(" Directory:"), dir);
|
|
766
|
+
const webPkg = path8.join(dir, "web/package.json");
|
|
767
|
+
const apiPkg = path8.join(dir, "api/pyproject.toml");
|
|
768
|
+
const composeFile = path8.join(dir, "compose.yaml");
|
|
769
|
+
console.log(
|
|
770
|
+
chalk8.gray(" Web package:"),
|
|
771
|
+
fs3.existsSync(webPkg) ? chalk8.green("\u2713") : chalk8.red("\u2717")
|
|
772
|
+
);
|
|
773
|
+
console.log(
|
|
774
|
+
chalk8.gray(" API package:"),
|
|
775
|
+
fs3.existsSync(apiPkg) ? chalk8.green("\u2713") : chalk8.red("\u2717")
|
|
776
|
+
);
|
|
777
|
+
console.log(
|
|
778
|
+
chalk8.gray(" Compose file:"),
|
|
779
|
+
fs3.existsSync(composeFile) ? chalk8.green("\u2713") : chalk8.red("\u2717")
|
|
780
|
+
);
|
|
781
|
+
console.log(chalk8.cyan("\n\u{1F510} Environment:"));
|
|
782
|
+
const webEnv = path8.join(dir, "web/.env");
|
|
783
|
+
const apiEnv = path8.join(dir, "api/.env");
|
|
784
|
+
const dockerEnv = path8.join(dir, "docker/.env");
|
|
785
|
+
console.log(
|
|
786
|
+
chalk8.gray(" Web .env:"),
|
|
787
|
+
fs3.existsSync(webEnv) ? chalk8.green("\u2713") : chalk8.yellow("\u2717 Missing")
|
|
788
|
+
);
|
|
789
|
+
console.log(
|
|
790
|
+
chalk8.gray(" API .env:"),
|
|
791
|
+
fs3.existsSync(apiEnv) ? chalk8.green("\u2713") : chalk8.yellow("\u2717 Missing")
|
|
792
|
+
);
|
|
793
|
+
console.log(
|
|
794
|
+
chalk8.gray(" Docker .env:"),
|
|
795
|
+
fs3.existsSync(dockerEnv) ? chalk8.green("\u2713") : chalk8.yellow("\u2717 Missing")
|
|
796
|
+
);
|
|
797
|
+
if (fs3.existsSync(apiEnv)) {
|
|
798
|
+
const missingKeys = detectMissingProviderKeys(apiEnv);
|
|
799
|
+
if (missingKeys.length > 0) {
|
|
800
|
+
console.log(
|
|
801
|
+
chalk8.gray(" Provider keys:"),
|
|
802
|
+
chalk8.yellow(`\u26A0\uFE0F ${missingKeys.length} missing`)
|
|
803
|
+
);
|
|
804
|
+
console.log(chalk8.gray(" Missing:"), missingKeys.map((k) => chalk8.cyan(k)).join(", "));
|
|
805
|
+
} else {
|
|
806
|
+
console.log(chalk8.gray(" Provider keys:"), chalk8.green("\u2713 Configured"));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
console.log(chalk8.cyan("\n\u2699\uFE0F Configuration:"));
|
|
810
|
+
const generatorsYaml = path8.join(dir, "api/config/generators.yaml");
|
|
811
|
+
const storageYaml = path8.join(dir, "api/config/storage_config.yaml");
|
|
812
|
+
console.log(
|
|
813
|
+
chalk8.gray(" generators.yaml:"),
|
|
814
|
+
fs3.existsSync(generatorsYaml) ? chalk8.green("\u2713") : chalk8.yellow("\u2717 Missing")
|
|
815
|
+
);
|
|
816
|
+
console.log(
|
|
817
|
+
chalk8.gray(" storage_config.yaml:"),
|
|
818
|
+
fs3.existsSync(storageYaml) ? chalk8.green("\u2713") : chalk8.yellow("\u2717 Missing")
|
|
819
|
+
);
|
|
820
|
+
const storageDir = path8.join(dir, "data/storage");
|
|
821
|
+
console.log(
|
|
822
|
+
chalk8.gray(" Storage directory:"),
|
|
823
|
+
fs3.existsSync(storageDir) ? chalk8.green("\u2713") : chalk8.yellow("\u2717 Missing")
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
console.log(chalk8.cyan("\n\u{1F4A1} Recommendations:"));
|
|
827
|
+
const recommendations = [];
|
|
828
|
+
if (!prereqs.node.satisfies) {
|
|
829
|
+
recommendations.push("Upgrade Node.js to v20 or higher");
|
|
830
|
+
}
|
|
831
|
+
if (!prereqs.docker.installed) {
|
|
832
|
+
recommendations.push("Install Docker Desktop: https://docs.docker.com/get-docker/");
|
|
833
|
+
} else if (!prereqs.docker.composeVersion) {
|
|
834
|
+
recommendations.push("Update Docker to get Compose v2");
|
|
835
|
+
}
|
|
836
|
+
if (!scaffolded) {
|
|
837
|
+
recommendations.push("Run " + chalk8.cyan("baseboards up") + " to scaffold a project");
|
|
838
|
+
}
|
|
839
|
+
if (recommendations.length === 0) {
|
|
840
|
+
console.log(chalk8.green(" \u2713 Everything looks good!"));
|
|
841
|
+
} else {
|
|
842
|
+
recommendations.forEach((rec) => console.log(chalk8.yellow(" \u2022"), rec));
|
|
843
|
+
}
|
|
844
|
+
console.log();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/index.ts
|
|
848
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
849
|
+
var __dirname2 = dirname(__filename2);
|
|
850
|
+
var packageJson = JSON.parse(
|
|
851
|
+
readFileSync(join(__dirname2, "../package.json"), "utf-8")
|
|
852
|
+
);
|
|
853
|
+
var program = new Command();
|
|
854
|
+
program.name("baseboards").description(
|
|
855
|
+
"\u{1F3A8} One-command launcher for the Boards image generation platform"
|
|
856
|
+
).version(packageJson.version, "-v, --version", "Output the current version");
|
|
857
|
+
program.command("up").description("Start Baseboards (scaffolds if needed)").argument("[directory]", "Project directory", ".").option("--dev", "Development mode with hot reload (default)", true).option("--prod", "Production mode with prebuilt images").option("--detached", "Run in detached mode (background)").option("--ports <ports>", "Custom ports (e.g., web=3300 api=8800)").action(up);
|
|
858
|
+
program.command("down").description("Stop Baseboards").argument("[directory]", "Project directory", ".").option("--volumes", "Also remove volumes").action(down);
|
|
859
|
+
program.command("logs").description("View logs from services").argument("[directory]", "Project directory", ".").argument(
|
|
860
|
+
"[services...]",
|
|
861
|
+
"Services to show logs for (web, api, db, cache)",
|
|
862
|
+
[]
|
|
863
|
+
).option("-f, --follow", "Follow log output").option("--since <time>", "Show logs since timestamp (e.g., 1h, 30m)").option("--tail <lines>", "Number of lines to show from end", "100").action(logs);
|
|
864
|
+
program.command("status").description("Show status of services").argument("[directory]", "Project directory", ".").action(status);
|
|
865
|
+
program.command("clean").description("Clean up Docker resources").argument("[directory]", "Project directory", ".").option("--hard", "Remove volumes and images (WARNING: deletes data)").action(clean);
|
|
866
|
+
program.command("update").description("Update Baseboards to latest version").argument("[directory]", "Project directory", ".").option("--force", "Force update without safety checks").option("--version <version>", "Update to specific version").action(update);
|
|
867
|
+
program.command("doctor").description("Run diagnostics and show system info").argument("[directory]", "Project directory", ".").action(doctor);
|
|
868
|
+
try {
|
|
869
|
+
await program.parseAsync(process.argv);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
const err = error;
|
|
872
|
+
console.error(chalk9.red("\n\u274C Error:"), err.message || "Unknown error");
|
|
873
|
+
if (err.stderr) {
|
|
874
|
+
console.error(chalk9.gray("\nDetails:"));
|
|
875
|
+
console.error(chalk9.gray(err.stderr));
|
|
876
|
+
}
|
|
877
|
+
console.error(
|
|
878
|
+
chalk9.yellow("\n\u{1F4A1} Try running:"),
|
|
879
|
+
chalk9.cyan("baseboards doctor")
|
|
880
|
+
);
|
|
881
|
+
console.error(
|
|
882
|
+
chalk9.yellow("\u{1F4D6} Documentation:"),
|
|
883
|
+
chalk9.cyan("https://baseboards.dev/docs")
|
|
884
|
+
);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
//# sourceMappingURL=index.js.map
|