@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.
Files changed (2) hide show
  1. package/lib/up/index.js +57 -6
  2. 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 already prints the password once (to stderr) and writes it to
214
- // .env.local; we deliberately do NOT echo it again here no secrets twice.
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.error('[ok] First admin created credentials written to .env.local (printed once).');
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
- await deps.installClaude({ endpoint: baseUrl, apiKey });
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(`${baseUrl}/setup`, logger);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashclaw/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "DashClaw terminal client — approve agent actions and diagnose your instance",
5
5
  "type": "module",
6
6
  "keywords": [