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