@dashclaw/cli 0.8.0 → 0.8.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/lib/up/index.js +57 -6
- package/package.json +1 -1
package/lib/up/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// without touching the network, Docker, or a real Next server.
|
|
14
14
|
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import { homedir } from 'node:os';
|
|
18
19
|
import { join } from 'node:path';
|
|
@@ -111,6 +112,32 @@ export function rewriteEnvDatabaseUrl(appDir, databaseUrl, logger = console) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Mint a one-time browser sign-in token into the app's .env.local, BEFORE the
|
|
117
|
+
* server starts (Next reads .env.local at boot). The server consumes it on
|
|
118
|
+
* first use (app/api/auth/local); the 15-minute expiry bounds replay. This is
|
|
119
|
+
* what lets `dashclaw up` open a browser that lands already signed in instead
|
|
120
|
+
* of a login form asking for a password the user never saw.
|
|
121
|
+
* Best-effort: returns null (→ fall back to /setup) when .env.local is absent.
|
|
122
|
+
*/
|
|
123
|
+
export function mintLoginToken(appDir, logger = console) {
|
|
124
|
+
try {
|
|
125
|
+
const envPath = join(appDir, '.env.local');
|
|
126
|
+
if (!existsSync(envPath)) return null;
|
|
127
|
+
const token = randomBytes(24).toString('base64url');
|
|
128
|
+
const line = `DASHCLAW_LOGIN_OTT=${token}.${Date.now() + 15 * 60_000}`;
|
|
129
|
+
const src = readFileSync(envPath, 'utf8');
|
|
130
|
+
const out = /^DASHCLAW_LOGIN_OTT=.*$/m.test(src)
|
|
131
|
+
? src.replace(/^DASHCLAW_LOGIN_OTT=.*$/m, line)
|
|
132
|
+
: `${src}${src.endsWith('\n') || src === '' ? '' : '\n'}${line}\n`;
|
|
133
|
+
writeFileSync(envPath, out);
|
|
134
|
+
return token;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
logger.error(`[warn] Could not mint a browser sign-in link: ${e.message} — sign in with the admin password instead.`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
/** Default process-liveness probe: signal 0 succeeds iff the pid exists. */
|
|
115
142
|
export function defaultProcessAlive(pid) {
|
|
116
143
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
@@ -125,6 +152,7 @@ export function realDeps() {
|
|
|
125
152
|
chooseDbMode,
|
|
126
153
|
provisionDatabase,
|
|
127
154
|
runSetupScript: runSetupScriptReal,
|
|
155
|
+
mintLoginToken,
|
|
128
156
|
buildApp,
|
|
129
157
|
startServer,
|
|
130
158
|
waitForHealth,
|
|
@@ -210,10 +238,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
210
238
|
if (out.ok === false) throw new Error(out.error || 'Setup failed.');
|
|
211
239
|
apiKey = out.apiKey;
|
|
212
240
|
inst = saveInstance(baseDir, { apiKey });
|
|
213
|
-
// setup.mjs
|
|
214
|
-
//
|
|
241
|
+
// setup.mjs prints the password to ITS stderr, but runSetupScript pipes
|
|
242
|
+
// (and on success discards) that stream — so this line is the one place
|
|
243
|
+
// the operator ever sees it. It is also saved to <appDir>/.env.local.
|
|
215
244
|
if (out.adminPassword) {
|
|
216
|
-
logger.
|
|
245
|
+
logger.log(`[ok] Dashboard admin password: ${out.adminPassword} (also saved to ${join(appDir, '.env.local')})`);
|
|
217
246
|
}
|
|
218
247
|
inst = checkpoint(baseDir, 'setup_done');
|
|
219
248
|
}
|
|
@@ -242,7 +271,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
242
271
|
// Pid alive but health check failed — fall through to a fresh start.
|
|
243
272
|
}
|
|
244
273
|
}
|
|
274
|
+
// One-time browser sign-in: only mintable for a server WE are about to start
|
|
275
|
+
// (Next reads .env.local at boot; a reused server would never see the token).
|
|
276
|
+
let loginToken = null;
|
|
245
277
|
if (!reusedServer) {
|
|
278
|
+
loginToken = deps.mintLoginToken?.(appDir, logger) ?? null;
|
|
246
279
|
child = deps.startServer({ appDir, port, logger });
|
|
247
280
|
inst = saveInstance(baseDir, { pid: child.pid });
|
|
248
281
|
try {
|
|
@@ -265,14 +298,32 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
265
298
|
connect = answer !== 'n' && answer !== 'no';
|
|
266
299
|
}
|
|
267
300
|
if (connect) {
|
|
268
|
-
|
|
301
|
+
// Hooks install is auxiliary to `up` (the dashboard is already running);
|
|
302
|
+
// a missing Python must not kill steps 8+ — the browser sign-in moment.
|
|
303
|
+
// No checkpoint on failure, so the next `dashclaw up` retries the install.
|
|
304
|
+
try {
|
|
305
|
+
await deps.installClaude({ endpoint: baseUrl, apiKey });
|
|
306
|
+
inst = checkpoint(baseDir, 'connected');
|
|
307
|
+
} catch (e) {
|
|
308
|
+
logger.error(`[warn] Claude Code not connected: ${e.message}`);
|
|
309
|
+
logger.error(' The dashboard still works; re-run `dashclaw up` after fixing this to retry.');
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
inst = checkpoint(baseDir, 'connected'); // declining is still a completed decision
|
|
269
313
|
}
|
|
270
|
-
inst = checkpoint(baseDir, 'connected'); // declining is still a completed decision
|
|
271
314
|
}
|
|
272
315
|
|
|
273
316
|
// 8. open -----------------------------------------------------------------
|
|
317
|
+
// With a fresh token the browser opens /login?ott=... and lands signed in
|
|
318
|
+
// (redirecting on to /setup); otherwise it opens /setup directly and any
|
|
319
|
+
// protected page will ask for the admin password.
|
|
320
|
+
const signInUrl = loginToken
|
|
321
|
+
? `${baseUrl}/login?ott=${loginToken}&next=${encodeURIComponent('/setup')}`
|
|
322
|
+
: `${baseUrl}/setup`;
|
|
274
323
|
if (!args.noBrowser) {
|
|
275
|
-
deps.openBrowser(
|
|
324
|
+
deps.openBrowser(signInUrl, logger);
|
|
325
|
+
} else if (loginToken) {
|
|
326
|
+
logger.log(`Sign in (link valid ~15 min, single use): ${signInUrl}`);
|
|
276
327
|
}
|
|
277
328
|
logger.log(`Done. First steps: ${baseUrl}/connect`);
|
|
278
329
|
|