yaver-cli 1.99.222 → 1.99.224

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-cli",
3
- "version": "1.99.222",
3
+ "version": "1.99.224",
4
4
  "mcpName": "io.github.kivanccakmak/yaver",
5
5
  "description": "Unified npm bootstrap for the Yaver agent, SDK injection, and local-first developer runtime",
6
6
  "bin": {
@@ -0,0 +1,531 @@
1
+ 'use strict';
2
+
3
+ // `yaver run dev[:target]` — start dev servers across a monorepo.
4
+ //
5
+ // Mirrors `yaver deploy`'s detection: a find/grep scan with framework
6
+ // classification, plus optional `yaver.deploy.json` overrides (a
7
+ // per-target `dev` field, or a top-level `dev` block). Where deploy
8
+ // maps each dir to a build+upload command, run-dev maps it to a hot-
9
+ // reload command (npm run dev, npx convex dev, npx expo start,
10
+ // npx wrangler dev, flutter run, docker compose up).
11
+ //
12
+ // Default with no target is `web` (single dev server, stdio inherited
13
+ // so reload/interactive keys work). `dev:all` starts every detected
14
+ // target concurrently with prefixed/coloured logs; SIGINT propagates
15
+ // to all children.
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { spawn, spawnSync } = require('child_process');
20
+
21
+ const CONFIG_FILE = 'yaver.deploy.json';
22
+
23
+ // ── Detection constants (same shape as deploy.js — kept local to keep
24
+ // deploy's blast radius zero) ───────────────────────────────────────
25
+ const PRUNE_DIRS = [
26
+ 'node_modules', '.git', 'build', 'dist', '.next', '.expo', 'Pods',
27
+ '.yaver', 'vendor', 'target', '.gradle', 'DerivedData', '.turbo',
28
+ '.cache', 'out', '.svelte-kit', '.output', 'coverage', '.venv',
29
+ '__pycache__', '.dart_tool',
30
+ ];
31
+ const MARKER_FILES = [
32
+ 'package.json', 'pubspec.yaml', 'convex.json',
33
+ 'wrangler.toml', 'wrangler.jsonc', 'wrangler.json',
34
+ 'config.toml', 'Dockerfile', 'docker-compose.yml',
35
+ 'docker-compose.yaml', 'compose.yaml', 'compose.yml',
36
+ 'Package.swift', 'build.gradle', 'build.gradle.kts',
37
+ 'AndroidManifest.xml', 'netlify.toml', 'vercel.json',
38
+ 'next.config.*', 'astro.config.*', 'svelte.config.*',
39
+ 'nuxt.config.*', 'remix.config.*', 'gatsby-config.*', 'vite.config.*',
40
+ ];
41
+ const MARKER_DIRS = ['convex', 'supabase', '*.xcodeproj', '*.xcworkspace'];
42
+ const MAXDEPTH = 6;
43
+
44
+ // Aliases for dev targets. backend resolves to convex|supabase (first
45
+ // that detected); frontend/web/front resolve to whichever web target
46
+ // fingerprints in this repo.
47
+ const BUILTIN_ALIASES = {
48
+ backend: ['convex', 'supabase'],
49
+ frontend: 'web',
50
+ front: 'web',
51
+ };
52
+
53
+ const RUN_HELP = `
54
+ yaver run — start dev servers across a monorepo
55
+
56
+ Usage:
57
+ yaver run dev Start the web dev server (default target)
58
+ yaver run dev:web Web (Next/Astro/Vite/SvelteKit/Cloudflare etc.)
59
+ yaver run dev:mobile Expo / React Native / Flutter dev launcher
60
+ yaver run dev:convex npx convex dev (alias: dev:backend)
61
+ yaver run dev:supabase Local Supabase stack
62
+ yaver run dev:docker docker compose up
63
+ yaver run dev:all Run every detected dev target in parallel
64
+ yaver run dev:list Show resolved dev targets + their command
65
+ yaver run dev --dry-run Resolve + print, do not execute
66
+
67
+ Overrides in ${CONFIG_FILE} at the repo root, either co-located:
68
+ { "targets": { "web": { "dir": "web", "dev": "npm run dev" } } }
69
+ or a dedicated dev block:
70
+ { "dev": { "web": { "dir": "web", "run": "npm run dev", "env": {...} } } }
71
+
72
+ Options:
73
+ --dry-run, --print Print the plan, don't execute
74
+ --bail Stop everything if any child exits non-zero
75
+ --help Show this help
76
+ `;
77
+
78
+ // index.js routes `yaver run <token>` here when isLocalRunToken returns
79
+ // true. Anything else (e.g. future Go-agent `run` subcommands) falls
80
+ // through to the agent.
81
+ function isLocalRunToken(token) {
82
+ if (!token) return false;
83
+ if (token === '--help' || token === '-h') return true;
84
+ if (token === 'dev') return true;
85
+ if (token.startsWith('dev:')) return true;
86
+ return false;
87
+ }
88
+
89
+ // ── Detection helpers ────────────────────────────────────────────────
90
+ function nameGroup(names) {
91
+ const g = ['('];
92
+ names.forEach((n, i) => {
93
+ if (i) g.push('-o');
94
+ g.push('-name', n);
95
+ });
96
+ g.push(')');
97
+ return g;
98
+ }
99
+ function loadConfig(start) {
100
+ let dir = path.resolve(start);
101
+ let gitRoot = null;
102
+ // eslint-disable-next-line no-constant-condition
103
+ while (true) {
104
+ const cfgPath = path.join(dir, CONFIG_FILE);
105
+ if (fs.existsSync(cfgPath)) {
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
109
+ } catch (err) {
110
+ throw new Error(`${CONFIG_FILE} at ${cfgPath} is not valid JSON: ${err.message}`);
111
+ }
112
+ return { repoRoot: dir, config: parsed, configPath: cfgPath };
113
+ }
114
+ if (!gitRoot && fs.existsSync(path.join(dir, '.git'))) gitRoot = dir;
115
+ const parent = path.dirname(dir);
116
+ if (parent === dir) break;
117
+ dir = parent;
118
+ }
119
+ return { repoRoot: gitRoot || path.resolve(start), config: null, configPath: null };
120
+ }
121
+ function readJson(p) {
122
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
123
+ }
124
+ function scanRepo(repoRoot) {
125
+ const args = [
126
+ repoRoot, '-maxdepth', String(MAXDEPTH),
127
+ ...nameGroup(PRUNE_DIRS), '-prune', '-o',
128
+ '(', '-type', 'f', ...nameGroup(MARKER_FILES), ')', '-print', '-o',
129
+ '(', '-type', 'd', ...nameGroup(MARKER_DIRS), ')', '-print',
130
+ ];
131
+ const res = spawnSync('find', args, { encoding: 'utf8', timeout: 20000, maxBuffer: 16 * 1024 * 1024 });
132
+ if (res.error || res.status == null) return jsWalk(repoRoot, MAXDEPTH);
133
+ return res.stdout.split('\n').filter(Boolean);
134
+ }
135
+ function jsWalk(root, depth, base = root, acc = []) {
136
+ if (depth < 0) return acc;
137
+ let entries;
138
+ try { entries = fs.readdirSync(base, { withFileTypes: true }); } catch { return acc; }
139
+ for (const e of entries) {
140
+ if (e.isDirectory() && PRUNE_DIRS.includes(e.name)) continue;
141
+ const full = path.join(base, e.name);
142
+ const isMarkerDir = MARKER_DIRS.some((m) => m.startsWith('*')
143
+ ? e.name.endsWith(m.slice(1)) : e.name === m);
144
+ const isMarkerFile = MARKER_FILES.some((m) => m.startsWith('*.')
145
+ ? e.name.endsWith(m.slice(1)) : e.name === m);
146
+ if ((e.isDirectory() && isMarkerDir) || (e.isFile() && isMarkerFile)) acc.push(full);
147
+ if (e.isDirectory()) jsWalk(root, depth - 1, full, acc);
148
+ }
149
+ return acc;
150
+ }
151
+ function rel(repoRoot, p) {
152
+ const r = path.relative(repoRoot, p);
153
+ return r === '' ? '.' : r;
154
+ }
155
+ function depthOf(relDir) {
156
+ return relDir === '.' ? 0 : relDir.split(path.sep).length;
157
+ }
158
+
159
+ // Group markers by their owning directory.
160
+ function dirSignatures(repoRoot) {
161
+ const markers = scanRepo(repoRoot);
162
+ const dirs = {};
163
+ const note = (d, kind, name) => {
164
+ const k = rel(repoRoot, d);
165
+ (dirs[k] || (dirs[k] = { files: new Set(), dirs: new Set() }))[kind].add(name);
166
+ };
167
+ for (const m of markers) {
168
+ let st;
169
+ try { st = fs.statSync(m); } catch { continue; }
170
+ if (st.isDirectory()) note(path.dirname(m), 'dirs', path.basename(m));
171
+ else note(path.dirname(m), 'files', path.basename(m));
172
+ }
173
+ return dirs;
174
+ }
175
+
176
+ // Map each detected dir to its dev command. Prefer a shallower dir
177
+ // when two candidates resolve to the same canonical target.
178
+ function autoDetectDev(repoRoot) {
179
+ const dirs = dirSignatures(repoRoot);
180
+ const targets = {};
181
+
182
+ const consider = (name, spec) => {
183
+ const cur = targets[name];
184
+ if (!cur) { targets[name] = spec; return; }
185
+ // Strength order: has a real command > has an authoritative marker
186
+ // (e.g. convex.json wins over just a convex/ subdir of generated
187
+ // types) > shallower dir.
188
+ const score = (s) => (s.run ? 2 : 0) + (s.strong ? 1 : 0);
189
+ if (score(spec) > score(cur)
190
+ || (score(spec) === score(cur) && depthOf(spec.dir) < depthOf(cur.dir))) {
191
+ targets[name] = spec;
192
+ }
193
+ };
194
+
195
+ for (const [relDir, sig] of Object.entries(dirs)) {
196
+ const abs = path.join(repoRoot, relDir);
197
+ const pkg = sig.files.has('package.json') ? readJson(path.join(abs, 'package.json')) : null;
198
+ const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
199
+ const scripts = (pkg && pkg.scripts) || {};
200
+ const pubspec = sig.files.has('pubspec.yaml');
201
+ let flutter = false;
202
+ if (pubspec) {
203
+ try { flutter = /\bflutter\s*:/.test(fs.readFileSync(path.join(abs, 'pubspec.yaml'), 'utf8')); }
204
+ catch { flutter = true; }
205
+ }
206
+
207
+ const isExpo = !!deps.expo;
208
+ const isRN = !!deps['react-native'];
209
+
210
+ // Mobile dev
211
+ if (flutter) {
212
+ consider('mobile', {
213
+ dir: relDir, run: 'flutter run', framework: 'flutter',
214
+ description: `Flutter dev (${relDir})`,
215
+ });
216
+ } else if (isExpo) {
217
+ const cmd = scripts.start ? 'npm run start' : 'npx expo start';
218
+ consider('mobile', {
219
+ dir: relDir, run: cmd, framework: 'expo',
220
+ description: `Expo dev server (${relDir})`,
221
+ });
222
+ } else if (isRN) {
223
+ const cmd = scripts.start ? 'npm run start' : 'npx react-native start';
224
+ consider('mobile', {
225
+ dir: relDir, run: cmd, framework: 'react-native',
226
+ description: `React Native Metro (${relDir})`,
227
+ });
228
+ }
229
+
230
+ // Convex. `convex.json` is the authoritative backend marker; a
231
+ // bare `convex/` subdir alone is often just generated client types
232
+ // (e.g. web/convex/) — treated as a weak fallback.
233
+ if (sig.files.has('convex.json') || sig.dirs.has('convex')) {
234
+ const cmd = scripts.dev ? 'npm run dev' : 'npx convex dev';
235
+ consider('convex', {
236
+ dir: relDir, run: cmd, framework: 'convex',
237
+ strong: sig.files.has('convex.json'),
238
+ description: `Convex dev (${relDir})`,
239
+ });
240
+ }
241
+
242
+ // Supabase
243
+ if (sig.dirs.has('supabase') || (relDir.endsWith('supabase') && sig.files.has('config.toml'))) {
244
+ const sbDir = relDir.endsWith('supabase') ? path.dirname(relDir) || '.' : relDir;
245
+ consider('supabase', {
246
+ dir: sbDir, run: 'npx supabase start', framework: 'supabase',
247
+ description: `Supabase local stack (${sbDir})`,
248
+ });
249
+ }
250
+
251
+ // Web / jamstack / Cloudflare worker
252
+ const jamstack = deps.next || deps.astro || deps.vite || deps['@sveltejs/kit']
253
+ || deps.nuxt || deps['@remix-run/dev'] || deps.gatsby;
254
+ const cfConfig = sig.files.has('wrangler.toml') || sig.files.has('wrangler.jsonc')
255
+ || sig.files.has('wrangler.json') || deps.wrangler || deps['@opennextjs/cloudflare'];
256
+ if (jamstack || cfConfig) {
257
+ const fw = deps.next ? 'next' : deps.astro ? 'astro' : deps.nuxt ? 'nuxt'
258
+ : deps['@sveltejs/kit'] ? 'sveltekit' : deps['@remix-run/dev'] ? 'remix'
259
+ : deps.gatsby ? 'gatsby' : deps.vite ? 'vite' : 'jamstack';
260
+ let cmd = null;
261
+ if (scripts.dev) cmd = 'npm run dev';
262
+ else if (cfConfig) cmd = 'npx wrangler dev';
263
+ else if (scripts.start) cmd = 'npm run start';
264
+ consider('web', {
265
+ dir: relDir, run: cmd, framework: fw,
266
+ description: cmd ? `${fw} dev server (${relDir})`
267
+ : `${fw} app — declare dev command in ${CONFIG_FILE}`,
268
+ });
269
+ }
270
+
271
+ // Docker compose (logs stream — no -d)
272
+ const compose = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yaml', 'compose.yml']
273
+ .some((f) => sig.files.has(f));
274
+ if (compose) {
275
+ consider('docker', {
276
+ dir: relDir, run: 'docker compose up', framework: 'docker-compose',
277
+ description: `Docker Compose dev (${relDir})`,
278
+ });
279
+ }
280
+ }
281
+
282
+ return targets;
283
+ }
284
+
285
+ function resolveTable(repoRoot, config) {
286
+ const targets = autoDetectDev(repoRoot);
287
+
288
+ // Per-target `dev` override on the existing deploy spec.
289
+ if (config && config.targets) {
290
+ for (const [name, spec] of Object.entries(config.targets)) {
291
+ if (spec.dev == null) continue;
292
+ const devSpec = typeof spec.dev === 'string' ? { run: spec.dev } : spec.dev;
293
+ targets[name] = {
294
+ dir: devSpec.dir || spec.dir || (targets[name] && targets[name].dir) || '.',
295
+ run: devSpec.run || (devSpec.script ? `bash ${devSpec.script}` : null),
296
+ env: devSpec.env || spec.env || {},
297
+ description: devSpec.description || `${name} dev (config)`,
298
+ framework: spec.framework || 'config',
299
+ };
300
+ }
301
+ }
302
+ // Standalone `dev` block.
303
+ if (config && config.dev && typeof config.dev === 'object') {
304
+ for (const [name, spec] of Object.entries(config.dev)) {
305
+ targets[name] = {
306
+ dir: spec.dir || '.',
307
+ run: spec.run || (spec.script ? `bash ${spec.script}` : null),
308
+ env: spec.env || {},
309
+ description: spec.description || `${name} dev (config)`,
310
+ framework: spec.framework || 'config',
311
+ };
312
+ }
313
+ }
314
+
315
+ // Aliases: built-ins, plus top-level config.aliases (these are
316
+ // alias→target maps and are dev-safe), plus any dev-specific aliases.
317
+ const aliases = {
318
+ ...BUILTIN_ALIASES,
319
+ ...(config && config.aliases),
320
+ ...(config && config.dev && config.dev.aliases),
321
+ };
322
+ // Groups: only dev-specific groups; the deploy-side `config.groups`
323
+ // (e.g. `mobile: [ios, android]`) describes builds, not dev servers,
324
+ // so we deliberately don't inherit them here.
325
+ const groups = {
326
+ all: Object.keys(targets).filter((n) => targets[n].run),
327
+ ...(config && config.dev && config.dev.groups),
328
+ };
329
+
330
+ return { targets, groups, aliases };
331
+ }
332
+
333
+ // Requested name → ordered, deduped list of concrete target names.
334
+ function expand(name, table, seen = new Set(), trail = []) {
335
+ if (trail.includes(name)) {
336
+ throw new Error(`circular run group: ${[...trail, name].join(' → ')}`);
337
+ }
338
+ let canonical = name;
339
+ const aliased = table.aliases[name];
340
+ if (Array.isArray(aliased)) {
341
+ canonical = aliased.find((c) => table.targets[c] || table.groups[c]) || aliased[0];
342
+ } else if (aliased) {
343
+ canonical = aliased;
344
+ }
345
+ if (table.targets[canonical]) {
346
+ if (!seen.has(canonical)) { seen.add(canonical); return [canonical]; }
347
+ return [];
348
+ }
349
+ const group = table.groups[canonical] || table.groups[name];
350
+ if (group) {
351
+ const out = [];
352
+ for (const member of group) out.push(...expand(member, table, seen, [...trail, name]));
353
+ return out;
354
+ }
355
+ return [];
356
+ }
357
+
358
+ function printTarget(name, spec, repoRoot) {
359
+ const where = path.join(repoRoot, spec.dir || '.');
360
+ console.log(` ${name}${spec.framework ? ` [${spec.framework}]` : ''}`);
361
+ console.log(` dir: ${where}`);
362
+ if (spec.description) console.log(` what: ${spec.description}`);
363
+ if (spec.run) console.log(` run: ${spec.run}`);
364
+ else console.log(` run: ⚠️ not configured`);
365
+ }
366
+
367
+ // ── Concurrent runner ────────────────────────────────────────────────
368
+ // ANSI 16-colour palette for log prefixes; plain when stdout isn't a
369
+ // TTY (keeps CI/grep output clean).
370
+ const PREFIX_COLORS = [36, 32, 33, 35, 34, 31, 96, 92, 93, 95];
371
+ function paint(i, text) {
372
+ if (!process.stdout.isTTY) return text;
373
+ return `\x1b[${PREFIX_COLORS[i % PREFIX_COLORS.length]}m${text}\x1b[0m`;
374
+ }
375
+
376
+ function streamWithPrefix(stream, prefix, sink) {
377
+ let buf = '';
378
+ stream.setEncoding('utf8');
379
+ stream.on('data', (chunk) => {
380
+ buf += chunk;
381
+ const lines = buf.split('\n');
382
+ buf = lines.pop();
383
+ for (const line of lines) sink.write(`${prefix} ${line}\n`);
384
+ });
385
+ stream.on('end', () => { if (buf.length) sink.write(`${prefix} ${buf}\n`); });
386
+ }
387
+
388
+ function spawnTarget(name, spec, repoRoot, colorIdx, soloMode) {
389
+ const cwd = path.join(repoRoot, spec.dir || '.');
390
+ if (!fs.existsSync(cwd)) throw new Error(`target "${name}" dir does not exist: ${cwd}`);
391
+ if (!spec.run) throw new Error(`target "${name}" has no dev command (${spec.description})`);
392
+ const prefix = paint(colorIdx, `[${name}]`);
393
+ console.log(`${prefix} $ (cd ${cwd} && ${spec.run})`);
394
+ // Solo mode inherits stdio so interactive dev servers (expo, RN
395
+ // Metro, vite) keep their keyboard shortcuts. Multi-target mode
396
+ // pipes so we can prefix each line.
397
+ const child = spawn('bash', ['-lc', spec.run], {
398
+ cwd,
399
+ env: { ...process.env, ...(spec.env || {}) },
400
+ stdio: soloMode ? 'inherit' : ['ignore', 'pipe', 'pipe'],
401
+ });
402
+ if (!soloMode) {
403
+ streamWithPrefix(child.stdout, prefix, process.stdout);
404
+ streamWithPrefix(child.stderr, prefix, process.stderr);
405
+ }
406
+ return { name, child, prefix };
407
+ }
408
+
409
+ async function runDevTargets(plan, table, repoRoot, opts) {
410
+ const soloMode = plan.length === 1;
411
+ const procs = [];
412
+ let killing = false;
413
+
414
+ const killAll = (signal) => {
415
+ if (killing) return;
416
+ killing = true;
417
+ for (const p of procs) {
418
+ if (!p.child.killed && p.child.exitCode == null) {
419
+ try { p.child.kill(signal); } catch { /* ignore */ }
420
+ }
421
+ }
422
+ };
423
+
424
+ process.on('SIGINT', () => {
425
+ if (!soloMode) console.log('\n^C — stopping dev servers');
426
+ killAll('SIGINT');
427
+ });
428
+ process.on('SIGTERM', () => killAll('SIGTERM'));
429
+
430
+ for (let i = 0; i < plan.length; i++) {
431
+ try {
432
+ procs.push(spawnTarget(plan[i], table.targets[plan[i]], repoRoot, i, soloMode));
433
+ } catch (err) {
434
+ console.error(`❌ ${plan[i]}: ${err.message}`);
435
+ if (opts.bail) { killAll('SIGTERM'); process.exit(1); }
436
+ }
437
+ }
438
+ if (!procs.length) {
439
+ console.error('❌ no targets started');
440
+ process.exit(1);
441
+ }
442
+
443
+ const results = await Promise.all(procs.map((p) => new Promise((resolve) => {
444
+ p.child.on('exit', (code, signal) => {
445
+ if (!soloMode) console.log(`${p.prefix} exited (code=${code}, signal=${signal || 'none'})`);
446
+ if (opts.bail && code !== 0 && !killing) killAll('SIGTERM');
447
+ resolve({ name: p.name, code, signal });
448
+ });
449
+ })));
450
+
451
+ let worst = 0;
452
+ for (const r of results) if (r.code && r.code !== 0) worst = r.code;
453
+ process.exit(worst);
454
+ }
455
+
456
+ // ── Entry point ──────────────────────────────────────────────────────
457
+ async function run(args) {
458
+ const first = args[0];
459
+ if (!first || first === '--help' || first === '-h') {
460
+ console.log(RUN_HELP);
461
+ process.exit(0);
462
+ }
463
+
464
+ // Parse "dev" or "dev:<target>".
465
+ let requested = null;
466
+ if (first === 'dev') {
467
+ requested = null; // resolve from positional or default below
468
+ } else if (first.startsWith('dev:')) {
469
+ requested = first.slice(4);
470
+ } else {
471
+ console.error(`Unknown run subcommand: ${first}`);
472
+ console.log(RUN_HELP);
473
+ process.exit(1);
474
+ }
475
+
476
+ const rest = args.slice(1);
477
+ const dryRun = rest.includes('--dry-run') || rest.includes('--print');
478
+ const bail = rest.includes('--bail');
479
+ const positionals = rest.filter((a) => !a.startsWith('-'));
480
+ if (!requested) requested = positionals[0] || 'web';
481
+
482
+ const { repoRoot, config, configPath } = loadConfig(process.cwd());
483
+ const table = resolveTable(repoRoot, config);
484
+ const source = configPath
485
+ ? path.relative(process.cwd(), configPath) || CONFIG_FILE
486
+ : `auto-detection (no ${CONFIG_FILE})`;
487
+
488
+ if (requested === 'list') {
489
+ console.log(`\nDev targets — source: ${source}`);
490
+ console.log(`Repo root: ${repoRoot}\n`);
491
+ const names = Object.keys(table.targets);
492
+ if (!names.length) {
493
+ console.log(' (none detected — add a ' + CONFIG_FILE + ' with a dev block)');
494
+ }
495
+ for (const n of names) printTarget(n, table.targets[n], repoRoot);
496
+ console.log('\nGroups:');
497
+ for (const [g, members] of Object.entries(table.groups)) {
498
+ console.log(` ${g}: ${members.join(', ')}`);
499
+ }
500
+ console.log('\nAliases:');
501
+ for (const [a, t] of Object.entries(table.aliases)) {
502
+ console.log(` ${a} → ${Array.isArray(t) ? t.join('|') : t}`);
503
+ }
504
+ console.log('');
505
+ process.exit(0);
506
+ }
507
+
508
+ const plan = expand(requested, table).filter((n) => table.targets[n] && table.targets[n].run);
509
+ if (!plan.length) {
510
+ console.error(`\n❌ "${requested}" resolved to no runnable dev targets.`);
511
+ console.error(` Source: ${source}`);
512
+ const detected = Object.keys(table.targets);
513
+ console.error(` Detected targets: ${detected.join(', ') || '(none)'}`);
514
+ console.error(` Try: yaver run dev:list`);
515
+ process.exit(1);
516
+ }
517
+
518
+ console.log(`\nyaver run dev${first.startsWith('dev:') ? first.slice(3) : ''} → ${plan.join(', ')}`);
519
+ console.log(`Source: ${source} · Repo root: ${repoRoot}`);
520
+
521
+ if (dryRun) {
522
+ console.log('\n(dry-run — nothing will be executed)\n');
523
+ for (const n of plan) printTarget(n, table.targets[n], repoRoot);
524
+ console.log('');
525
+ process.exit(0);
526
+ }
527
+
528
+ await runDevTargets(plan, table, repoRoot, { bail });
529
+ }
530
+
531
+ module.exports = { run, isLocalRunToken, RUN_HELP };
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ const { reset } = require('./commands/reset');
9
9
  const { status } = require('./commands/status');
