@ahmednawaz/crank 0.1.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/bin/crank.js ADDED
@@ -0,0 +1,737 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Crank — one command for any repo / IDE.
6
+ *
7
+ * cd your-app
8
+ * npx crank
9
+ *
10
+ * Auto: init → companion → MCP (stdio local / HTTP Replit) → npm run dev when present.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const http = require('http');
16
+ const { spawn } = require('child_process');
17
+ const {
18
+ initProject,
19
+ PACKAGE_ROOT,
20
+ writeReplitGuide
21
+ } = require('../lib/init');
22
+ const { detectEnvironment } = require('../lib/detect');
23
+ const { env } = require('../lib/env');
24
+ const { loadTeamEnvFile } = require('../lib/load-team-env');
25
+ const {
26
+ resolveUrls,
27
+ writeRuntimeManifest,
28
+ applyUrlEnv,
29
+ readRuntimeManifest
30
+ } = require('../lib/urls');
31
+ const { resolveProjectRoot, describeEnvironment } = require('../lib/resolve-project');
32
+
33
+ const LOG_FILE = env('LOG', '/tmp/crank-companion.log');
34
+ const PID_FILE = env('PID_FILE', '/tmp/crank-companion.pid');
35
+
36
+ function printHelp() {
37
+ process.stdout.write(`Usage: crank [command] [project-root]
38
+
39
+ Commands:
40
+ (default) Init + start everything (companion, MCP, dev server when present)
41
+ setup Same as init — Retune-style one-time config (alias for init)
42
+ init Only write IDE / Vite / MCP setup into the project
43
+ mcp Start stdio MCP server (used by npx crank mcp in IDE configs)
44
+ start Start companion (assumes init already done)
45
+ stop Stop daemon companion + HTTP MCP listeners
46
+ doctor Print detected environment + runtime URLs
47
+ status Print whether companion /health responds
48
+
49
+ Flags:
50
+ --daemon Detach durable supervisor (default on local; Replit stays foreground)
51
+ --no-daemon Keep companion in this terminal
52
+ --no-mcp Skip writing MCP config files
53
+ --no-vite Skip Vite config patch
54
+ --no-open Do not open the setup page in a browser
55
+ --no-dev Do not auto-run npm run dev
56
+ --dev Force npm run dev even if not auto-detected
57
+ --no-claude Skip Claude Desktop MCP merge
58
+ --http-mcp Force HTTP MCP (Replit auto-enables this)
59
+ -h, --help Show help
60
+
61
+ Examples:
62
+ cd ~/code/my-app && npx crank setup && npx crank
63
+ npx crank
64
+ npx crank mcp
65
+ npx crank stop
66
+ `);
67
+ }
68
+
69
+ function parseArgs(argv) {
70
+ const flags = {
71
+ mcp: true,
72
+ vite: true,
73
+ open: true,
74
+ dev: null,
75
+ httpMcp: false,
76
+ claude: true,
77
+ help: false,
78
+ daemon: null,
79
+ explicit: {}
80
+ };
81
+ const positional = [];
82
+ for (const arg of argv) {
83
+ if (arg === '-h' || arg === '--help') {
84
+ flags.help = true;
85
+ } else if (arg === '--no-mcp') {
86
+ flags.mcp = false;
87
+ } else if (arg === '--no-vite') {
88
+ flags.vite = false;
89
+ } else if (arg === '--no-open') {
90
+ flags.open = false;
91
+ } else if (arg === '--dev') {
92
+ flags.dev = true;
93
+ flags.explicit.dev = true;
94
+ } else if (arg === '--no-dev') {
95
+ flags.dev = false;
96
+ flags.explicit.dev = true;
97
+ } else if (arg === '--no-claude') {
98
+ flags.claude = false;
99
+ } else if (arg === '--http-mcp') {
100
+ flags.httpMcp = true;
101
+ flags.explicit.httpMcp = true;
102
+ } else if (arg === '--daemon') {
103
+ flags.daemon = true;
104
+ flags.explicit.daemon = true;
105
+ } else if (arg === '--no-daemon') {
106
+ flags.daemon = false;
107
+ flags.explicit.daemon = true;
108
+ } else if (!arg.startsWith('-')) {
109
+ positional.push(arg);
110
+ }
111
+ }
112
+ let command = 'run';
113
+ let projectRoot = process.cwd();
114
+ const cmds = new Set(['init', 'setup', 'start', 'doctor', 'stop', 'status', 'mcp']);
115
+ if (positional[0] === 'setup') {
116
+ command = 'init';
117
+ } else if (cmds.has(positional[0])) {
118
+ command = positional[0];
119
+ if (positional[1]) {
120
+ projectRoot = path.resolve(positional[1]);
121
+ }
122
+ } else if (positional[0]) {
123
+ projectRoot = path.resolve(positional[0]);
124
+ }
125
+
126
+ let resolved = null;
127
+ if (command !== 'mcp' && command !== 'stop' && command !== 'status') {
128
+ resolved = resolveProjectRoot(projectRoot);
129
+ projectRoot = resolved.projectRoot;
130
+ }
131
+
132
+ return { command, projectRoot, flags, resolved };
133
+ }
134
+
135
+ function loadEnvFile(file) {
136
+ if (!fs.existsSync(file)) {
137
+ return;
138
+ }
139
+ for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
140
+ const trimmed = line.trim();
141
+ if (!trimmed || trimmed.startsWith('#')) {
142
+ continue;
143
+ }
144
+ const eq = trimmed.indexOf('=');
145
+ if (eq < 1) {
146
+ continue;
147
+ }
148
+ const key = trimmed.slice(0, eq).trim();
149
+ let value = trimmed.slice(eq + 1).trim();
150
+ if (
151
+ (value.startsWith('"') && value.endsWith('"')) ||
152
+ (value.startsWith("'") && value.endsWith("'"))
153
+ ) {
154
+ value = value.slice(1, -1);
155
+ }
156
+ if (process.env[key] == null) {
157
+ process.env[key] = value;
158
+ }
159
+ }
160
+ }
161
+
162
+ function loadAllEnv(projectRoot) {
163
+ loadTeamEnvFile(PACKAGE_ROOT);
164
+ loadEnvFile(path.join(PACKAGE_ROOT, '.env'));
165
+ loadEnvFile(path.join(projectRoot, '.env'));
166
+ loadEnvFile(path.join(projectRoot, '.env.local'));
167
+ if (!process.env.CRANK_PROJECT_ROOT) {
168
+ process.env.CRANK_PROJECT_ROOT = path.resolve(projectRoot);
169
+ }
170
+ }
171
+
172
+ function hasDevScript(detectEnv) {
173
+ return Boolean(detectEnv.packageJson?.scripts?.dev);
174
+ }
175
+
176
+ function applySmartDefaults(flags, detectEnv) {
177
+ if (detectEnv.isReplit) {
178
+ flags.httpMcp = true;
179
+ if (!flags.explicit.daemon) {
180
+ flags.daemon = false;
181
+ }
182
+ } else if (!flags.explicit.daemon) {
183
+ flags.daemon = true;
184
+ }
185
+
186
+ if (flags.dev == null) {
187
+ flags.dev = hasDevScript(detectEnv);
188
+ }
189
+
190
+ if (flags.httpMcp || detectEnv.isReplit) {
191
+ process.env.CRANK_MCP_MODE = 'http';
192
+ }
193
+ }
194
+
195
+ function companionBaseUrl() {
196
+ const publicUrl = process.env.CRANK_PUBLIC_URL;
197
+ if (publicUrl) {
198
+ return publicUrl.replace(/\/$/, '');
199
+ }
200
+ const urls = resolveUrls({
201
+ isReplit: Boolean(
202
+ process.env.REPL_ID ||
203
+ process.env.REPLIT_DEV_DOMAIN ||
204
+ process.env.REPL_SLUG
205
+ )
206
+ });
207
+ return urls.companionPublic;
208
+ }
209
+
210
+ function checkHealth(timeoutMs = 1500, baseUrl) {
211
+ const base = baseUrl || companionBaseUrl();
212
+ return new Promise((resolve) => {
213
+ const req = http.get(`${base}/health`, { timeout: timeoutMs }, (res) => {
214
+ let body = '';
215
+ res.on('data', (c) => {
216
+ body += c;
217
+ });
218
+ res.on('end', () => {
219
+ try {
220
+ resolve({ ok: res.statusCode === 200, status: res.statusCode, body: JSON.parse(body) });
221
+ } catch {
222
+ resolve({ ok: res.statusCode === 200, status: res.statusCode, body: null });
223
+ }
224
+ });
225
+ });
226
+ req.on('error', () => resolve({ ok: false }));
227
+ req.on('timeout', () => {
228
+ req.destroy();
229
+ resolve({ ok: false });
230
+ });
231
+ });
232
+ }
233
+
234
+ function openUrl(url) {
235
+ const platform = process.platform;
236
+ let cmd;
237
+ let args;
238
+ if (platform === 'darwin') {
239
+ cmd = 'open';
240
+ args = [url];
241
+ } else if (platform === 'win32') {
242
+ cmd = 'cmd';
243
+ args = ['/c', 'start', '', url];
244
+ } else {
245
+ cmd = 'xdg-open';
246
+ args = [url];
247
+ }
248
+ try {
249
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
250
+ } catch {
251
+ // ignore
252
+ }
253
+ }
254
+
255
+ function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
256
+ const setup = runtime?.companion?.publicUrl || companionBaseUrl();
257
+
258
+ console.log('');
259
+ console.log('┌──────────────────────────────────────────┐');
260
+ console.log('│ Crank — one-command copy tooling │');
261
+ console.log('└──────────────────────────────────────────┘');
262
+ console.log(` Project: ${projectRoot}`);
263
+ if (resolved?.repoRoot && resolved.repoRoot !== projectRoot) {
264
+ console.log(` Repo: ${resolved.repoRoot} (${resolved.reason})`);
265
+ } else if (resolved?.reason) {
266
+ console.log(` Detected: ${resolved.source} — ${resolved.reason}`);
267
+ }
268
+ console.log(` Companion: ${setup}`);
269
+ if (runtime?.mcp?.mode === 'http' && runtime?.mcp?.endpoint) {
270
+ console.log(` MCP (HTTP): ${runtime.mcp.endpoint}`);
271
+ }
272
+ if (process.env.CRANK_TEAM_TOKEN) {
273
+ console.log(' Team lock: ON (CRANK_TEAM_TOKEN set)');
274
+ } else {
275
+ console.log(' Team lock: OFF — set CRANK_TEAM_TOKEN in .env to require a passphrase');
276
+ }
277
+ if (modeLabel) {
278
+ console.log(` Mode: ${modeLabel}`);
279
+ }
280
+ if (initResult?.wrote?.length) {
281
+ console.log(` Config: ${initResult.wrote.slice(0, 5).join(', ')}`);
282
+ }
283
+ console.log('');
284
+
285
+ if (detectEnv.isReplit) {
286
+ console.log(' Replit:');
287
+ console.log(' • Keep this process running (companion + HTTP MCP + dev).');
288
+ console.log(' • Register MCP URL in Agent → Integrations (see .crank/replit.md).');
289
+ console.log(' • Open the webview — select copy → Get options.');
290
+ } else if (detectEnv.hasVite && hasDevScript(detectEnv)) {
291
+ console.log(' Ready:');
292
+ console.log(' • Dev server starting (or already running).');
293
+ console.log(' • Open your app — Crank panel loads automatically.');
294
+ console.log(' • Reload MCP in Cursor / VS Code / Claude after first init.');
295
+ } else {
296
+ console.log(' Ready:');
297
+ console.log(` • Open ${setup} and drag the Crank bookmarklet.`);
298
+ console.log(' • Click it on any page → Select → Get options.');
299
+ console.log(' • Reload MCP in Cursor / VS Code / Claude after first init.');
300
+ }
301
+ console.log('');
302
+ }
303
+
304
+ function writePid(pid) {
305
+ try {
306
+ fs.writeFileSync(PID_FILE, String(pid) + '\n', 'utf8');
307
+ } catch {
308
+ // ignore
309
+ }
310
+ }
311
+
312
+ function readPid() {
313
+ try {
314
+ const raw = fs.readFileSync(PID_FILE, 'utf8').trim();
315
+ const pid = Number(raw);
316
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
317
+ } catch {
318
+ return null;
319
+ }
320
+ }
321
+
322
+ function clearPid() {
323
+ try {
324
+ fs.unlinkSync(PID_FILE);
325
+ } catch {
326
+ // ignore
327
+ }
328
+ }
329
+
330
+ function isPidAlive(pid) {
331
+ try {
332
+ process.kill(pid, 0);
333
+ return true;
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ function killPortListeners(port) {
340
+ try {
341
+ const out = require('child_process').execSync(`lsof -tiTCP:${port} -sTCP:LISTEN`, {
342
+ encoding: 'utf8'
343
+ });
344
+ for (const line of out.split(/\s+/).filter(Boolean)) {
345
+ const pid = Number(line);
346
+ if (pid > 0 && pid !== process.pid) {
347
+ try {
348
+ process.kill(pid, 'SIGTERM');
349
+ } catch {
350
+ // ignore
351
+ }
352
+ }
353
+ }
354
+ } catch {
355
+ // nothing listening
356
+ }
357
+ }
358
+
359
+ function startHttpMcp(foreground) {
360
+ process.env.CRANK_MCP_MODE = 'http';
361
+ const mcpPath = path.join(PACKAGE_ROOT, 'mcp-server', 'index.js');
362
+ const stdio = foreground
363
+ ? 'inherit'
364
+ : ['ignore', fs.openSync(LOG_FILE, 'a'), fs.openSync(LOG_FILE, 'a')];
365
+ return spawn(process.execPath, [mcpPath], {
366
+ stdio,
367
+ env: process.env,
368
+ cwd: PACKAGE_ROOT
369
+ });
370
+ }
371
+
372
+ function startNpmDev(projectRoot) {
373
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
374
+ return spawn(npm, ['run', 'dev'], {
375
+ stdio: 'inherit',
376
+ cwd: projectRoot,
377
+ env: process.env
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Foreground or in-process supervisor: companion + optional HTTP MCP.
383
+ */
384
+ function runCompanionSupervisor(projectRoot, { foreground, httpMcp }) {
385
+ process.env.CRANK_PROJECT_ROOT = projectRoot;
386
+ const serverPath = path.join(PACKAGE_ROOT, 'companion', 'server.js');
387
+ let companionChild = null;
388
+ let mcpChild = null;
389
+ let shuttingDown = false;
390
+ let restartDelayMs = 400;
391
+ let spawnGeneration = 0;
392
+
393
+ writePid(process.pid);
394
+
395
+ const log = (msg) => {
396
+ const line = `[Crank supervisor] ${msg}`;
397
+ if (foreground) {
398
+ console.error(line);
399
+ } else {
400
+ try {
401
+ fs.appendFileSync(LOG_FILE, line + '\n');
402
+ } catch {
403
+ // ignore
404
+ }
405
+ }
406
+ };
407
+
408
+ function spawnMcpOnce() {
409
+ if (!httpMcp || shuttingDown) return;
410
+ if (mcpChild) return;
411
+ mcpChild = startHttpMcp(foreground);
412
+ log(`spawned http mcp pid=${mcpChild.pid}`);
413
+ mcpChild.on('exit', (code, signal) => {
414
+ mcpChild = null;
415
+ if (shuttingDown) return;
416
+ log(`http mcp exited code=${code} signal=${signal || ''} — restart in 800ms`);
417
+ setTimeout(spawnMcpOnce, 800);
418
+ });
419
+ }
420
+
421
+ function spawnCompanionOnce() {
422
+ if (shuttingDown) return;
423
+ const gen = ++spawnGeneration;
424
+ const stdio = foreground
425
+ ? 'inherit'
426
+ : ['ignore', fs.openSync(LOG_FILE, 'a'), fs.openSync(LOG_FILE, 'a')];
427
+ companionChild = spawn(process.execPath, [serverPath], {
428
+ stdio,
429
+ env: process.env,
430
+ cwd: PACKAGE_ROOT
431
+ });
432
+ log(`spawned companion pid=${companionChild.pid} gen=${gen}`);
433
+
434
+ companionChild.on('exit', async (code, signal) => {
435
+ if (gen !== spawnGeneration) return;
436
+ companionChild = null;
437
+ if (shuttingDown) return;
438
+
439
+ if (code === 0 && !signal) {
440
+ const health = await checkHealth(800);
441
+ if (health.ok) {
442
+ log('port already served by healthy companion — supervisor idle');
443
+ const watch = setInterval(async () => {
444
+ if (shuttingDown) {
445
+ clearInterval(watch);
446
+ return;
447
+ }
448
+ const h = await checkHealth(800);
449
+ if (!h.ok) {
450
+ clearInterval(watch);
451
+ log('health lost — respawning companion');
452
+ restartDelayMs = 400;
453
+ spawnCompanionOnce();
454
+ }
455
+ }, 5000);
456
+ return;
457
+ }
458
+ }
459
+
460
+ log(
461
+ `companion exited code=${code} signal=${signal || ''} — restart in ${restartDelayMs}ms`
462
+ );
463
+ setTimeout(() => {
464
+ restartDelayMs = Math.min(restartDelayMs * 2, 8000);
465
+ spawnCompanionOnce();
466
+ }, restartDelayMs);
467
+ });
468
+
469
+ companionChild.on('spawn', () => {
470
+ restartDelayMs = 400;
471
+ });
472
+ }
473
+
474
+ const shutdown = (signal) => {
475
+ shuttingDown = true;
476
+ log(`shutdown ${signal}`);
477
+ for (const child of [companionChild, mcpChild]) {
478
+ if (child) {
479
+ try {
480
+ child.kill(signal);
481
+ } catch {
482
+ // ignore
483
+ }
484
+ }
485
+ }
486
+ clearPid();
487
+ process.exit(0);
488
+ };
489
+ process.on('SIGINT', () => shutdown('SIGINT'));
490
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
491
+
492
+ spawnCompanionOnce();
493
+ spawnMcpOnce();
494
+ }
495
+
496
+ function launchDetachedDaemon(projectRoot, argvFlags) {
497
+ const args = [__filename, 'start', '--daemon', '--no-open', projectRoot];
498
+ if (argvFlags.httpMcp) {
499
+ args.push('--http-mcp');
500
+ }
501
+ const outFd = fs.openSync(LOG_FILE, 'a');
502
+ const child = spawn(process.execPath, args, {
503
+ detached: true,
504
+ stdio: ['ignore', outFd, outFd],
505
+ env: {
506
+ ...process.env,
507
+ CRANK_SUPERVISOR: '1',
508
+ CRANK_PROJECT_ROOT: projectRoot
509
+ },
510
+ cwd: PACKAGE_ROOT
511
+ });
512
+ writePid(child.pid);
513
+ child.unref();
514
+ console.log(`[Crank] Daemon supervisor started (pid ${child.pid})`);
515
+ console.log(`[Crank] Log: ${LOG_FILE}`);
516
+ console.log(`[Crank] Stop: npx crank stop`);
517
+ if (argvFlags.open) {
518
+ setTimeout(() => openUrl(companionBaseUrl()), 900);
519
+ }
520
+ }
521
+
522
+ async function stopDaemon() {
523
+ const pid = readPid();
524
+ if (pid && isPidAlive(pid)) {
525
+ try {
526
+ try {
527
+ process.kill(-pid, 'SIGTERM');
528
+ } catch {
529
+ process.kill(pid, 'SIGTERM');
530
+ }
531
+ console.log(`[Crank] Sent SIGTERM to supervisor pid ${pid}`);
532
+ } catch (err) {
533
+ console.error(`[Crank] Could not signal pid ${pid}:`, err.message);
534
+ }
535
+ } else if (pid) {
536
+ console.log(`[Crank] Stale PID file (${pid}); clearing`);
537
+ } else {
538
+ console.log('[Crank] No PID file; stopping listeners on companion/MCP ports if any');
539
+ }
540
+ await new Promise((r) => setTimeout(r, 500));
541
+ killPortListeners(process.env.CRANK_PORT || '3344');
542
+ killPortListeners(process.env.CRANK_MCP_HTTP_PORT || '3345');
543
+ clearPid();
544
+ await new Promise((r) => setTimeout(r, 400));
545
+ const health = await checkHealth(800);
546
+ console.log(
547
+ health.ok
548
+ ? '[Crank] Warning: /health still responds — another process may own the port'
549
+ : '[Crank] Companion stopped'
550
+ );
551
+ }
552
+
553
+ function prepareRuntime(projectRoot, detectEnv) {
554
+ const urls = resolveUrls(detectEnv);
555
+ applyUrlEnv(urls);
556
+ const runtime = writeRuntimeManifest(projectRoot, detectEnv);
557
+ if (detectEnv.isReplit) {
558
+ writeReplitGuide(projectRoot, detectEnv, { wrote: [] }, runtime);
559
+ }
560
+ return runtime;
561
+ }
562
+
563
+ async function main() {
564
+ const { command, projectRoot, flags: rawFlags, resolved } = parseArgs(process.argv.slice(2));
565
+ const flags = { ...rawFlags };
566
+ if (flags.help) {
567
+ printHelp();
568
+ process.exit(0);
569
+ }
570
+
571
+ loadAllEnv(projectRoot);
572
+ process.env.CRANK_PROJECT_ROOT = path.resolve(projectRoot);
573
+
574
+ if (command === 'mcp') {
575
+ process.env.CRANK_MCP_MODE = process.env.CRANK_MCP_MODE || 'stdio';
576
+ const mcpPath = path.join(PACKAGE_ROOT, 'mcp-server', 'index.js');
577
+ const child = spawn(process.execPath, [mcpPath], {
578
+ stdio: 'inherit',
579
+ env: process.env,
580
+ cwd: PACKAGE_ROOT
581
+ });
582
+ child.on('exit', (code) => process.exit(code || 0));
583
+ return;
584
+ }
585
+
586
+ if (command === 'status') {
587
+ const health = await checkHealth(2000);
588
+ const pid = readPid();
589
+ console.log(
590
+ JSON.stringify(
591
+ {
592
+ url: companionBaseUrl(),
593
+ healthy: !!health.ok,
594
+ pidFile: PID_FILE,
595
+ supervisorPid: pid,
596
+ supervisorAlive: pid ? isPidAlive(pid) : false,
597
+ health: health.body || null
598
+ },
599
+ null,
600
+ 2
601
+ )
602
+ );
603
+ process.exit(health.ok ? 0 : 1);
604
+ }
605
+
606
+ if (command === 'stop') {
607
+ await stopDaemon();
608
+ return;
609
+ }
610
+
611
+ if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
612
+ console.error(`Project root not found: ${projectRoot}`);
613
+ process.exit(1);
614
+ }
615
+
616
+ const detectEnv = detectEnvironment(projectRoot);
617
+ applySmartDefaults(flags, detectEnv);
618
+
619
+ if (command === 'doctor') {
620
+ const runtime = prepareRuntime(projectRoot, detectEnv);
621
+ console.log(
622
+ JSON.stringify(
623
+ {
624
+ projectRoot,
625
+ resolved,
626
+ packageRoot: PACKAGE_ROOT,
627
+ environment: detectEnv,
628
+ runtime,
629
+ flags,
630
+ commands: {
631
+ setup: 'npx crank setup',
632
+ start: 'npx crank',
633
+ mcp: 'npx crank mcp',
634
+ agentPaste: 'Apply the pending Crank change'
635
+ }
636
+ },
637
+ null,
638
+ 2
639
+ )
640
+ );
641
+ return;
642
+ }
643
+
644
+ let initResult = null;
645
+ if (command === 'init' || command === 'run') {
646
+ initResult = initProject(projectRoot, {
647
+ mcp: flags.mcp,
648
+ vite: flags.vite,
649
+ claude: flags.claude,
650
+ scripts: true
651
+ });
652
+ if (command === 'init') {
653
+ const runtime = prepareRuntime(projectRoot, detectEnv);
654
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'init only');
655
+ console.log('Init complete. Run `npx crank` to start everything.');
656
+ return;
657
+ }
658
+ }
659
+
660
+ const runtime = prepareRuntime(projectRoot, detectEnv);
661
+ const httpMcp = flags.httpMcp || detectEnv.isReplit;
662
+
663
+ if (process.env.CRANK_SUPERVISOR === '1' && flags.daemon) {
664
+ process.env.CRANK_PROJECT_ROOT = projectRoot;
665
+ runCompanionSupervisor(projectRoot, { foreground: false, httpMcp });
666
+ return;
667
+ }
668
+
669
+ if (flags.daemon && !flags.dev) {
670
+ const existing = await checkHealth(800);
671
+ if (existing.ok) {
672
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon (already running)');
673
+ console.log('[Crank] Companion already healthy — leaving it running.');
674
+ console.log(`[Crank] ${companionBaseUrl()}/health`);
675
+ return;
676
+ }
677
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon');
678
+ launchDetachedDaemon(projectRoot, flags);
679
+ await new Promise((r) => setTimeout(r, 1200));
680
+ const health = await checkHealth(2000);
681
+ console.log(health.ok ? '[Crank] Health OK' : `[Crank] Health not ready — check ${LOG_FILE}`);
682
+ return;
683
+ }
684
+
685
+ if (flags.daemon && flags.dev) {
686
+ const existing = await checkHealth(800);
687
+ if (!existing.ok) {
688
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon + dev');
689
+ launchDetachedDaemon(projectRoot, { ...flags, httpMcp });
690
+ await new Promise((r) => setTimeout(r, 1500));
691
+ } else {
692
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'dev (companion already up)');
693
+ }
694
+ if (flags.open) {
695
+ openUrl(companionBaseUrl());
696
+ }
697
+ const devChild = startNpmDev(projectRoot);
698
+ devChild.on('exit', (code) => process.exit(code || 0));
699
+ return;
700
+ }
701
+
702
+ printBanner(
703
+ projectRoot,
704
+ detectEnv,
705
+ initResult,
706
+ runtime,
707
+ detectEnv.isReplit ? 'Replit foreground' : 'foreground'
708
+ );
709
+
710
+ if (flags.open) {
711
+ setTimeout(() => openUrl(runtime.companion.publicUrl), 800);
712
+ }
713
+
714
+ let devChild = null;
715
+ if (flags.dev) {
716
+ devChild = startNpmDev(projectRoot);
717
+ }
718
+
719
+ const shutdownDev = (signal) => {
720
+ if (devChild) {
721
+ try {
722
+ devChild.kill(signal);
723
+ } catch {
724
+ // ignore
725
+ }
726
+ }
727
+ };
728
+ process.on('SIGINT', () => shutdownDev('SIGINT'));
729
+ process.on('SIGTERM', () => shutdownDev('SIGTERM'));
730
+
731
+ runCompanionSupervisor(projectRoot, { foreground: true, httpMcp });
732
+ }
733
+
734
+ main().catch((err) => {
735
+ console.error(err);
736
+ process.exit(1);
737
+ });