@revealui/cli 0.4.0 → 0.6.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 +17 -112
- package/dist/cli.js +1107 -435
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1107 -435
- package/dist/index.js.map +1 -1
- package/package.json +5 -6
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { createLogger as
|
|
2
|
+
import { createLogger as createLogger14 } from "@revealui/setup/utils";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
|
|
5
5
|
// src/commands/agent.ts
|
|
@@ -13,7 +13,9 @@ async function runAgentStatusCommand() {
|
|
|
13
13
|
logger.info(`Project root: ${projectRoot}`);
|
|
14
14
|
logger.info(`Available: ${available ? "yes" : "no"}`);
|
|
15
15
|
if (!available) {
|
|
16
|
-
logger.warn(
|
|
16
|
+
logger.warn(
|
|
17
|
+
"No LLM provider detected. Start BitNet, Ollama, or install an Ubuntu inference snap."
|
|
18
|
+
);
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
async function runAgentHeadlessCommand(prompt) {
|
|
@@ -198,9 +200,6 @@ async function detectProvider() {
|
|
|
198
200
|
}
|
|
199
201
|
} catch {
|
|
200
202
|
}
|
|
201
|
-
if (process.env.GROQ_API_KEY) {
|
|
202
|
-
return { available: true, provider: "groq", model: "llama-3.3-70b-versatile", projectRoot };
|
|
203
|
-
}
|
|
204
203
|
return { available: false, provider: "none", model: "none", projectRoot };
|
|
205
204
|
}
|
|
206
205
|
function buildMinimalInstructions() {
|
|
@@ -213,20 +212,337 @@ function buildMinimalInstructions() {
|
|
|
213
212
|
].join("\n");
|
|
214
213
|
}
|
|
215
214
|
|
|
215
|
+
// src/commands/auth.ts
|
|
216
|
+
import fs from "fs/promises";
|
|
217
|
+
import os from "os";
|
|
218
|
+
import path from "path";
|
|
219
|
+
import { createLogger as createLogger2 } from "@revealui/setup/utils";
|
|
220
|
+
import { execa as execa2 } from "execa";
|
|
221
|
+
|
|
222
|
+
// src/utils/command.ts
|
|
223
|
+
import net from "net";
|
|
224
|
+
import { execa } from "execa";
|
|
225
|
+
async function commandExists(command) {
|
|
226
|
+
try {
|
|
227
|
+
await execa("bash", ["-lc", `command -v ${command}`], {
|
|
228
|
+
stdio: "pipe"
|
|
229
|
+
});
|
|
230
|
+
return true;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function isTcpReachable(host, port, timeoutMs = 1500) {
|
|
236
|
+
return await new Promise((resolve2) => {
|
|
237
|
+
const socket = new net.Socket();
|
|
238
|
+
const finalize = (value) => {
|
|
239
|
+
socket.removeAllListeners();
|
|
240
|
+
socket.destroy();
|
|
241
|
+
resolve2(value);
|
|
242
|
+
};
|
|
243
|
+
socket.setTimeout(timeoutMs);
|
|
244
|
+
socket.once("connect", () => finalize(true));
|
|
245
|
+
socket.once("timeout", () => finalize(false));
|
|
246
|
+
socket.once("error", () => finalize(false));
|
|
247
|
+
socket.connect(port, host);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/commands/auth.ts
|
|
252
|
+
var logger2 = createLogger2({ prefix: "Auth" });
|
|
253
|
+
var REVVAULT_NPM_PATH = "revealui/env/npm";
|
|
254
|
+
var NPM_TOKEN_INTERPOLATION = "${NPM_TOKEN}";
|
|
255
|
+
var NPMRC_AUTH_LINE = `//registry.npmjs.org/:_authToken=${NPM_TOKEN_INTERPOLATION}`;
|
|
256
|
+
function write(text5) {
|
|
257
|
+
process.stdout.write(text5);
|
|
258
|
+
}
|
|
259
|
+
function writeJson(data) {
|
|
260
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
261
|
+
`);
|
|
262
|
+
}
|
|
263
|
+
function getNpmrcPaths() {
|
|
264
|
+
const user = path.join(os.homedir(), ".npmrc");
|
|
265
|
+
let dir = process.cwd();
|
|
266
|
+
let project = null;
|
|
267
|
+
for (let i = 0; i < 10; i++) {
|
|
268
|
+
const candidate = path.join(dir, ".npmrc");
|
|
269
|
+
if (dir !== os.homedir()) {
|
|
270
|
+
project ??= candidate;
|
|
271
|
+
}
|
|
272
|
+
const parent = path.dirname(dir);
|
|
273
|
+
if (parent === dir) break;
|
|
274
|
+
dir = parent;
|
|
275
|
+
}
|
|
276
|
+
return { user, project };
|
|
277
|
+
}
|
|
278
|
+
async function fileContains(filePath, needle) {
|
|
279
|
+
try {
|
|
280
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
281
|
+
return content.includes(needle);
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function getTokenFromEnv() {
|
|
287
|
+
return process.env.NPM_TOKEN || void 0;
|
|
288
|
+
}
|
|
289
|
+
async function getTokenFromNpmrc() {
|
|
290
|
+
const userRc = path.join(os.homedir(), ".npmrc");
|
|
291
|
+
try {
|
|
292
|
+
const content = await fs.readFile(userRc, "utf-8");
|
|
293
|
+
const match = content.match(/\/\/registry\.npmjs\.org\/:_authToken=(.+)/);
|
|
294
|
+
return match?.[1]?.trim() || void 0;
|
|
295
|
+
} catch {
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function maskToken(token) {
|
|
300
|
+
if (token.length <= 8) return "***";
|
|
301
|
+
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
302
|
+
}
|
|
303
|
+
async function hasRevvault() {
|
|
304
|
+
return commandExists("revvault");
|
|
305
|
+
}
|
|
306
|
+
async function revvaultGet(secretPath) {
|
|
307
|
+
try {
|
|
308
|
+
const { stdout } = await execa2("revvault", ["get", secretPath], {
|
|
309
|
+
stdio: "pipe"
|
|
310
|
+
});
|
|
311
|
+
return stdout.trim() || void 0;
|
|
312
|
+
} catch {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function revvaultSet(secretPath, value) {
|
|
317
|
+
try {
|
|
318
|
+
await execa2("revvault", ["set", secretPath], {
|
|
319
|
+
input: value,
|
|
320
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
321
|
+
});
|
|
322
|
+
return true;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function runAuthStatusCommand(options) {
|
|
328
|
+
const envToken = await getTokenFromEnv();
|
|
329
|
+
const npmrcToken = await getTokenFromNpmrc();
|
|
330
|
+
const hasVault = await hasRevvault();
|
|
331
|
+
const vaultValue = hasVault ? await revvaultGet(REVVAULT_NPM_PATH) : void 0;
|
|
332
|
+
const paths = getNpmrcPaths();
|
|
333
|
+
const projectUsesEnvVar = paths.project ? await fileContains(paths.project, NPM_TOKEN_INTERPOLATION) : false;
|
|
334
|
+
const effectiveToken = envToken || npmrcToken;
|
|
335
|
+
let npmUser;
|
|
336
|
+
try {
|
|
337
|
+
const { stdout } = await execa2("npm", ["whoami"], { stdio: "pipe" });
|
|
338
|
+
npmUser = stdout.trim();
|
|
339
|
+
} catch {
|
|
340
|
+
npmUser = void 0;
|
|
341
|
+
}
|
|
342
|
+
if (options.json) {
|
|
343
|
+
writeJson({
|
|
344
|
+
authenticated: !!npmUser,
|
|
345
|
+
user: npmUser ?? null,
|
|
346
|
+
tokenSource: envToken ? "env" : npmrcToken ? "npmrc" : null,
|
|
347
|
+
tokenMasked: effectiveToken ? maskToken(effectiveToken) : null,
|
|
348
|
+
revvaultConfigured: !!vaultValue,
|
|
349
|
+
projectNpmrcUsesEnvVar: projectUsesEnvVar,
|
|
350
|
+
paths: {
|
|
351
|
+
userNpmrc: paths.user,
|
|
352
|
+
projectNpmrc: paths.project
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
logger2.header("npm Authentication Status");
|
|
358
|
+
if (npmUser) {
|
|
359
|
+
logger2.success(`Authenticated as: ${npmUser}`);
|
|
360
|
+
} else {
|
|
361
|
+
logger2.error("Not authenticated \u2014 npm whoami failed");
|
|
362
|
+
}
|
|
363
|
+
write("\n");
|
|
364
|
+
logger2.info(
|
|
365
|
+
`Token source: ${envToken ? "$NPM_TOKEN (env)" : npmrcToken ? "~/.npmrc (file)" : "none"}`
|
|
366
|
+
);
|
|
367
|
+
if (effectiveToken) {
|
|
368
|
+
logger2.info(`Token: ${maskToken(effectiveToken)}`);
|
|
369
|
+
}
|
|
370
|
+
write("\n");
|
|
371
|
+
logger2.info(
|
|
372
|
+
`RevVault (${REVVAULT_NPM_PATH}): ${vaultValue ? "configured" : hasVault ? "not set" : "revvault not installed"}`
|
|
373
|
+
);
|
|
374
|
+
logger2.info(`Project .npmrc uses NPM_TOKEN: ${projectUsesEnvVar ? "yes" : "no"}`);
|
|
375
|
+
if (!projectUsesEnvVar && paths.project) {
|
|
376
|
+
logger2.warn(
|
|
377
|
+
"Project .npmrc does not reference NPM_TOKEN \u2014 direnv/RevVault tokens won't be used for publishing"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (!vaultValue && hasVault && effectiveToken) {
|
|
381
|
+
logger2.warn("Token is not stored in RevVault. Run: revealui auth set-token to persist it.");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function runAuthSetTokenCommand(options) {
|
|
385
|
+
logger2.header("Set npm Publish Token");
|
|
386
|
+
const token = options.token || process.env.NPM_TOKEN;
|
|
387
|
+
if (!token) {
|
|
388
|
+
logger2.error("No token provided. Pass --token <value> or set NPM_TOKEN in your environment.");
|
|
389
|
+
logger2.info("Create a Granular Access Token at: https://www.npmjs.com/settings/<user>/tokens");
|
|
390
|
+
logger2.info(" Type: Granular Access Token");
|
|
391
|
+
logger2.info(" Packages: All packages (read and write)");
|
|
392
|
+
logger2.info(" This bypasses 2FA \u2014 no OTP needed for publish");
|
|
393
|
+
process.exitCode = 1;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!token.startsWith("npm_")) {
|
|
397
|
+
logger2.warn('Token does not start with "npm_" \u2014 this may not be a valid npm token');
|
|
398
|
+
}
|
|
399
|
+
if (!options.skipVault) {
|
|
400
|
+
const hasVault = await hasRevvault();
|
|
401
|
+
if (hasVault) {
|
|
402
|
+
const stored = await revvaultSet(REVVAULT_NPM_PATH, `NPM_TOKEN=${token}`);
|
|
403
|
+
if (stored) {
|
|
404
|
+
logger2.success(`Stored in RevVault (${REVVAULT_NPM_PATH})`);
|
|
405
|
+
} else {
|
|
406
|
+
logger2.error("Failed to store in RevVault");
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
logger2.warn("RevVault not installed \u2014 skipping vault storage");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!options.skipNpmrc) {
|
|
413
|
+
const paths = getNpmrcPaths();
|
|
414
|
+
if (paths.project) {
|
|
415
|
+
const hasInterpolation = await fileContains(paths.project, NPM_TOKEN_INTERPOLATION);
|
|
416
|
+
if (!hasInterpolation) {
|
|
417
|
+
try {
|
|
418
|
+
const content = await fs.readFile(paths.project, "utf-8");
|
|
419
|
+
const updatedContent = `${content.trimEnd()}
|
|
420
|
+
${NPMRC_AUTH_LINE}
|
|
421
|
+
`;
|
|
422
|
+
await fs.writeFile(paths.project, updatedContent, "utf-8");
|
|
423
|
+
logger2.success(`Added NPM_TOKEN interpolation to ${paths.project}`);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
logger2.error(`Failed to update ${paths.project}: ${err}`);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
logger2.info("Project .npmrc already uses NPM_TOKEN");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const userRc = path.join(os.homedir(), ".npmrc");
|
|
433
|
+
try {
|
|
434
|
+
const content = await fs.readFile(userRc, "utf-8");
|
|
435
|
+
const filtered = content.split("\n").filter((line) => !line.includes("//registry.npmjs.org/:_authToken=")).join("\n");
|
|
436
|
+
if (filtered !== content) {
|
|
437
|
+
await fs.writeFile(userRc, filtered, "utf-8");
|
|
438
|
+
logger2.success("Removed hardcoded token from ~/.npmrc (now managed via env var)");
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
write("\n");
|
|
443
|
+
logger2.info("Verifying...");
|
|
444
|
+
process.env.NPM_TOKEN = token;
|
|
445
|
+
try {
|
|
446
|
+
const { stdout } = await execa2("npm", ["whoami"], { stdio: "pipe" });
|
|
447
|
+
logger2.success(`Authenticated as: ${stdout.trim()}`);
|
|
448
|
+
} catch {
|
|
449
|
+
logger2.error("npm whoami failed \u2014 token may be invalid or expired");
|
|
450
|
+
process.exitCode = 1;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
write("\n");
|
|
454
|
+
logger2.success("Token configured. Run `direnv reload` to load it in all terminals.");
|
|
455
|
+
}
|
|
456
|
+
async function runAuthVerifyCommand(options) {
|
|
457
|
+
const checks = [];
|
|
458
|
+
let npmUser;
|
|
459
|
+
try {
|
|
460
|
+
const { stdout } = await execa2("npm", ["whoami"], { stdio: "pipe" });
|
|
461
|
+
npmUser = stdout.trim();
|
|
462
|
+
checks.push({ name: "npm whoami", pass: true, detail: npmUser });
|
|
463
|
+
} catch {
|
|
464
|
+
checks.push({
|
|
465
|
+
name: "npm whoami",
|
|
466
|
+
pass: false,
|
|
467
|
+
detail: "Not authenticated"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const envToken = await getTokenFromEnv();
|
|
471
|
+
const npmrcToken = await getTokenFromNpmrc();
|
|
472
|
+
const source = envToken ? "$NPM_TOKEN (env)" : npmrcToken ? "~/.npmrc (hardcoded)" : "none";
|
|
473
|
+
checks.push({
|
|
474
|
+
name: "Token source",
|
|
475
|
+
pass: !!envToken,
|
|
476
|
+
detail: source + (npmrcToken && !envToken ? " \u2014 consider migrating to RevVault" : "")
|
|
477
|
+
});
|
|
478
|
+
const hasVault = await hasRevvault();
|
|
479
|
+
if (hasVault) {
|
|
480
|
+
const vaultValue = await revvaultGet(REVVAULT_NPM_PATH);
|
|
481
|
+
checks.push({
|
|
482
|
+
name: "RevVault",
|
|
483
|
+
pass: !!vaultValue,
|
|
484
|
+
detail: vaultValue ? `${REVVAULT_NPM_PATH} configured` : "Not stored in vault"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const paths = getNpmrcPaths();
|
|
488
|
+
if (paths.project) {
|
|
489
|
+
const usesEnvVar = await fileContains(paths.project, NPM_TOKEN_INTERPOLATION);
|
|
490
|
+
checks.push({
|
|
491
|
+
name: ".npmrc NPM_TOKEN",
|
|
492
|
+
pass: usesEnvVar,
|
|
493
|
+
detail: usesEnvVar ? "Project .npmrc uses env var" : "Missing \u2014 direnv token won't reach npm"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const envrcPath = path.join(process.cwd(), ".envrc");
|
|
497
|
+
const loadsNpm = await fileContains(envrcPath, REVVAULT_NPM_PATH);
|
|
498
|
+
checks.push({
|
|
499
|
+
name: ".envrc loads npm",
|
|
500
|
+
pass: loadsNpm,
|
|
501
|
+
detail: loadsNpm ? "revealui/env/npm in .envrc" : "Missing \u2014 direnv won't export NPM_TOKEN"
|
|
502
|
+
});
|
|
503
|
+
const hardcodedToken = await getTokenFromNpmrc();
|
|
504
|
+
checks.push({
|
|
505
|
+
name: "~/.npmrc clean",
|
|
506
|
+
pass: !hardcodedToken,
|
|
507
|
+
detail: hardcodedToken ? `Hardcoded token found (${maskToken(hardcodedToken)})` : "No hardcoded tokens"
|
|
508
|
+
});
|
|
509
|
+
if (options.json) {
|
|
510
|
+
const allPass2 = checks.every((c) => c.pass);
|
|
511
|
+
writeJson({ pass: allPass2, checks });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
logger2.header("npm Auth Verification");
|
|
515
|
+
for (const check of checks) {
|
|
516
|
+
if (check.pass) {
|
|
517
|
+
logger2.success(`${check.name}: ${check.detail}`);
|
|
518
|
+
} else {
|
|
519
|
+
logger2.error(`${check.name}: ${check.detail}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const allPass = checks.every((c) => c.pass);
|
|
523
|
+
write("\n");
|
|
524
|
+
if (allPass) {
|
|
525
|
+
logger2.success("All checks passed \u2014 ready to publish");
|
|
526
|
+
} else {
|
|
527
|
+
logger2.warn("Some checks failed. Run `revealui auth set-token` to fix.");
|
|
528
|
+
process.exitCode = 1;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
216
532
|
// src/commands/create-flow.ts
|
|
217
533
|
import { readFileSync } from "fs";
|
|
218
534
|
import { homedir } from "os";
|
|
219
535
|
import { join } from "path";
|
|
220
|
-
import { createLogger as
|
|
536
|
+
import { createLogger as createLogger8 } from "@revealui/setup/utils";
|
|
221
537
|
import { importSPKI, jwtVerify } from "jose";
|
|
222
538
|
|
|
223
539
|
// src/prompts/database.ts
|
|
224
|
-
import
|
|
540
|
+
import { isCancel, select, text } from "@clack/prompts";
|
|
225
541
|
|
|
226
542
|
// src/validators/credentials.ts
|
|
227
|
-
import { createLogger as
|
|
228
|
-
var
|
|
229
|
-
|
|
543
|
+
import { createLogger as createLogger3 } from "@revealui/setup/utils";
|
|
544
|
+
var logger3 = createLogger3({ prefix: "Validator" });
|
|
545
|
+
function validateStripeKey(key) {
|
|
230
546
|
if (!(key.startsWith("sk_test_") || key.startsWith("sk_live_"))) {
|
|
231
547
|
return {
|
|
232
548
|
valid: false,
|
|
@@ -235,7 +551,7 @@ async function validateStripeKey(key) {
|
|
|
235
551
|
}
|
|
236
552
|
return { valid: true };
|
|
237
553
|
}
|
|
238
|
-
|
|
554
|
+
function validateNeonUrl(url) {
|
|
239
555
|
try {
|
|
240
556
|
const parsed = new URL(url);
|
|
241
557
|
if (!parsed.protocol.startsWith("postgres")) {
|
|
@@ -252,7 +568,7 @@ async function validateNeonUrl(url) {
|
|
|
252
568
|
};
|
|
253
569
|
}
|
|
254
570
|
}
|
|
255
|
-
|
|
571
|
+
function validateVercelToken(token) {
|
|
256
572
|
if (!token || token.length < 20) {
|
|
257
573
|
return {
|
|
258
574
|
valid: false,
|
|
@@ -261,11 +577,11 @@ async function validateVercelToken(token) {
|
|
|
261
577
|
}
|
|
262
578
|
return { valid: true };
|
|
263
579
|
}
|
|
264
|
-
|
|
580
|
+
function validateSupabaseUrl(url) {
|
|
265
581
|
try {
|
|
266
582
|
const parsed = new URL(url);
|
|
267
583
|
if (!parsed.hostname.includes("supabase")) {
|
|
268
|
-
|
|
584
|
+
logger3.warn("URL does not appear to be a Supabase URL");
|
|
269
585
|
}
|
|
270
586
|
return { valid: true };
|
|
271
587
|
} catch {
|
|
@@ -275,150 +591,152 @@ async function validateSupabaseUrl(url) {
|
|
|
275
591
|
};
|
|
276
592
|
}
|
|
277
593
|
}
|
|
594
|
+
function validateNpmToken(token) {
|
|
595
|
+
if (!token.startsWith("npm_")) {
|
|
596
|
+
return {
|
|
597
|
+
valid: false,
|
|
598
|
+
message: "npm token must start with npm_"
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (token.length < 20) {
|
|
602
|
+
return {
|
|
603
|
+
valid: false,
|
|
604
|
+
message: "npm token appears invalid (too short)"
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return { valid: true };
|
|
608
|
+
}
|
|
278
609
|
|
|
279
610
|
// src/prompts/database.ts
|
|
280
611
|
async function promptDatabaseConfig() {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
name: "Local PostgreSQL - Use existing local database",
|
|
297
|
-
value: "local"
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
name: "Skip - Configure later",
|
|
301
|
-
value: "skip"
|
|
302
|
-
}
|
|
303
|
-
],
|
|
304
|
-
default: "neon"
|
|
305
|
-
}
|
|
306
|
-
]);
|
|
612
|
+
const provider = await select({
|
|
613
|
+
message: "Which database provider would you like to use?",
|
|
614
|
+
options: [
|
|
615
|
+
{ value: "neon", label: "NeonDB - Serverless PostgreSQL (recommended)" },
|
|
616
|
+
{ value: "supabase", label: "Supabase - PostgreSQL with built-in features" },
|
|
617
|
+
{ value: "local", label: "Local PostgreSQL - Use existing local database" },
|
|
618
|
+
{ value: "skip", label: "Skip - Configure later" }
|
|
619
|
+
],
|
|
620
|
+
initialValue: "neon"
|
|
621
|
+
});
|
|
622
|
+
if (isCancel(provider)) {
|
|
623
|
+
process.exit(0);
|
|
624
|
+
}
|
|
307
625
|
if (provider === "skip") {
|
|
308
626
|
return { provider: "skip" };
|
|
309
627
|
}
|
|
310
628
|
if (provider === "local") {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const result = await validateNeonUrl(input);
|
|
319
|
-
return result.valid ? true : result.message || "Invalid database URL";
|
|
320
|
-
}
|
|
629
|
+
const postgresUrl2 = await text({
|
|
630
|
+
message: "Enter your PostgreSQL connection string:",
|
|
631
|
+
defaultValue: "postgresql://postgres:postgres@localhost:5432/revealui",
|
|
632
|
+
validate: (input) => {
|
|
633
|
+
if (!input) return void 0;
|
|
634
|
+
const result = validateNeonUrl(input);
|
|
635
|
+
return result.valid ? void 0 : result.message || "Invalid database URL";
|
|
321
636
|
}
|
|
322
|
-
|
|
637
|
+
});
|
|
638
|
+
if (isCancel(postgresUrl2)) {
|
|
639
|
+
process.exit(0);
|
|
640
|
+
}
|
|
323
641
|
return { provider: "local", postgresUrl: postgresUrl2 };
|
|
324
642
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (!input || input.trim() === "") {
|
|
332
|
-
return "Database URL is required";
|
|
333
|
-
}
|
|
334
|
-
const result = await validateNeonUrl(input);
|
|
335
|
-
return result.valid ? true : result.message || "Invalid database URL";
|
|
643
|
+
const label = provider === "neon" ? "Neon" : "Supabase";
|
|
644
|
+
const postgresUrl = await text({
|
|
645
|
+
message: `Enter your ${label} database connection string:`,
|
|
646
|
+
validate: (input) => {
|
|
647
|
+
if (!input || input.trim() === "") {
|
|
648
|
+
return "Database URL is required";
|
|
336
649
|
}
|
|
650
|
+
const result = validateNeonUrl(input);
|
|
651
|
+
return result.valid ? void 0 : result.message || "Invalid database URL";
|
|
337
652
|
}
|
|
338
|
-
|
|
653
|
+
});
|
|
654
|
+
if (isCancel(postgresUrl)) {
|
|
655
|
+
process.exit(0);
|
|
656
|
+
}
|
|
339
657
|
return { provider, postgresUrl };
|
|
340
658
|
}
|
|
341
659
|
|
|
342
660
|
// src/prompts/devenv.ts
|
|
343
|
-
import
|
|
661
|
+
import { confirm, isCancel as isCancel2 } from "@clack/prompts";
|
|
344
662
|
async function promptDevEnvConfig() {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return
|
|
663
|
+
const createDevContainer = await confirm({
|
|
664
|
+
message: "Create Dev Container configuration for VS Code / GitHub Codespaces?",
|
|
665
|
+
initialValue: true
|
|
666
|
+
});
|
|
667
|
+
if (isCancel2(createDevContainer)) {
|
|
668
|
+
process.exit(0);
|
|
669
|
+
}
|
|
670
|
+
const createDevbox = await confirm({
|
|
671
|
+
message: "Create Devbox configuration for Nix-powered development?",
|
|
672
|
+
initialValue: true
|
|
673
|
+
});
|
|
674
|
+
if (isCancel2(createDevbox)) {
|
|
675
|
+
process.exit(0);
|
|
676
|
+
}
|
|
677
|
+
return { createDevContainer, createDevbox };
|
|
360
678
|
}
|
|
361
679
|
|
|
362
680
|
// src/prompts/payments.ts
|
|
363
|
-
import
|
|
681
|
+
import { confirm as confirm2, isCancel as isCancel3, text as text2 } from "@clack/prompts";
|
|
364
682
|
async function promptPaymentConfig() {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
]);
|
|
683
|
+
const enabled = await confirm2({
|
|
684
|
+
message: "Do you want to configure Stripe payments?",
|
|
685
|
+
initialValue: true
|
|
686
|
+
});
|
|
687
|
+
if (isCancel3(enabled)) {
|
|
688
|
+
process.exit(0);
|
|
689
|
+
}
|
|
373
690
|
if (!enabled) {
|
|
374
691
|
return { enabled: false };
|
|
375
692
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
validate: async (input) => {
|
|
382
|
-
if (!input || input.trim() === "") {
|
|
383
|
-
return "Stripe secret key is required";
|
|
384
|
-
}
|
|
385
|
-
const result = await validateStripeKey(input);
|
|
386
|
-
return result.valid ? true : result.message || "Invalid Stripe key";
|
|
693
|
+
const stripeSecretKey = await text2({
|
|
694
|
+
message: "Enter your Stripe secret key (sk_test_... or sk_live_...):",
|
|
695
|
+
validate: (input) => {
|
|
696
|
+
if (!input || input.trim() === "") {
|
|
697
|
+
return "Stripe secret key is required";
|
|
387
698
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return true;
|
|
699
|
+
const result = validateStripeKey(input);
|
|
700
|
+
return result.valid ? void 0 : result.message || "Invalid Stripe key";
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
if (isCancel3(stripeSecretKey)) {
|
|
704
|
+
process.exit(0);
|
|
705
|
+
}
|
|
706
|
+
const stripePublishableKey = await text2({
|
|
707
|
+
message: "Enter your Stripe publishable key (pk_test_... or pk_live_...):",
|
|
708
|
+
validate: (input) => {
|
|
709
|
+
if (!input || input.trim() === "") {
|
|
710
|
+
return "Stripe publishable key is required";
|
|
401
711
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
message: "Enter your Stripe webhook secret (whsec_..., optional - press Enter to skip):",
|
|
407
|
-
default: ""
|
|
712
|
+
if (!(input.startsWith("pk_test_") || input.startsWith("pk_live_"))) {
|
|
713
|
+
return "Stripe publishable key must start with pk_test_ or pk_live_";
|
|
714
|
+
}
|
|
715
|
+
return void 0;
|
|
408
716
|
}
|
|
409
|
-
|
|
717
|
+
});
|
|
718
|
+
if (isCancel3(stripePublishableKey)) {
|
|
719
|
+
process.exit(0);
|
|
720
|
+
}
|
|
721
|
+
const stripeWebhookSecret = await text2({
|
|
722
|
+
message: "Enter your Stripe webhook secret (whsec_..., optional - press Enter to skip):",
|
|
723
|
+
defaultValue: ""
|
|
724
|
+
});
|
|
725
|
+
if (isCancel3(stripeWebhookSecret)) {
|
|
726
|
+
process.exit(0);
|
|
727
|
+
}
|
|
410
728
|
return {
|
|
411
729
|
enabled: true,
|
|
412
|
-
stripeSecretKey
|
|
413
|
-
stripePublishableKey
|
|
414
|
-
stripeWebhookSecret:
|
|
730
|
+
stripeSecretKey,
|
|
731
|
+
stripePublishableKey,
|
|
732
|
+
stripeWebhookSecret: stripeWebhookSecret || void 0
|
|
415
733
|
};
|
|
416
734
|
}
|
|
417
735
|
|
|
418
736
|
// src/prompts/project.ts
|
|
419
|
-
import
|
|
420
|
-
import
|
|
421
|
-
import
|
|
737
|
+
import fs2 from "fs";
|
|
738
|
+
import path2 from "path";
|
|
739
|
+
import { isCancel as isCancel4, select as select2, text as text3 } from "@clack/prompts";
|
|
422
740
|
var VALID_TEMPLATES = ["basic-blog", "e-commerce", "portfolio"];
|
|
423
741
|
async function promptProjectConfig(defaultName, templateArg, nonInteractive = false) {
|
|
424
742
|
let projectName;
|
|
@@ -427,25 +745,31 @@ async function promptProjectConfig(defaultName, templateArg, nonInteractive = fa
|
|
|
427
745
|
} else if (nonInteractive) {
|
|
428
746
|
projectName = "my-revealui-project";
|
|
429
747
|
} else {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
return true;
|
|
748
|
+
const name = await text3({
|
|
749
|
+
message: "What is your project name?",
|
|
750
|
+
defaultValue: "my-revealui-project",
|
|
751
|
+
validate: (input) => {
|
|
752
|
+
if (!input) return void 0;
|
|
753
|
+
const isValid = input.split("").every((ch) => {
|
|
754
|
+
const code = ch.charCodeAt(0);
|
|
755
|
+
return code >= 97 && code <= 122 || // a-z
|
|
756
|
+
code >= 48 && code <= 57 || // 0-9
|
|
757
|
+
code === 45;
|
|
758
|
+
});
|
|
759
|
+
if (!isValid) {
|
|
760
|
+
return "Project name must contain only lowercase letters, numbers, and hyphens";
|
|
445
761
|
}
|
|
762
|
+
const projectPath = path2.resolve(process.cwd(), input);
|
|
763
|
+
if (fs2.existsSync(projectPath)) {
|
|
764
|
+
return `Directory "${input}" already exists`;
|
|
765
|
+
}
|
|
766
|
+
return void 0;
|
|
446
767
|
}
|
|
447
|
-
|
|
448
|
-
|
|
768
|
+
});
|
|
769
|
+
if (isCancel4(name)) {
|
|
770
|
+
process.exit(0);
|
|
771
|
+
}
|
|
772
|
+
projectName = name;
|
|
449
773
|
}
|
|
450
774
|
let template;
|
|
451
775
|
if (templateArg && VALID_TEMPLATES.includes(templateArg)) {
|
|
@@ -453,118 +777,106 @@ async function promptProjectConfig(defaultName, templateArg, nonInteractive = fa
|
|
|
453
777
|
} else if (nonInteractive) {
|
|
454
778
|
template = "basic-blog";
|
|
455
779
|
} else {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
template = answers.template;
|
|
780
|
+
const selected = await select2({
|
|
781
|
+
message: "Which template would you like to use?",
|
|
782
|
+
options: [
|
|
783
|
+
{ value: "basic-blog", label: "Basic Blog - A simple blog with posts and pages" },
|
|
784
|
+
{ value: "e-commerce", label: "E-commerce - Product catalog with checkout" },
|
|
785
|
+
{ value: "portfolio", label: "Portfolio - Personal portfolio site" }
|
|
786
|
+
],
|
|
787
|
+
initialValue: "basic-blog"
|
|
788
|
+
});
|
|
789
|
+
if (isCancel4(selected)) {
|
|
790
|
+
process.exit(0);
|
|
791
|
+
}
|
|
792
|
+
template = selected;
|
|
470
793
|
}
|
|
471
794
|
return {
|
|
472
795
|
projectName,
|
|
473
|
-
projectPath:
|
|
796
|
+
projectPath: path2.resolve(process.cwd(), projectName),
|
|
474
797
|
template
|
|
475
798
|
};
|
|
476
799
|
}
|
|
477
800
|
|
|
478
801
|
// src/prompts/storage.ts
|
|
479
|
-
import
|
|
802
|
+
import { isCancel as isCancel5, select as select3, text as text4 } from "@clack/prompts";
|
|
480
803
|
async function promptStorageConfig() {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
value: "supabase"
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
name: "Skip - Configure later",
|
|
497
|
-
value: "skip"
|
|
498
|
-
}
|
|
499
|
-
],
|
|
500
|
-
default: "vercel-blob"
|
|
501
|
-
}
|
|
502
|
-
]);
|
|
804
|
+
const provider = await select3({
|
|
805
|
+
message: "Which storage provider would you like to use?",
|
|
806
|
+
options: [
|
|
807
|
+
{ value: "vercel-blob", label: "Vercel Blob - Simple object storage (recommended)" },
|
|
808
|
+
{ value: "supabase", label: "Supabase Storage - Integrated with Supabase" },
|
|
809
|
+
{ value: "skip", label: "Skip - Configure later" }
|
|
810
|
+
],
|
|
811
|
+
initialValue: "vercel-blob"
|
|
812
|
+
});
|
|
813
|
+
if (isCancel5(provider)) {
|
|
814
|
+
process.exit(0);
|
|
815
|
+
}
|
|
503
816
|
if (provider === "skip") {
|
|
504
817
|
return { provider: "skip" };
|
|
505
818
|
}
|
|
506
819
|
if (provider === "vercel-blob") {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
validate: async (input) => {
|
|
513
|
-
if (!input || input.trim() === "") {
|
|
514
|
-
return "Blob token is required";
|
|
515
|
-
}
|
|
516
|
-
const result = await validateVercelToken(input);
|
|
517
|
-
return result.valid ? true : result.message || "Invalid token";
|
|
820
|
+
const blobToken = await text4({
|
|
821
|
+
message: "Enter your Vercel Blob read/write token:",
|
|
822
|
+
validate: (input) => {
|
|
823
|
+
if (!input || input.trim() === "") {
|
|
824
|
+
return "Blob token is required";
|
|
518
825
|
}
|
|
826
|
+
const result = validateVercelToken(input);
|
|
827
|
+
return result.valid ? void 0 : result.message || "Invalid token";
|
|
519
828
|
}
|
|
520
|
-
|
|
829
|
+
});
|
|
830
|
+
if (isCancel5(blobToken)) {
|
|
831
|
+
process.exit(0);
|
|
832
|
+
}
|
|
521
833
|
return { provider: "vercel-blob", blobToken };
|
|
522
834
|
}
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
validate: async (input) => {
|
|
529
|
-
if (!input || input.trim() === "") {
|
|
530
|
-
return "Supabase URL is required";
|
|
531
|
-
}
|
|
532
|
-
const result = await validateSupabaseUrl(input);
|
|
533
|
-
return result.valid ? true : result.message || "Invalid URL";
|
|
835
|
+
const supabaseUrl = await text4({
|
|
836
|
+
message: "Enter your Supabase project URL:",
|
|
837
|
+
validate: (input) => {
|
|
838
|
+
if (!input || input.trim() === "") {
|
|
839
|
+
return "Supabase URL is required";
|
|
534
840
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
841
|
+
const result = validateSupabaseUrl(input);
|
|
842
|
+
return result.valid ? void 0 : result.message || "Invalid URL";
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
if (isCancel5(supabaseUrl)) {
|
|
846
|
+
process.exit(0);
|
|
847
|
+
}
|
|
848
|
+
const supabasePublishableKey = await text4({
|
|
849
|
+
message: "Enter your Supabase publishable key (sb_publishable_...):",
|
|
850
|
+
validate: (input) => {
|
|
851
|
+
if (!input || input.trim() === "") {
|
|
852
|
+
return "Supabase publishable key is required";
|
|
545
853
|
}
|
|
854
|
+
return void 0;
|
|
546
855
|
}
|
|
547
|
-
|
|
856
|
+
});
|
|
857
|
+
if (isCancel5(supabasePublishableKey)) {
|
|
858
|
+
process.exit(0);
|
|
859
|
+
}
|
|
548
860
|
return {
|
|
549
861
|
provider: "supabase",
|
|
550
|
-
supabaseUrl
|
|
551
|
-
supabasePublishableKey
|
|
862
|
+
supabaseUrl,
|
|
863
|
+
supabasePublishableKey
|
|
552
864
|
};
|
|
553
865
|
}
|
|
554
866
|
|
|
555
867
|
// src/validators/node-version.ts
|
|
556
|
-
import { createLogger as
|
|
557
|
-
var
|
|
868
|
+
import { createLogger as createLogger4 } from "@revealui/setup/utils";
|
|
869
|
+
var logger4 = createLogger4({ prefix: "Setup" });
|
|
558
870
|
var REQUIRED_NODE_VERSION = "24.13.0";
|
|
559
871
|
function validateNodeVersion() {
|
|
560
872
|
const currentVersion = process.version.slice(1);
|
|
561
873
|
const [currentMajor, currentMinor] = currentVersion.split(".").map(Number);
|
|
562
874
|
const [requiredMajor, requiredMinor] = REQUIRED_NODE_VERSION.split(".").map(Number);
|
|
563
875
|
if (currentMajor < requiredMajor || currentMajor === requiredMajor && currentMinor < requiredMinor) {
|
|
564
|
-
|
|
876
|
+
logger4.error(
|
|
565
877
|
`Node.js ${REQUIRED_NODE_VERSION} or higher is required. You have ${currentVersion}.`
|
|
566
878
|
);
|
|
567
|
-
|
|
879
|
+
logger4.info("Please upgrade Node.js: https://nodejs.org/");
|
|
568
880
|
return false;
|
|
569
881
|
}
|
|
570
882
|
return true;
|
|
@@ -573,15 +885,15 @@ function validateNodeVersion() {
|
|
|
573
885
|
// src/commands/create.ts
|
|
574
886
|
import crypto from "crypto";
|
|
575
887
|
import { existsSync } from "fs";
|
|
576
|
-
import
|
|
577
|
-
import
|
|
888
|
+
import fs6 from "fs/promises";
|
|
889
|
+
import path6 from "path";
|
|
578
890
|
import { fileURLToPath } from "url";
|
|
579
|
-
import { createLogger as
|
|
891
|
+
import { createLogger as createLogger7 } from "@revealui/setup/utils";
|
|
580
892
|
import ora2 from "ora";
|
|
581
893
|
|
|
582
894
|
// src/generators/devbox.ts
|
|
583
|
-
import
|
|
584
|
-
import
|
|
895
|
+
import fs3 from "fs/promises";
|
|
896
|
+
import path3 from "path";
|
|
585
897
|
async function generateDevbox(projectPath) {
|
|
586
898
|
const devboxConfig = {
|
|
587
899
|
packages: ["nodejs@24.13.0", "pnpm@10.28.2", "postgresql@16", "stripe-cli@latest"],
|
|
@@ -601,19 +913,19 @@ async function generateDevbox(projectPath) {
|
|
|
601
913
|
NODE_ENV: "development"
|
|
602
914
|
}
|
|
603
915
|
};
|
|
604
|
-
await
|
|
605
|
-
|
|
916
|
+
await fs3.writeFile(
|
|
917
|
+
path3.join(projectPath, "devbox.json"),
|
|
606
918
|
JSON.stringify(devboxConfig, null, 2),
|
|
607
919
|
"utf-8"
|
|
608
920
|
);
|
|
609
921
|
}
|
|
610
922
|
|
|
611
923
|
// src/generators/devcontainer.ts
|
|
612
|
-
import
|
|
613
|
-
import
|
|
924
|
+
import fs4 from "fs/promises";
|
|
925
|
+
import path4 from "path";
|
|
614
926
|
async function generateDevContainer(projectPath) {
|
|
615
|
-
const devcontainerDir =
|
|
616
|
-
await
|
|
927
|
+
const devcontainerDir = path4.join(projectPath, ".devcontainer");
|
|
928
|
+
await fs4.mkdir(devcontainerDir, { recursive: true });
|
|
617
929
|
const devcontainerConfig = {
|
|
618
930
|
name: "RevealUI Development",
|
|
619
931
|
image: "mcr.microsoft.com/devcontainers/typescript-node:24",
|
|
@@ -660,8 +972,8 @@ async function generateDevContainer(projectPath) {
|
|
|
660
972
|
},
|
|
661
973
|
remoteUser: "node"
|
|
662
974
|
};
|
|
663
|
-
await
|
|
664
|
-
|
|
975
|
+
await fs4.writeFile(
|
|
976
|
+
path4.join(devcontainerDir, "devcontainer.json"),
|
|
665
977
|
JSON.stringify(devcontainerConfig, null, 2),
|
|
666
978
|
"utf-8"
|
|
667
979
|
);
|
|
@@ -690,7 +1002,7 @@ services:
|
|
|
690
1002
|
volumes:
|
|
691
1003
|
postgres-data:
|
|
692
1004
|
`;
|
|
693
|
-
await
|
|
1005
|
+
await fs4.writeFile(path4.join(devcontainerDir, "docker-compose.yml"), dockerCompose, "utf-8");
|
|
694
1006
|
const dockerfile = `FROM mcr.microsoft.com/devcontainers/typescript-node:24
|
|
695
1007
|
|
|
696
1008
|
# Install additional tools
|
|
@@ -700,7 +1012,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\
|
|
|
700
1012
|
# Enable pnpm
|
|
701
1013
|
RUN corepack enable
|
|
702
1014
|
`;
|
|
703
|
-
await
|
|
1015
|
+
await fs4.writeFile(path4.join(devcontainerDir, "Dockerfile"), dockerfile, "utf-8");
|
|
704
1016
|
const readme = `# Dev Container Setup
|
|
705
1017
|
|
|
706
1018
|
This directory contains the Dev Container configuration for RevealUI.
|
|
@@ -742,12 +1054,12 @@ For GitHub Codespaces, set secrets in your repository settings.
|
|
|
742
1054
|
- 4000: CMS
|
|
743
1055
|
- 5432: PostgreSQL database
|
|
744
1056
|
`;
|
|
745
|
-
await
|
|
1057
|
+
await fs4.writeFile(path4.join(devcontainerDir, "README.md"), readme, "utf-8");
|
|
746
1058
|
}
|
|
747
1059
|
|
|
748
1060
|
// src/generators/readme.ts
|
|
749
|
-
import
|
|
750
|
-
import
|
|
1061
|
+
import fs5 from "fs/promises";
|
|
1062
|
+
import path5 from "path";
|
|
751
1063
|
async function generateReadme(projectPath, projectConfig) {
|
|
752
1064
|
const readme = `# ${projectConfig.projectName}
|
|
753
1065
|
|
|
@@ -819,31 +1131,31 @@ This project was created using the **${projectConfig.template}** template.
|
|
|
819
1131
|
|
|
820
1132
|
MIT
|
|
821
1133
|
`;
|
|
822
|
-
await
|
|
1134
|
+
await fs5.writeFile(path5.join(projectPath, "README.md"), readme, "utf-8");
|
|
823
1135
|
}
|
|
824
1136
|
|
|
825
1137
|
// src/installers/dependencies.ts
|
|
826
|
-
import { createLogger as
|
|
827
|
-
import { execa } from "execa";
|
|
1138
|
+
import { createLogger as createLogger5 } from "@revealui/setup/utils";
|
|
1139
|
+
import { execa as execa3 } from "execa";
|
|
828
1140
|
import ora from "ora";
|
|
829
|
-
var
|
|
1141
|
+
var logger5 = createLogger5({ prefix: "Install" });
|
|
830
1142
|
async function installDependencies(projectPath) {
|
|
831
1143
|
const spinner = ora("Installing dependencies with pnpm...").start();
|
|
832
1144
|
try {
|
|
833
|
-
await
|
|
1145
|
+
await execa3("pnpm", ["install"], {
|
|
834
1146
|
cwd: projectPath,
|
|
835
1147
|
stdio: "pipe"
|
|
836
1148
|
});
|
|
837
1149
|
spinner.succeed("Dependencies installed successfully");
|
|
838
1150
|
} catch (error) {
|
|
839
1151
|
spinner.fail("Failed to install dependencies");
|
|
840
|
-
|
|
1152
|
+
logger5.error('Please run "pnpm install" manually');
|
|
841
1153
|
throw error;
|
|
842
1154
|
}
|
|
843
1155
|
}
|
|
844
1156
|
async function isPnpmInstalled() {
|
|
845
1157
|
try {
|
|
846
|
-
await
|
|
1158
|
+
await execa3("pnpm", ["--version"]);
|
|
847
1159
|
return true;
|
|
848
1160
|
} catch {
|
|
849
1161
|
return false;
|
|
@@ -851,31 +1163,31 @@ async function isPnpmInstalled() {
|
|
|
851
1163
|
}
|
|
852
1164
|
|
|
853
1165
|
// src/utils/git.ts
|
|
854
|
-
import { createLogger as
|
|
855
|
-
import { execa as
|
|
856
|
-
var
|
|
1166
|
+
import { createLogger as createLogger6 } from "@revealui/setup/utils";
|
|
1167
|
+
import { execa as execa4 } from "execa";
|
|
1168
|
+
var logger6 = createLogger6({ prefix: "Git" });
|
|
857
1169
|
async function initializeGitRepo(projectPath) {
|
|
858
1170
|
try {
|
|
859
|
-
await
|
|
860
|
-
|
|
1171
|
+
await execa4("git", ["init"], { cwd: projectPath });
|
|
1172
|
+
logger6.success("Initialized git repository");
|
|
861
1173
|
} catch (error) {
|
|
862
|
-
|
|
1174
|
+
logger6.warn("Failed to initialize git repository");
|
|
863
1175
|
throw error;
|
|
864
1176
|
}
|
|
865
1177
|
}
|
|
866
1178
|
async function createInitialCommit(projectPath) {
|
|
867
1179
|
try {
|
|
868
|
-
await
|
|
869
|
-
await
|
|
870
|
-
|
|
1180
|
+
await execa4("git", ["add", "."], { cwd: projectPath });
|
|
1181
|
+
await execa4("git", ["commit", "-m", "Initial commit from @revealui/cli"], { cwd: projectPath });
|
|
1182
|
+
logger6.success("Created initial commit");
|
|
871
1183
|
} catch (error) {
|
|
872
|
-
|
|
1184
|
+
logger6.warn("Failed to create initial commit");
|
|
873
1185
|
throw error;
|
|
874
1186
|
}
|
|
875
1187
|
}
|
|
876
1188
|
async function isGitInstalled() {
|
|
877
1189
|
try {
|
|
878
|
-
await
|
|
1190
|
+
await execa4("git", ["--version"]);
|
|
879
1191
|
return true;
|
|
880
1192
|
} catch {
|
|
881
1193
|
return false;
|
|
@@ -883,10 +1195,10 @@ async function isGitInstalled() {
|
|
|
883
1195
|
}
|
|
884
1196
|
|
|
885
1197
|
// src/commands/create.ts
|
|
886
|
-
var
|
|
1198
|
+
var logger7 = createLogger7({ prefix: "Create" });
|
|
887
1199
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
888
|
-
var __dirname2 =
|
|
889
|
-
var TEMPLATES_DIR = existsSync(
|
|
1200
|
+
var __dirname2 = path6.dirname(__filename2);
|
|
1201
|
+
var TEMPLATES_DIR = existsSync(path6.resolve(__dirname2, "../../templates")) ? path6.resolve(__dirname2, "../../templates") : path6.resolve(__dirname2, "../templates");
|
|
890
1202
|
function buildEnvLocal(cfg) {
|
|
891
1203
|
const lines = [
|
|
892
1204
|
"# Generated by @revealui/cli \u2014 fill in the remaining placeholders before running `pnpm dev`",
|
|
@@ -932,16 +1244,16 @@ function generateSecret() {
|
|
|
932
1244
|
}
|
|
933
1245
|
async function listAvailableTemplates() {
|
|
934
1246
|
try {
|
|
935
|
-
const entries = await
|
|
1247
|
+
const entries = await fs6.readdir(TEMPLATES_DIR, { withFileTypes: true });
|
|
936
1248
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
937
1249
|
} catch {
|
|
938
1250
|
return [];
|
|
939
1251
|
}
|
|
940
1252
|
}
|
|
941
1253
|
async function copyTemplate(templateName, targetPath) {
|
|
942
|
-
const templatePath =
|
|
1254
|
+
const templatePath = path6.join(TEMPLATES_DIR, templateName);
|
|
943
1255
|
try {
|
|
944
|
-
await
|
|
1256
|
+
await fs6.access(templatePath);
|
|
945
1257
|
} catch {
|
|
946
1258
|
const available = await listAvailableTemplates();
|
|
947
1259
|
const listing = available.length > 0 ? `Available templates: ${available.join(", ")}` : `No templates found in ${TEMPLATES_DIR}`;
|
|
@@ -950,16 +1262,16 @@ async function copyTemplate(templateName, targetPath) {
|
|
|
950
1262
|
await copyDir(templatePath, targetPath);
|
|
951
1263
|
}
|
|
952
1264
|
async function copyDir(src, dest) {
|
|
953
|
-
await
|
|
954
|
-
const entries = await
|
|
1265
|
+
await fs6.mkdir(dest, { recursive: true });
|
|
1266
|
+
const entries = await fs6.readdir(src, { withFileTypes: true });
|
|
955
1267
|
for (const entry of entries) {
|
|
956
|
-
const srcPath =
|
|
1268
|
+
const srcPath = path6.join(src, entry.name);
|
|
957
1269
|
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
958
|
-
const destPath =
|
|
1270
|
+
const destPath = path6.join(dest, destName);
|
|
959
1271
|
if (entry.isDirectory()) {
|
|
960
1272
|
await copyDir(srcPath, destPath);
|
|
961
1273
|
} else {
|
|
962
|
-
await
|
|
1274
|
+
await fs6.copyFile(srcPath, destPath);
|
|
963
1275
|
}
|
|
964
1276
|
}
|
|
965
1277
|
}
|
|
@@ -972,7 +1284,17 @@ function resolveTemplateName(template) {
|
|
|
972
1284
|
return map[template] ?? "starter";
|
|
973
1285
|
}
|
|
974
1286
|
async function pullContentRules(projectPath) {
|
|
975
|
-
const
|
|
1287
|
+
const rawBaseUrl = process.env.REVEALUI_RULES_URL ?? "https://raw.githubusercontent.com/RevealUIStudio/editor-configs/main/harnesses";
|
|
1288
|
+
let baseUrl;
|
|
1289
|
+
try {
|
|
1290
|
+
const parsed = new URL(rawBaseUrl);
|
|
1291
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
baseUrl = rawBaseUrl;
|
|
1295
|
+
} catch {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
976
1298
|
const spinner = ora2("Pulling AI coding rules...").start();
|
|
977
1299
|
try {
|
|
978
1300
|
const manifestRes = await fetch(`${baseUrl}/manifest.json`, {
|
|
@@ -986,18 +1308,27 @@ async function pullContentRules(projectPath) {
|
|
|
986
1308
|
const generatorId = "claude-code";
|
|
987
1309
|
const ossDefs = manifest.definitions.filter((d) => d.tier === "oss");
|
|
988
1310
|
let written = 0;
|
|
1311
|
+
const allowedExtensions = /* @__PURE__ */ new Set([".md", ".json", ".txt", ".yaml", ".yml", ".ts", ".js"]);
|
|
1312
|
+
const maxFileSize = 1048576;
|
|
989
1313
|
for (const def of ossDefs) {
|
|
990
1314
|
const paths = def.generatorPaths[generatorId] ?? [];
|
|
991
1315
|
for (const relPath of paths) {
|
|
992
1316
|
try {
|
|
1317
|
+
if (relPath.includes("..") || relPath.startsWith("/")) continue;
|
|
1318
|
+
const ext = path6.extname(relPath).toLowerCase();
|
|
1319
|
+
if (!allowedExtensions.has(ext)) continue;
|
|
1320
|
+
const absolutePath = path6.resolve(projectPath, relPath);
|
|
1321
|
+
if (!absolutePath.startsWith(path6.resolve(projectPath))) continue;
|
|
993
1322
|
const fileRes = await fetch(`${baseUrl}/generators/${generatorId}/${relPath}`, {
|
|
994
1323
|
signal: AbortSignal.timeout(5e3)
|
|
995
1324
|
});
|
|
996
1325
|
if (!fileRes.ok) continue;
|
|
1326
|
+
const contentLength = fileRes.headers.get("content-length");
|
|
1327
|
+
if (contentLength && Number.parseInt(contentLength, 10) > maxFileSize) continue;
|
|
997
1328
|
const content = await fileRes.text();
|
|
998
|
-
|
|
999
|
-
await
|
|
1000
|
-
await
|
|
1329
|
+
if (content.length > maxFileSize) continue;
|
|
1330
|
+
await fs6.mkdir(path6.dirname(absolutePath), { recursive: true });
|
|
1331
|
+
await fs6.writeFile(absolutePath, content, "utf-8");
|
|
1001
1332
|
written++;
|
|
1002
1333
|
} catch {
|
|
1003
1334
|
}
|
|
@@ -1023,58 +1354,58 @@ async function createProject(cfg) {
|
|
|
1023
1354
|
spinner.fail("Failed to copy template files");
|
|
1024
1355
|
throw err;
|
|
1025
1356
|
}
|
|
1026
|
-
const pkgJsonPath =
|
|
1357
|
+
const pkgJsonPath = path6.join(projectPath, "package.json");
|
|
1027
1358
|
try {
|
|
1028
|
-
const raw = await
|
|
1029
|
-
await
|
|
1359
|
+
const raw = await fs6.readFile(pkgJsonPath, "utf-8");
|
|
1360
|
+
await fs6.writeFile(pkgJsonPath, raw.replaceAll("{{PROJECT_NAME}}", projectName), "utf-8");
|
|
1030
1361
|
} catch {
|
|
1031
1362
|
}
|
|
1032
1363
|
const envSpinner = ora2("Writing .env.local...").start();
|
|
1033
1364
|
try {
|
|
1034
|
-
await
|
|
1365
|
+
await fs6.writeFile(path6.join(projectPath, ".env.local"), buildEnvLocal(cfg), "utf-8");
|
|
1035
1366
|
envSpinner.succeed(".env.local written");
|
|
1036
1367
|
} catch (err) {
|
|
1037
1368
|
envSpinner.fail("Failed to write .env.local");
|
|
1038
1369
|
throw err;
|
|
1039
1370
|
}
|
|
1040
1371
|
await generateReadme(projectPath, project);
|
|
1041
|
-
|
|
1372
|
+
logger7.success("README.md generated");
|
|
1042
1373
|
if (cfg.devenv.createDevContainer) {
|
|
1043
1374
|
await generateDevContainer(projectPath);
|
|
1044
|
-
|
|
1375
|
+
logger7.success(".devcontainer/ generated");
|
|
1045
1376
|
}
|
|
1046
1377
|
if (cfg.devenv.createDevbox) {
|
|
1047
1378
|
await generateDevbox(projectPath);
|
|
1048
|
-
|
|
1379
|
+
logger7.success("devbox.json generated");
|
|
1049
1380
|
}
|
|
1050
1381
|
await pullContentRules(projectPath);
|
|
1051
1382
|
if (!skipInstall) {
|
|
1052
1383
|
const pnpmOk = await isPnpmInstalled();
|
|
1053
1384
|
if (!pnpmOk) {
|
|
1054
|
-
|
|
1385
|
+
logger7.warn(
|
|
1055
1386
|
"pnpm not found \u2014 skipping dependency installation. Run `pnpm install` manually."
|
|
1056
1387
|
);
|
|
1057
1388
|
} else {
|
|
1058
1389
|
await installDependencies(projectPath);
|
|
1059
1390
|
}
|
|
1060
1391
|
} else {
|
|
1061
|
-
|
|
1392
|
+
logger7.info("Skipping dependency installation (--skip-install)");
|
|
1062
1393
|
}
|
|
1063
1394
|
if (!skipGit) {
|
|
1064
1395
|
const gitOk = await isGitInstalled();
|
|
1065
1396
|
if (!gitOk) {
|
|
1066
|
-
|
|
1397
|
+
logger7.warn("git not found \u2014 skipping repository initialisation.");
|
|
1067
1398
|
} else {
|
|
1068
1399
|
await initializeGitRepo(projectPath);
|
|
1069
1400
|
await createInitialCommit(projectPath);
|
|
1070
1401
|
}
|
|
1071
1402
|
} else {
|
|
1072
|
-
|
|
1403
|
+
logger7.info("Skipping git initialisation (--skip-git)");
|
|
1073
1404
|
}
|
|
1074
1405
|
}
|
|
1075
1406
|
|
|
1076
1407
|
// src/commands/create-flow.ts
|
|
1077
|
-
var
|
|
1408
|
+
var logger8 = createLogger8({ prefix: "@revealui/cli" });
|
|
1078
1409
|
var PRO_TEMPLATES = /* @__PURE__ */ new Set([]);
|
|
1079
1410
|
async function checkProLicense() {
|
|
1080
1411
|
let key = process.env.REVEALUI_LICENSE_KEY;
|
|
@@ -1112,108 +1443,113 @@ async function checkProLicense() {
|
|
|
1112
1443
|
}
|
|
1113
1444
|
}
|
|
1114
1445
|
function printBanner() {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1446
|
+
logger8.divider();
|
|
1447
|
+
logger8.info(" RevealUI \u2014 Agentic Business Runtime");
|
|
1448
|
+
logger8.info(" Build your business, not your boilerplate.");
|
|
1449
|
+
logger8.divider();
|
|
1450
|
+
logger8.info("");
|
|
1120
1451
|
}
|
|
1121
1452
|
function printPostCreateSummary(projectName) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1453
|
+
logger8.divider();
|
|
1454
|
+
logger8.success("Your RevealUI project is ready!");
|
|
1455
|
+
logger8.info("");
|
|
1456
|
+
logger8.info(" Quick start:");
|
|
1457
|
+
logger8.info(` cd ${projectName}`);
|
|
1458
|
+
logger8.info(" pnpm install");
|
|
1459
|
+
logger8.info(" pnpm dev");
|
|
1460
|
+
logger8.info("");
|
|
1461
|
+
logger8.info(" What was created:");
|
|
1462
|
+
logger8.info(` ./${projectName}/ \u2014 project root`);
|
|
1463
|
+
logger8.info(` ./${projectName}/.env.local \u2014 environment variables (edit before pnpm dev)`);
|
|
1464
|
+
logger8.info(` ./${projectName}/README.md \u2014 getting started guide`);
|
|
1465
|
+
logger8.info("");
|
|
1466
|
+
logger8.info(" RevealUI ecosystem:");
|
|
1467
|
+
logger8.info(" Studio: Native AI experience \u2014 agent hub, local inference, dev environment");
|
|
1468
|
+
logger8.info(" Terminal: TUI client \u2014 run `revealui terminal install`");
|
|
1469
|
+
logger8.info(" CMS: Admin dashboard at your-domain.com/admin");
|
|
1470
|
+
logger8.info("");
|
|
1471
|
+
logger8.info(" Helpful links:");
|
|
1472
|
+
logger8.info(" Docs: https://docs.revealui.com");
|
|
1473
|
+
logger8.info(" GitHub: https://github.com/RevealUIStudio/revealui");
|
|
1474
|
+
logger8.info(" Support: support@revealui.com");
|
|
1475
|
+
logger8.divider();
|
|
1140
1476
|
}
|
|
1141
1477
|
function formatCreateError(err) {
|
|
1142
1478
|
const message = err instanceof Error ? err.message : String(err);
|
|
1143
1479
|
const code = err instanceof Error && "code" in err ? err.code : void 0;
|
|
1144
|
-
|
|
1145
|
-
|
|
1480
|
+
logger8.error("Project creation failed.");
|
|
1481
|
+
logger8.info("");
|
|
1146
1482
|
if (code === "EACCES") {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1483
|
+
logger8.info(" Permission denied. Try:");
|
|
1484
|
+
logger8.info(" - Running from a directory you own");
|
|
1485
|
+
logger8.info(" - Checking folder permissions with `ls -la`");
|
|
1150
1486
|
} else if (code === "ENOENT") {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1487
|
+
logger8.info(" A required file or directory was not found.");
|
|
1488
|
+
logger8.info(" - Check that you are in the correct directory");
|
|
1489
|
+
logger8.info(" - Try running `revealui doctor` to diagnose your environment");
|
|
1154
1490
|
} else if (code === "ENOSPC") {
|
|
1155
|
-
|
|
1491
|
+
logger8.info(" Disk full. Free up space and try again.");
|
|
1156
1492
|
} else if (message.includes("fetch") || message.includes("ENOTFOUND") || message.includes("ETIMEDOUT")) {
|
|
1157
|
-
|
|
1493
|
+
logger8.info(" Network error. Check your internet connection and try again.");
|
|
1158
1494
|
} else {
|
|
1159
|
-
|
|
1495
|
+
logger8.info(` ${message}`);
|
|
1160
1496
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1497
|
+
logger8.info("");
|
|
1498
|
+
logger8.info(" Troubleshooting:");
|
|
1499
|
+
logger8.info(" revealui doctor \u2014 diagnose your environment");
|
|
1500
|
+
logger8.info(" https://docs.revealui.com/docs/TROUBLESHOOTING");
|
|
1501
|
+
logger8.info(" support@revealui.com \u2014 we are here to help");
|
|
1502
|
+
logger8.divider();
|
|
1167
1503
|
}
|
|
1168
1504
|
async function runCreateFlow(projectName, options) {
|
|
1169
1505
|
printBanner();
|
|
1170
1506
|
try {
|
|
1171
|
-
|
|
1507
|
+
logger8.info("[1/8] Validating Node.js version...");
|
|
1172
1508
|
if (!validateNodeVersion()) {
|
|
1173
1509
|
process.exit(1);
|
|
1174
1510
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1511
|
+
logger8.success(`Node.js version: ${process.version}`);
|
|
1512
|
+
logger8.info("[2/8] Configure your project");
|
|
1177
1513
|
const projectConfig = await promptProjectConfig(projectName, options.template, options.yes);
|
|
1178
|
-
|
|
1179
|
-
|
|
1514
|
+
logger8.success(`Project: ${projectConfig.projectName}`);
|
|
1515
|
+
logger8.success(`Template: ${projectConfig.template}`);
|
|
1180
1516
|
if (PRO_TEMPLATES.has(projectConfig.template)) {
|
|
1181
1517
|
if (!await checkProLicense()) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1518
|
+
logger8.error(`The "${projectConfig.template}" template requires a RevealUI Pro license.`);
|
|
1519
|
+
logger8.info("Get Pro at https://revealui.com/pricing");
|
|
1520
|
+
logger8.info("Set your license key: export REVEALUI_LICENSE_KEY=<your-key>");
|
|
1185
1521
|
process.exit(2);
|
|
1186
1522
|
}
|
|
1187
|
-
|
|
1523
|
+
logger8.success("Pro license verified");
|
|
1188
1524
|
}
|
|
1189
1525
|
const nonInteractive = options.yes === true;
|
|
1190
|
-
|
|
1526
|
+
logger8.info("[3/8] Configure database");
|
|
1191
1527
|
const databaseConfig = nonInteractive ? { provider: "skip" } : await promptDatabaseConfig();
|
|
1192
1528
|
if (databaseConfig.provider !== "skip") {
|
|
1193
|
-
|
|
1529
|
+
logger8.success(`Database: ${databaseConfig.provider}`);
|
|
1194
1530
|
} else {
|
|
1195
|
-
|
|
1531
|
+
logger8.info("Database configuration skipped");
|
|
1196
1532
|
}
|
|
1197
|
-
|
|
1533
|
+
logger8.info("[4/8] Configure storage");
|
|
1198
1534
|
const storageConfig = nonInteractive ? { provider: "skip" } : await promptStorageConfig();
|
|
1199
1535
|
if (storageConfig.provider !== "skip") {
|
|
1200
|
-
|
|
1536
|
+
logger8.success(`Storage: ${storageConfig.provider}`);
|
|
1201
1537
|
} else {
|
|
1202
|
-
|
|
1538
|
+
logger8.info("Storage configuration skipped");
|
|
1203
1539
|
}
|
|
1204
|
-
|
|
1540
|
+
logger8.info("[5/8] Configure payments");
|
|
1205
1541
|
const paymentConfig = nonInteractive ? { enabled: false } : await promptPaymentConfig();
|
|
1206
1542
|
if (paymentConfig.enabled) {
|
|
1207
|
-
|
|
1543
|
+
logger8.success("Stripe configured");
|
|
1208
1544
|
} else {
|
|
1209
|
-
|
|
1545
|
+
logger8.info("Payments disabled");
|
|
1210
1546
|
}
|
|
1211
|
-
|
|
1547
|
+
logger8.info("[6/8] Configure development environment");
|
|
1212
1548
|
const devEnvConfig = nonInteractive ? { createDevContainer: false, createDevbox: false } : await promptDevEnvConfig();
|
|
1213
|
-
|
|
1549
|
+
logger8.success(
|
|
1214
1550
|
`Dev Container: ${devEnvConfig.createDevContainer ? "Yes" : "No"}, Devbox: ${devEnvConfig.createDevbox ? "Yes" : "No"}`
|
|
1215
1551
|
);
|
|
1216
|
-
|
|
1552
|
+
logger8.info("[7/8] Creating project...");
|
|
1217
1553
|
await createProject({
|
|
1218
1554
|
project: projectConfig,
|
|
1219
1555
|
database: databaseConfig,
|
|
@@ -1223,8 +1559,8 @@ async function runCreateFlow(projectName, options) {
|
|
|
1223
1559
|
skipGit: options.skipGit,
|
|
1224
1560
|
skipInstall: options.skipInstall
|
|
1225
1561
|
});
|
|
1226
|
-
|
|
1227
|
-
|
|
1562
|
+
logger8.success("Project created successfully");
|
|
1563
|
+
logger8.info("[8/8] Done!");
|
|
1228
1564
|
printPostCreateSummary(projectConfig.projectName);
|
|
1229
1565
|
} catch (err) {
|
|
1230
1566
|
formatCreateError(err);
|
|
@@ -1233,45 +1569,16 @@ async function runCreateFlow(projectName, options) {
|
|
|
1233
1569
|
}
|
|
1234
1570
|
|
|
1235
1571
|
// src/commands/db.ts
|
|
1236
|
-
import
|
|
1237
|
-
import
|
|
1238
|
-
import { createLogger as
|
|
1239
|
-
import { execa as
|
|
1240
|
-
|
|
1241
|
-
// src/utils/command.ts
|
|
1242
|
-
import net from "net";
|
|
1243
|
-
import { execa as execa3 } from "execa";
|
|
1244
|
-
async function commandExists(command) {
|
|
1245
|
-
try {
|
|
1246
|
-
await execa3("bash", ["-lc", `command -v ${command}`], {
|
|
1247
|
-
stdio: "pipe"
|
|
1248
|
-
});
|
|
1249
|
-
return true;
|
|
1250
|
-
} catch {
|
|
1251
|
-
return false;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
async function isTcpReachable(host, port, timeoutMs = 1500) {
|
|
1255
|
-
return await new Promise((resolve) => {
|
|
1256
|
-
const socket = new net.Socket();
|
|
1257
|
-
const finalize = (value) => {
|
|
1258
|
-
socket.removeAllListeners();
|
|
1259
|
-
socket.destroy();
|
|
1260
|
-
resolve(value);
|
|
1261
|
-
};
|
|
1262
|
-
socket.setTimeout(timeoutMs);
|
|
1263
|
-
socket.once("connect", () => finalize(true));
|
|
1264
|
-
socket.once("timeout", () => finalize(false));
|
|
1265
|
-
socket.once("error", () => finalize(false));
|
|
1266
|
-
socket.connect(port, host);
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1572
|
+
import fs9 from "fs/promises";
|
|
1573
|
+
import path9 from "path";
|
|
1574
|
+
import { createLogger as createLogger9 } from "@revealui/setup/utils";
|
|
1575
|
+
import { execa as execa5 } from "execa";
|
|
1269
1576
|
|
|
1270
1577
|
// src/utils/db.ts
|
|
1271
|
-
import
|
|
1272
|
-
import
|
|
1578
|
+
import fs7 from "fs/promises";
|
|
1579
|
+
import path7 from "path";
|
|
1273
1580
|
function resolveLocalDbConfig(cwd = process.cwd(), env = process.env) {
|
|
1274
|
-
const pgdata = env.PGDATA ||
|
|
1581
|
+
const pgdata = env.PGDATA || path7.join(cwd, ".pgdata");
|
|
1275
1582
|
const pghost = env.PGHOST || pgdata;
|
|
1276
1583
|
const pgdatabase = env.PGDATABASE || "postgres";
|
|
1277
1584
|
const pguser = env.PGUSER || "postgres";
|
|
@@ -1318,27 +1625,27 @@ host all all ::1/128 trust
|
|
|
1318
1625
|
`.trimStart();
|
|
1319
1626
|
}
|
|
1320
1627
|
async function writeLocalDbConfigs(pgdata) {
|
|
1321
|
-
await
|
|
1322
|
-
await
|
|
1628
|
+
await fs7.appendFile(path7.join(pgdata, "postgresql.conf"), buildPostgresConfig(pgdata), "utf8");
|
|
1629
|
+
await fs7.writeFile(path7.join(pgdata, "pg_hba.conf"), buildPgHbaConfig(), "utf8");
|
|
1323
1630
|
}
|
|
1324
1631
|
|
|
1325
1632
|
// src/utils/workspace.ts
|
|
1326
|
-
import
|
|
1327
|
-
import
|
|
1633
|
+
import fs8 from "fs";
|
|
1634
|
+
import path8 from "path";
|
|
1328
1635
|
function findWorkspaceRoot(startDir = process.cwd()) {
|
|
1329
|
-
let current =
|
|
1636
|
+
let current = path8.resolve(startDir);
|
|
1330
1637
|
while (true) {
|
|
1331
|
-
const packageJsonPath =
|
|
1332
|
-
if (
|
|
1638
|
+
const packageJsonPath = path8.join(current, "package.json");
|
|
1639
|
+
if (fs8.existsSync(packageJsonPath)) {
|
|
1333
1640
|
try {
|
|
1334
|
-
const pkg = JSON.parse(
|
|
1641
|
+
const pkg = JSON.parse(fs8.readFileSync(packageJsonPath, "utf8"));
|
|
1335
1642
|
if (pkg.name === "revealui" && pkg.private === true) {
|
|
1336
1643
|
return current;
|
|
1337
1644
|
}
|
|
1338
1645
|
} catch {
|
|
1339
1646
|
}
|
|
1340
1647
|
}
|
|
1341
|
-
const parent =
|
|
1648
|
+
const parent = path8.dirname(current);
|
|
1342
1649
|
if (parent === current) {
|
|
1343
1650
|
return null;
|
|
1344
1651
|
}
|
|
@@ -1347,7 +1654,7 @@ function findWorkspaceRoot(startDir = process.cwd()) {
|
|
|
1347
1654
|
}
|
|
1348
1655
|
|
|
1349
1656
|
// src/commands/db.ts
|
|
1350
|
-
var
|
|
1657
|
+
var logger9 = createLogger9({ prefix: "DB" });
|
|
1351
1658
|
function isPgCtlNotRunningError(error) {
|
|
1352
1659
|
return typeof error === "object" && error !== null && "exitCode" in error && error.exitCode === 3;
|
|
1353
1660
|
}
|
|
@@ -1388,18 +1695,18 @@ async function runDbInitCommand(options = {}) {
|
|
|
1388
1695
|
const pgdata = getPgDataOrThrow(env);
|
|
1389
1696
|
await requireCommand("initdb");
|
|
1390
1697
|
try {
|
|
1391
|
-
await
|
|
1698
|
+
await fs9.access(pgdata);
|
|
1392
1699
|
if (!options.force) {
|
|
1393
1700
|
throw new Error(`PostgreSQL is already initialized at ${pgdata}. Use --force to reset it.`);
|
|
1394
1701
|
}
|
|
1395
|
-
await
|
|
1702
|
+
await fs9.rm(pgdata, { recursive: true, force: true });
|
|
1396
1703
|
} catch (error) {
|
|
1397
1704
|
if (error instanceof Error && !error.message.includes("already initialized")) {
|
|
1398
1705
|
} else if (error instanceof Error) {
|
|
1399
1706
|
throw error;
|
|
1400
1707
|
}
|
|
1401
1708
|
}
|
|
1402
|
-
await
|
|
1709
|
+
await execa5(
|
|
1403
1710
|
"initdb",
|
|
1404
1711
|
["--locale=C.UTF-8", "--encoding=UTF8", "-D", pgdata, "--username=postgres"],
|
|
1405
1712
|
{
|
|
@@ -1408,17 +1715,17 @@ async function runDbInitCommand(options = {}) {
|
|
|
1408
1715
|
}
|
|
1409
1716
|
);
|
|
1410
1717
|
await writeLocalDbConfigs(pgdata);
|
|
1411
|
-
|
|
1718
|
+
logger9.success(`PostgreSQL initialized at ${pgdata}`);
|
|
1412
1719
|
}
|
|
1413
1720
|
async function runDbStartCommand() {
|
|
1414
1721
|
const root = getWorkspaceRootOrThrow();
|
|
1415
1722
|
const env = buildDbEnv(root);
|
|
1416
1723
|
const pgdata = getPgDataOrThrow(env);
|
|
1417
1724
|
await requireCommand("pg_ctl");
|
|
1418
|
-
await
|
|
1419
|
-
await
|
|
1725
|
+
await fs9.access(pgdata);
|
|
1726
|
+
await execa5(
|
|
1420
1727
|
"pg_ctl",
|
|
1421
|
-
["start", "-D", pgdata, "-l",
|
|
1728
|
+
["start", "-D", pgdata, "-l", path9.join(pgdata, "logfile"), "-o", `-k ${pgdata}`],
|
|
1422
1729
|
{
|
|
1423
1730
|
env,
|
|
1424
1731
|
stdio: "inherit"
|
|
@@ -1430,7 +1737,7 @@ async function runDbStopCommand() {
|
|
|
1430
1737
|
const env = buildDbEnv(root);
|
|
1431
1738
|
const pgdata = getPgDataOrThrow(env);
|
|
1432
1739
|
await requireCommand("pg_ctl");
|
|
1433
|
-
await
|
|
1740
|
+
await execa5("pg_ctl", ["stop", "-D", pgdata], {
|
|
1434
1741
|
env,
|
|
1435
1742
|
stdio: "inherit"
|
|
1436
1743
|
});
|
|
@@ -1441,13 +1748,13 @@ async function runDbStatusCommand() {
|
|
|
1441
1748
|
const pgdata = getPgDataOrThrow(env);
|
|
1442
1749
|
await requireCommand("pg_ctl");
|
|
1443
1750
|
try {
|
|
1444
|
-
await
|
|
1751
|
+
await execa5("pg_ctl", ["status", "-D", pgdata], {
|
|
1445
1752
|
env,
|
|
1446
1753
|
stdio: "inherit"
|
|
1447
1754
|
});
|
|
1448
1755
|
} catch (error) {
|
|
1449
1756
|
if (isPgCtlNotRunningError(error)) {
|
|
1450
|
-
|
|
1757
|
+
logger9.warn(`PostgreSQL is not running (PGDATA: ${pgdata})`);
|
|
1451
1758
|
return;
|
|
1452
1759
|
}
|
|
1453
1760
|
throw error;
|
|
@@ -1462,17 +1769,17 @@ async function runDbResetCommand(options = {}) {
|
|
|
1462
1769
|
const pgdata = getPgDataOrThrow(env);
|
|
1463
1770
|
if (await commandExists("pg_ctl")) {
|
|
1464
1771
|
try {
|
|
1465
|
-
await
|
|
1772
|
+
await execa5("pg_ctl", ["stop", "-D", pgdata], { env, stdio: "pipe" });
|
|
1466
1773
|
} catch {
|
|
1467
1774
|
}
|
|
1468
1775
|
}
|
|
1469
|
-
await
|
|
1776
|
+
await fs9.rm(pgdata, { recursive: true, force: true });
|
|
1470
1777
|
await runDbInitCommand({ force: false });
|
|
1471
1778
|
}
|
|
1472
1779
|
async function runDbMigrateCommand() {
|
|
1473
1780
|
const root = getWorkspaceRootOrThrow();
|
|
1474
1781
|
const env = buildDbEnv(root);
|
|
1475
|
-
await
|
|
1782
|
+
await execa5("pnpm", ["--filter", "@revealui/db", "db:migrate"], {
|
|
1476
1783
|
cwd: root,
|
|
1477
1784
|
env,
|
|
1478
1785
|
stdio: "inherit"
|
|
@@ -1483,7 +1790,7 @@ async function runDbCleanupCommand(options = {}) {
|
|
|
1483
1790
|
const env = { ...process.env };
|
|
1484
1791
|
if (options.dryRun) env.DRY_RUN = "true";
|
|
1485
1792
|
if (options.tables) env.TABLES = options.tables;
|
|
1486
|
-
await
|
|
1793
|
+
await execa5("pnpm", ["--filter", "@revealui/db", "db:cleanup"], {
|
|
1487
1794
|
cwd: root,
|
|
1488
1795
|
env,
|
|
1489
1796
|
stdio: "inherit"
|
|
@@ -1491,8 +1798,8 @@ async function runDbCleanupCommand(options = {}) {
|
|
|
1491
1798
|
}
|
|
1492
1799
|
|
|
1493
1800
|
// src/commands/dev.ts
|
|
1494
|
-
import { createLogger as
|
|
1495
|
-
import { execa as
|
|
1801
|
+
import { createLogger as createLogger12 } from "@revealui/setup/utils";
|
|
1802
|
+
import { execa as execa7 } from "execa";
|
|
1496
1803
|
|
|
1497
1804
|
// src/runtime/doctor.ts
|
|
1498
1805
|
function getMcpDetail(env) {
|
|
@@ -1511,6 +1818,140 @@ function getMcpDetail(env) {
|
|
|
1511
1818
|
detail: `mcp credentials configured for ${configuredKeys.join(", ")}`
|
|
1512
1819
|
};
|
|
1513
1820
|
}
|
|
1821
|
+
var ENV_VAR_SPECS = [
|
|
1822
|
+
// Core
|
|
1823
|
+
{
|
|
1824
|
+
key: "REVEALUI_SECRET",
|
|
1825
|
+
label: "App secret",
|
|
1826
|
+
required: false
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
key: "REVEALUI_ADMIN_EMAIL",
|
|
1830
|
+
label: "Admin email",
|
|
1831
|
+
required: false
|
|
1832
|
+
},
|
|
1833
|
+
// Database
|
|
1834
|
+
{
|
|
1835
|
+
key: "POSTGRES_URL",
|
|
1836
|
+
label: "PostgreSQL URL",
|
|
1837
|
+
required: true,
|
|
1838
|
+
validate: validateNeonUrl
|
|
1839
|
+
},
|
|
1840
|
+
// Stripe
|
|
1841
|
+
{
|
|
1842
|
+
key: "STRIPE_SECRET_KEY",
|
|
1843
|
+
label: "Stripe secret key",
|
|
1844
|
+
required: false,
|
|
1845
|
+
validate: validateStripeKey
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
key: "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
|
|
1849
|
+
label: "Stripe publishable key",
|
|
1850
|
+
required: false,
|
|
1851
|
+
validate: (v) => ({
|
|
1852
|
+
valid: v.startsWith("pk_test_") || v.startsWith("pk_live_"),
|
|
1853
|
+
message: "Must start with pk_test_ or pk_live_"
|
|
1854
|
+
})
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
key: "STRIPE_WEBHOOK_SECRET",
|
|
1858
|
+
label: "Stripe webhook secret",
|
|
1859
|
+
required: false,
|
|
1860
|
+
validate: (v) => ({
|
|
1861
|
+
valid: v.startsWith("whsec_"),
|
|
1862
|
+
message: "Must start with whsec_"
|
|
1863
|
+
})
|
|
1864
|
+
},
|
|
1865
|
+
// Supabase
|
|
1866
|
+
{
|
|
1867
|
+
key: "NEXT_PUBLIC_SUPABASE_URL",
|
|
1868
|
+
label: "Supabase URL",
|
|
1869
|
+
required: false,
|
|
1870
|
+
validate: validateSupabaseUrl
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
key: "SUPABASE_SERVICE_ROLE_KEY",
|
|
1874
|
+
label: "Supabase service role key",
|
|
1875
|
+
required: false,
|
|
1876
|
+
validate: (v) => ({
|
|
1877
|
+
valid: v.startsWith("eyJ"),
|
|
1878
|
+
message: "Must be a JWT (starts with eyJ)"
|
|
1879
|
+
})
|
|
1880
|
+
},
|
|
1881
|
+
// Services
|
|
1882
|
+
{
|
|
1883
|
+
key: "RESEND_API_KEY",
|
|
1884
|
+
label: "Resend API key",
|
|
1885
|
+
required: false,
|
|
1886
|
+
validate: (v) => ({
|
|
1887
|
+
valid: v.startsWith("re_"),
|
|
1888
|
+
message: "Must start with re_"
|
|
1889
|
+
})
|
|
1890
|
+
},
|
|
1891
|
+
// npm
|
|
1892
|
+
{
|
|
1893
|
+
key: "NPM_TOKEN",
|
|
1894
|
+
label: "npm publish token",
|
|
1895
|
+
required: false,
|
|
1896
|
+
validate: validateNpmToken
|
|
1897
|
+
},
|
|
1898
|
+
// License
|
|
1899
|
+
{
|
|
1900
|
+
key: "REVEALUI_LICENSE_PRIVATE_KEY",
|
|
1901
|
+
label: "License signing key",
|
|
1902
|
+
required: false
|
|
1903
|
+
},
|
|
1904
|
+
// CRON
|
|
1905
|
+
{
|
|
1906
|
+
key: "REVEALUI_CRON_SECRET",
|
|
1907
|
+
label: "Cron secret",
|
|
1908
|
+
required: false
|
|
1909
|
+
},
|
|
1910
|
+
// AI (open-model inference)
|
|
1911
|
+
{
|
|
1912
|
+
key: "BITNET_BASE_URL",
|
|
1913
|
+
label: "BitNet base URL",
|
|
1914
|
+
required: false
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
key: "OLLAMA_BASE_URL",
|
|
1918
|
+
label: "Ollama base URL",
|
|
1919
|
+
required: false
|
|
1920
|
+
}
|
|
1921
|
+
];
|
|
1922
|
+
function maskValue(value) {
|
|
1923
|
+
if (value.length <= 8) return "***";
|
|
1924
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
1925
|
+
}
|
|
1926
|
+
function gatherCredentialChecks(env) {
|
|
1927
|
+
const checks = [];
|
|
1928
|
+
for (const spec of ENV_VAR_SPECS) {
|
|
1929
|
+
const value = env[spec.key];
|
|
1930
|
+
if (!value) {
|
|
1931
|
+
checks.push({
|
|
1932
|
+
id: `env:${spec.key}`,
|
|
1933
|
+
ok: !spec.required,
|
|
1934
|
+
detail: spec.required ? `${spec.label} \u2014 missing (required)` : `${spec.label} \u2014 not set`
|
|
1935
|
+
});
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
if (spec.validate) {
|
|
1939
|
+
const result = spec.validate(value);
|
|
1940
|
+
checks.push({
|
|
1941
|
+
id: `env:${spec.key}`,
|
|
1942
|
+
ok: result.valid,
|
|
1943
|
+
detail: result.valid ? `${spec.label} \u2014 ${maskValue(value)}` : `${spec.label} \u2014 ${result.message} (got ${maskValue(value)})`
|
|
1944
|
+
});
|
|
1945
|
+
} else {
|
|
1946
|
+
checks.push({
|
|
1947
|
+
id: `env:${spec.key}`,
|
|
1948
|
+
ok: true,
|
|
1949
|
+
detail: `${spec.label} \u2014 set`
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return checks;
|
|
1954
|
+
}
|
|
1514
1955
|
async function gatherDoctorReport(cwd = process.cwd(), env = process.env) {
|
|
1515
1956
|
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
1516
1957
|
const dbConfig = resolveLocalDbConfig(workspaceRoot ?? cwd, env);
|
|
@@ -1524,6 +1965,7 @@ async function gatherDoctorReport(cwd = process.cwd(), env = process.env) {
|
|
|
1524
1965
|
const dockerOk = await commandExists("docker");
|
|
1525
1966
|
const postgresReachable = dbTarget === "local" ? await isTcpReachable("127.0.0.1", 5432, 1e3) : false;
|
|
1526
1967
|
const mcp = getMcpDetail(env);
|
|
1968
|
+
const credentials = gatherCredentialChecks(env);
|
|
1527
1969
|
return {
|
|
1528
1970
|
workspaceRoot,
|
|
1529
1971
|
dbTarget,
|
|
@@ -1582,7 +2024,8 @@ async function gatherDoctorReport(cwd = process.cwd(), env = process.env) {
|
|
|
1582
2024
|
id: "mcp",
|
|
1583
2025
|
ok: mcp.ok,
|
|
1584
2026
|
detail: mcp.detail
|
|
1585
|
-
}
|
|
2027
|
+
},
|
|
2028
|
+
...credentials
|
|
1586
2029
|
]
|
|
1587
2030
|
};
|
|
1588
2031
|
}
|
|
@@ -1595,22 +2038,22 @@ function formatDoctorReport(report) {
|
|
|
1595
2038
|
}
|
|
1596
2039
|
|
|
1597
2040
|
// src/utils/dev-config.ts
|
|
1598
|
-
import
|
|
1599
|
-
import
|
|
2041
|
+
import fs10 from "fs";
|
|
2042
|
+
import path10 from "path";
|
|
1600
2043
|
function getDevConfigPath(startDir = process.cwd()) {
|
|
1601
2044
|
const workspaceRoot = findWorkspaceRoot(startDir);
|
|
1602
2045
|
if (!workspaceRoot) {
|
|
1603
2046
|
return null;
|
|
1604
2047
|
}
|
|
1605
|
-
return
|
|
2048
|
+
return path10.join(workspaceRoot, ".revealui", "dev.json");
|
|
1606
2049
|
}
|
|
1607
2050
|
function readDevConfig(startDir = process.cwd()) {
|
|
1608
2051
|
const configPath = getDevConfigPath(startDir);
|
|
1609
|
-
if (!(configPath &&
|
|
2052
|
+
if (!(configPath && fs10.existsSync(configPath))) {
|
|
1610
2053
|
return {};
|
|
1611
2054
|
}
|
|
1612
2055
|
try {
|
|
1613
|
-
return JSON.parse(
|
|
2056
|
+
return JSON.parse(fs10.readFileSync(configPath, "utf8"));
|
|
1614
2057
|
} catch {
|
|
1615
2058
|
return {};
|
|
1616
2059
|
}
|
|
@@ -1620,15 +2063,15 @@ function writeDevConfig(config, startDir = process.cwd()) {
|
|
|
1620
2063
|
if (!configPath) {
|
|
1621
2064
|
throw new Error("RevealUI workspace root not found");
|
|
1622
2065
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
2066
|
+
fs10.mkdirSync(path10.dirname(configPath), { recursive: true });
|
|
2067
|
+
fs10.writeFileSync(`${configPath}`, `${JSON.stringify(config, null, 2)}
|
|
1625
2068
|
`, "utf8");
|
|
1626
2069
|
return configPath;
|
|
1627
2070
|
}
|
|
1628
2071
|
|
|
1629
2072
|
// src/commands/doctor.ts
|
|
1630
|
-
import { createLogger as
|
|
1631
|
-
var
|
|
2073
|
+
import { createLogger as createLogger10 } from "@revealui/setup/utils";
|
|
2074
|
+
var logger10 = createLogger10({ prefix: "Doctor" });
|
|
1632
2075
|
function getDoctorFixPlan(report) {
|
|
1633
2076
|
const attempted = [];
|
|
1634
2077
|
const skipped = [];
|
|
@@ -1686,7 +2129,7 @@ async function runDoctorCommand(options = {}) {
|
|
|
1686
2129
|
`);
|
|
1687
2130
|
if (options.fix) {
|
|
1688
2131
|
if (fixPlan.attempted.length === 0) {
|
|
1689
|
-
|
|
2132
|
+
logger10.warn("No safe automatic fixes available");
|
|
1690
2133
|
} else {
|
|
1691
2134
|
await applyDoctorFixes(report);
|
|
1692
2135
|
report = await gatherDoctorReport();
|
|
@@ -1695,7 +2138,7 @@ async function runDoctorCommand(options = {}) {
|
|
|
1695
2138
|
}
|
|
1696
2139
|
}
|
|
1697
2140
|
if (report.checks.some((check) => !check.ok)) {
|
|
1698
|
-
|
|
2141
|
+
logger10.warn("Some checks failed");
|
|
1699
2142
|
if (options.strict || options.json || process.env.CI) {
|
|
1700
2143
|
process.exitCode = 1;
|
|
1701
2144
|
}
|
|
@@ -1703,15 +2146,15 @@ async function runDoctorCommand(options = {}) {
|
|
|
1703
2146
|
}
|
|
1704
2147
|
|
|
1705
2148
|
// src/commands/shell.ts
|
|
1706
|
-
import { createLogger as
|
|
1707
|
-
import { execa as
|
|
1708
|
-
var
|
|
2149
|
+
import { createLogger as createLogger11 } from "@revealui/setup/utils";
|
|
2150
|
+
import { execa as execa6 } from "execa";
|
|
2151
|
+
var logger11 = createLogger11({ prefix: "Shell" });
|
|
1709
2152
|
async function runShellCommand(options = {}) {
|
|
1710
2153
|
if (!(options.inside || process.env.IN_NIX_SHELL) && await commandExists("nix")) {
|
|
1711
2154
|
const workspaceRoot = findWorkspaceRoot();
|
|
1712
2155
|
if (workspaceRoot) {
|
|
1713
2156
|
const reentryArgs = options.ensure ? ["dev", "up"] : ["dev", "status"];
|
|
1714
|
-
await
|
|
2157
|
+
await execa6(
|
|
1715
2158
|
"nix",
|
|
1716
2159
|
[
|
|
1717
2160
|
"develop",
|
|
@@ -1750,12 +2193,12 @@ async function runShellCommand(options = {}) {
|
|
|
1750
2193
|
}
|
|
1751
2194
|
process.stdout.write(`${formatDoctorReport(freshReport)}
|
|
1752
2195
|
`);
|
|
1753
|
-
|
|
2196
|
+
logger11.info("Use `revealui doctor --json` for machine-readable status.");
|
|
1754
2197
|
return false;
|
|
1755
2198
|
}
|
|
1756
2199
|
|
|
1757
2200
|
// src/commands/dev.ts
|
|
1758
|
-
var
|
|
2201
|
+
var logger12 = createLogger12({ prefix: "Dev" });
|
|
1759
2202
|
var DEV_PROFILES = {
|
|
1760
2203
|
local: {},
|
|
1761
2204
|
agent: {
|
|
@@ -1881,21 +2324,21 @@ async function runDevUpCommand(options = {}) {
|
|
|
1881
2324
|
process.stdout.write(`${formatDevActions(plan)}
|
|
1882
2325
|
`);
|
|
1883
2326
|
if (plan.dryRun) {
|
|
1884
|
-
|
|
2327
|
+
logger12.info("Dry run only; no migrations or services were started.");
|
|
1885
2328
|
return;
|
|
1886
2329
|
}
|
|
1887
2330
|
if (options.fix) {
|
|
1888
2331
|
await runDoctorCommand({ fix: true });
|
|
1889
2332
|
}
|
|
1890
2333
|
await runDbMigrateCommand();
|
|
1891
|
-
|
|
2334
|
+
logger12.success("Database migration complete");
|
|
1892
2335
|
if (shouldIncludeMcp(resolved.include)) {
|
|
1893
2336
|
const workspaceRoot = findWorkspaceRoot();
|
|
1894
2337
|
if (!workspaceRoot) {
|
|
1895
2338
|
throw new Error("RevealUI workspace root not found");
|
|
1896
2339
|
}
|
|
1897
|
-
|
|
1898
|
-
await
|
|
2340
|
+
logger12.info("Validating MCP setup");
|
|
2341
|
+
await execa7("pnpm", ["setup:mcp"], {
|
|
1899
2342
|
cwd: workspaceRoot,
|
|
1900
2343
|
stdio: "inherit"
|
|
1901
2344
|
});
|
|
@@ -1905,8 +2348,8 @@ async function runDevUpCommand(options = {}) {
|
|
|
1905
2348
|
if (!workspaceRoot) {
|
|
1906
2349
|
throw new Error("RevealUI workspace root not found");
|
|
1907
2350
|
}
|
|
1908
|
-
|
|
1909
|
-
await
|
|
2351
|
+
logger12.info(`Starting dev script: ${resolved.script}`);
|
|
2352
|
+
await execa7("pnpm", [resolved.script], {
|
|
1910
2353
|
cwd: workspaceRoot,
|
|
1911
2354
|
stdio: "inherit"
|
|
1912
2355
|
});
|
|
@@ -1963,7 +2406,7 @@ async function runDevProfileSetCommand(profile) {
|
|
|
1963
2406
|
);
|
|
1964
2407
|
}
|
|
1965
2408
|
const configPath = writeDevConfig({ defaultProfile: profile });
|
|
1966
|
-
|
|
2409
|
+
logger12.success(`Default dev profile set to "${profile}" in ${configPath}`);
|
|
1967
2410
|
}
|
|
1968
2411
|
async function runDevProfileShowCommand(options = {}) {
|
|
1969
2412
|
const configuredProfile = getConfiguredProfile() ?? null;
|
|
@@ -1976,11 +2419,223 @@ async function runDevProfileShowCommand(options = {}) {
|
|
|
1976
2419
|
`);
|
|
1977
2420
|
}
|
|
1978
2421
|
|
|
2422
|
+
// src/commands/terminal.ts
|
|
2423
|
+
import { copyFile, mkdir, readdir, stat } from "fs/promises";
|
|
2424
|
+
import { homedir as homedir2, platform } from "os";
|
|
2425
|
+
import { dirname, join as join2, resolve } from "path";
|
|
2426
|
+
import { createLogger as createLogger13 } from "@revealui/setup/utils";
|
|
2427
|
+
var logger13 = createLogger13({ prefix: "Terminal" });
|
|
2428
|
+
async function dirExists(path11) {
|
|
2429
|
+
try {
|
|
2430
|
+
const s = await stat(path11);
|
|
2431
|
+
return s.isDirectory();
|
|
2432
|
+
} catch {
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
async function fileExists(path11) {
|
|
2437
|
+
try {
|
|
2438
|
+
const s = await stat(path11);
|
|
2439
|
+
return s.isFile();
|
|
2440
|
+
} catch {
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function getMacProfiles(home) {
|
|
2445
|
+
return [
|
|
2446
|
+
{
|
|
2447
|
+
name: "iTerm2",
|
|
2448
|
+
sourceFile: "iterm2-revealui.json",
|
|
2449
|
+
destPath: join2(
|
|
2450
|
+
home,
|
|
2451
|
+
"Library",
|
|
2452
|
+
"Application Support",
|
|
2453
|
+
"iTerm2",
|
|
2454
|
+
"DynamicProfiles",
|
|
2455
|
+
"revealui.json"
|
|
2456
|
+
),
|
|
2457
|
+
postInstall: 'Profile loaded automatically. Select "RevealUI" in iTerm2 > Settings > Profiles.',
|
|
2458
|
+
detect: async () => dirExists(join2(home, "Library", "Application Support", "iTerm2"))
|
|
2459
|
+
},
|
|
2460
|
+
{
|
|
2461
|
+
name: "Terminal.app",
|
|
2462
|
+
sourceFile: "Terminal.app-RevealUI.terminal",
|
|
2463
|
+
destPath: join2(home, "Desktop", "RevealUI.terminal"),
|
|
2464
|
+
postInstall: "Double-click ~/Desktop/RevealUI.terminal to import, then set as default in Terminal > Settings > Profiles.",
|
|
2465
|
+
detect: async () => fileExists("/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal")
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
name: "Alacritty",
|
|
2469
|
+
sourceFile: "alacritty-revealui.toml",
|
|
2470
|
+
destPath: join2(home, ".config", "alacritty", "revealui.toml"),
|
|
2471
|
+
postInstall: 'Add `import = ["~/.config/alacritty/revealui.toml"]` to your alacritty.toml [general] section.',
|
|
2472
|
+
detect: async () => dirExists(join2(home, ".config", "alacritty"))
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
name: "Kitty",
|
|
2476
|
+
sourceFile: "kitty-revealui.conf",
|
|
2477
|
+
destPath: join2(home, ".config", "kitty", "revealui.conf"),
|
|
2478
|
+
postInstall: "Add `include revealui.conf` to your ~/.config/kitty/kitty.conf.",
|
|
2479
|
+
detect: async () => dirExists(join2(home, ".config", "kitty"))
|
|
2480
|
+
}
|
|
2481
|
+
];
|
|
2482
|
+
}
|
|
2483
|
+
function getLinuxProfiles(home) {
|
|
2484
|
+
return [
|
|
2485
|
+
{
|
|
2486
|
+
name: "Alacritty",
|
|
2487
|
+
sourceFile: "alacritty-revealui.toml",
|
|
2488
|
+
destPath: join2(home, ".config", "alacritty", "revealui.toml"),
|
|
2489
|
+
postInstall: 'Add `import = ["~/.config/alacritty/revealui.toml"]` to your alacritty.toml [general] section.',
|
|
2490
|
+
detect: async () => dirExists(join2(home, ".config", "alacritty"))
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
name: "Kitty",
|
|
2494
|
+
sourceFile: "kitty-revealui.conf",
|
|
2495
|
+
destPath: join2(home, ".config", "kitty", "revealui.conf"),
|
|
2496
|
+
postInstall: "Add `include revealui.conf` to your ~/.config/kitty/kitty.conf.",
|
|
2497
|
+
detect: async () => dirExists(join2(home, ".config", "kitty"))
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
name: "GNOME Terminal",
|
|
2501
|
+
sourceFile: "gnome-terminal-revealui.dconf",
|
|
2502
|
+
destPath: join2(home, ".config", "revealui", "gnome-terminal-revealui.dconf"),
|
|
2503
|
+
postInstall: "Import with: dconf load /org/gnome/terminal/legacy/profiles:/ < ~/.config/revealui/gnome-terminal-revealui.dconf",
|
|
2504
|
+
detect: async () => {
|
|
2505
|
+
const gnomeConfigDir = join2(home, ".config", "dconf");
|
|
2506
|
+
return dirExists(gnomeConfigDir);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
];
|
|
2510
|
+
}
|
|
2511
|
+
async function findConfigDir() {
|
|
2512
|
+
const monorepoPath = resolve(
|
|
2513
|
+
dirname(new URL(import.meta.url).pathname),
|
|
2514
|
+
"..",
|
|
2515
|
+
"..",
|
|
2516
|
+
"..",
|
|
2517
|
+
"..",
|
|
2518
|
+
"config",
|
|
2519
|
+
"terminal"
|
|
2520
|
+
);
|
|
2521
|
+
if (await dirExists(monorepoPath)) return monorepoPath;
|
|
2522
|
+
const npmPath = resolve(dirname(new URL(import.meta.url).pathname), "..", "config", "terminal");
|
|
2523
|
+
if (await dirExists(npmPath)) return npmPath;
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
async function runTerminalInstallCommand(options) {
|
|
2527
|
+
const os2 = platform();
|
|
2528
|
+
const home = homedir2();
|
|
2529
|
+
if (os2 !== "darwin" && os2 !== "linux") {
|
|
2530
|
+
logger13.error(
|
|
2531
|
+
`Unsupported platform: ${os2}. Terminal profiles are available for macOS and Linux.`
|
|
2532
|
+
);
|
|
2533
|
+
if (os2 === "win32") {
|
|
2534
|
+
logger13.info("For Windows Terminal, copy config/terminal/ profiles manually.");
|
|
2535
|
+
}
|
|
2536
|
+
process.exitCode = 1;
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const profiles = os2 === "darwin" ? getMacProfiles(home) : getLinuxProfiles(home);
|
|
2540
|
+
if (options.list) {
|
|
2541
|
+
if (options.json) {
|
|
2542
|
+
const detected = await Promise.all(
|
|
2543
|
+
profiles.map(async (p) => ({
|
|
2544
|
+
name: p.name,
|
|
2545
|
+
sourceFile: p.sourceFile,
|
|
2546
|
+
destPath: p.destPath,
|
|
2547
|
+
detected: await p.detect()
|
|
2548
|
+
}))
|
|
2549
|
+
);
|
|
2550
|
+
process.stdout.write(`${JSON.stringify({ platform: os2, profiles: detected }, null, 2)}
|
|
2551
|
+
`);
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
logger13.header("Available Terminal Profiles");
|
|
2555
|
+
logger13.info(`Platform: ${os2 === "darwin" ? "macOS" : "Linux"}`);
|
|
2556
|
+
for (const profile of profiles) {
|
|
2557
|
+
const detected = await profile.detect();
|
|
2558
|
+
const icon = detected ? "[detected]" : "[not found]";
|
|
2559
|
+
logger13.info(` ${profile.name} ${icon} \u2014 ${profile.sourceFile}`);
|
|
2560
|
+
}
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
const configDir = await findConfigDir();
|
|
2564
|
+
if (!configDir) {
|
|
2565
|
+
logger13.error("Could not find config/terminal/ directory.");
|
|
2566
|
+
logger13.info(
|
|
2567
|
+
"Run this command from the RevealUI monorepo root, or install via npx create-revealui."
|
|
2568
|
+
);
|
|
2569
|
+
process.exitCode = 1;
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const sourceFiles = await readdir(configDir);
|
|
2573
|
+
let targetProfiles = profiles;
|
|
2574
|
+
if (options.terminal) {
|
|
2575
|
+
const match = profiles.find((p) => p.name.toLowerCase() === options.terminal?.toLowerCase());
|
|
2576
|
+
if (!match) {
|
|
2577
|
+
logger13.error(`Unknown terminal: ${options.terminal}`);
|
|
2578
|
+
logger13.info(`Available: ${profiles.map((p) => p.name).join(", ")}`);
|
|
2579
|
+
process.exitCode = 1;
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
targetProfiles = [match];
|
|
2583
|
+
}
|
|
2584
|
+
let installed = 0;
|
|
2585
|
+
let skipped = 0;
|
|
2586
|
+
logger13.header("RevealUI Terminal Profile Installer");
|
|
2587
|
+
logger13.info(`Platform: ${os2 === "darwin" ? "macOS" : "Linux"}`);
|
|
2588
|
+
for (const profile of targetProfiles) {
|
|
2589
|
+
const detected = await profile.detect();
|
|
2590
|
+
if (!(detected || options.terminal)) {
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
if (!detected && options.terminal) {
|
|
2594
|
+
logger13.warn(`${profile.name} not detected, installing anyway (--terminal flag).`);
|
|
2595
|
+
}
|
|
2596
|
+
if (!sourceFiles.includes(profile.sourceFile)) {
|
|
2597
|
+
logger13.warn(`Source file missing: ${profile.sourceFile} \u2014 skipping ${profile.name}`);
|
|
2598
|
+
skipped++;
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
const sourcePath = join2(configDir, profile.sourceFile);
|
|
2602
|
+
const destPath = profile.destPath;
|
|
2603
|
+
if (await fileExists(destPath)) {
|
|
2604
|
+
if (!options.force) {
|
|
2605
|
+
logger13.warn(`${profile.name}: ${destPath} already exists. Use --force to overwrite.`);
|
|
2606
|
+
skipped++;
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
2611
|
+
await copyFile(sourcePath, destPath);
|
|
2612
|
+
installed++;
|
|
2613
|
+
logger13.success(`${profile.name}: installed to ${destPath}`);
|
|
2614
|
+
logger13.info(` ${profile.postInstall}`);
|
|
2615
|
+
}
|
|
2616
|
+
if (installed === 0 && skipped === 0) {
|
|
2617
|
+
logger13.info("No supported terminal emulators detected.");
|
|
2618
|
+
logger13.info(`Supported: ${profiles.map((p) => p.name).join(", ")}`);
|
|
2619
|
+
logger13.info("Use --terminal <name> to install for a specific terminal.");
|
|
2620
|
+
} else if (installed > 0) {
|
|
2621
|
+
logger13.success(
|
|
2622
|
+
`Installed ${installed} profile(s). ${skipped > 0 ? `Skipped ${skipped}.` : ""}`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
if (options.json) {
|
|
2626
|
+
process.stdout.write(`${JSON.stringify({ installed, skipped, platform: os2 }, null, 2)}
|
|
2627
|
+
`);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async function runTerminalListCommand(options) {
|
|
2631
|
+
await runTerminalInstallCommand({ list: true, json: options.json });
|
|
2632
|
+
}
|
|
2633
|
+
|
|
1979
2634
|
// src/cli.ts
|
|
1980
|
-
var
|
|
2635
|
+
var logger14 = createLogger14({ prefix: "CLI" });
|
|
1981
2636
|
function configureCreateCommand(command, legacyName) {
|
|
1982
2637
|
command.description("Create a new RevealUI project").argument("[project-name]", "Name of the project").option("-t, --template <name>", "Template to use (basic-blog, e-commerce, portfolio)").option("--skip-git", "Skip git initialization", false).option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip all prompts and use defaults", false).action(async (projectName, options) => {
|
|
1983
|
-
|
|
2638
|
+
logger14.header(legacyName ? "Create RevealUI Project" : "RevealUI Create");
|
|
1984
2639
|
await runCreateFlow(projectName, options);
|
|
1985
2640
|
});
|
|
1986
2641
|
return command;
|
|
@@ -2075,6 +2730,23 @@ function createCli() {
|
|
|
2075
2730
|
devProfile.command("show").description("Show the configured default dev profile").option("--json", "Output machine-readable JSON", false).action(async (options) => {
|
|
2076
2731
|
await runDevProfileShowCommand(options);
|
|
2077
2732
|
});
|
|
2733
|
+
const auth = program.command("auth").description("Manage npm registry authentication and publish tokens");
|
|
2734
|
+
auth.command("status").description("Show current npm authentication state").option("--json", "Output machine-readable JSON", false).action(async (options) => {
|
|
2735
|
+
await runAuthStatusCommand(options);
|
|
2736
|
+
});
|
|
2737
|
+
auth.command("set-token").description("Store an npm token via RevVault and configure .npmrc").option("--token <value>", "npm token (or set NPM_TOKEN env var)").option("--skip-vault", "Skip RevVault storage", false).option("--skip-npmrc", "Skip .npmrc modification", false).action(async (options) => {
|
|
2738
|
+
await runAuthSetTokenCommand(options);
|
|
2739
|
+
});
|
|
2740
|
+
auth.command("verify").description("Verify the full npm auth chain (env \u2192 RevVault \u2192 .npmrc \u2192 registry)").option("--json", "Output machine-readable JSON", false).action(async (options) => {
|
|
2741
|
+
await runAuthVerifyCommand(options);
|
|
2742
|
+
});
|
|
2743
|
+
const terminal = program.command("terminal").description("Install RevealUI terminal profiles for your terminal emulator");
|
|
2744
|
+
terminal.command("install").description("Detect and install terminal profiles for supported emulators").option("--terminal <name>", "Install for a specific terminal (e.g. iTerm2, Alacritty, Kitty)").option("--force", "Overwrite existing profile files", false).option("--json", "Output machine-readable JSON", false).action(async (options) => {
|
|
2745
|
+
await runTerminalInstallCommand(options);
|
|
2746
|
+
});
|
|
2747
|
+
terminal.command("list").description("List available terminal profiles and detected emulators").option("--json", "Output machine-readable JSON", false).action(async (options) => {
|
|
2748
|
+
await runTerminalListCommand(options);
|
|
2749
|
+
});
|
|
2078
2750
|
program.command("shell").description("Deprecated alias for `revealui dev shell`").option("--ensure", "Initialize/start the local DB when possible", false).option("--json", "Output machine-readable JSON", false).option("--inside", "Internal flag used after re-entering nix develop", false).action(async (options) => {
|
|
2079
2751
|
await runDevUpCommand({
|
|
2080
2752
|
ensure: options.ensure,
|