10
10
  const { feedback } = require('./commands/feedback');
11
11
  const { deploy, isLocalDeployToken } = require('./commands/deploy');
12
+ const { run, isLocalRunToken } = require('./commands/run');
12
13
 
13
14
  const PUSH_HELP = `
14
15
  yaver push — Push existing React Native projects to the Yaver mobile host
@@ -54,6 +55,17 @@ Push-to-device commands:
54
55
  yaver push reset Clear pushed bundle on the selected device
55
56
  yaver push status Show project + device push status
56
57
 
58
+ Dev-server commands (monorepo-aware; yaver.deploy.json + find/grep scan):
59
+ yaver run dev Start the web dev server (default target)
60
+ yaver run dev:web Web (Next/Astro/Vite/SvelteKit/Cloudflare etc.)
61
+ yaver run dev:mobile Expo / React Native / Flutter dev launcher
62
+ yaver run dev:convex Convex dev (alias: dev:backend)
63
+ yaver run dev:supabase Local Supabase stack
64
+ yaver run dev:docker docker compose up
65
+ yaver run dev:all Run every detected dev target in parallel
66
+ yaver run dev:list Show resolved dev targets + their command
67
+ yaver run dev --dry-run Resolve + print, do not execute
68
+
57
69
  Deploy commands (monorepo-aware; yaver.deploy.json + find/grep scan):
58
70
  yaver deploy ios Mobile → TestFlight (RN/Expo, Flutter, Swift)
59
71
  yaver deploy android Mobile → Play internal (RN/Expo, Flutter, Kotlin)
@@ -180,6 +192,17 @@ async function runUnified(args) {
180
192
  return;
181
193
  }
182
194
 
195
+ if (command === 'run') {
196
+ // Local-handle `yaver run dev[:target]`. Anything else (future
197
+ // Go-agent `run` subcommands) falls through to the agent.
198
+ if (isLocalRunToken(args[1])) {
199
+ await run(args.slice(1));
200
+ return;
201
+ }
202
+ await runAgentCommand(args);
203
+ return;
204
+ }
205
+
183
206
  if (command === 'deploy') {
184
207
  // Friendly monorepo targets (ios/android/convex/cloudflare/
185
208
  // mobile/all/aliases/list/--help) are handled locally. Legacy