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 +1 -1
- package/src/commands/run.js +531 -0
- package/src/index.js +23 -0
package/package.json
CHANGED
|
@@ -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
|