yaver-cli 1.99.199 → 1.99.200
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/deploy.js +622 -0
- package/src/index.js +28 -0
package/package.json
CHANGED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `yaver deploy <target>` — friendly, monorepo-aware deploy front-end.
|
|
4
|
+
//
|
|
5
|
+
// This is the human-facing layer. It maps short target names
|
|
6
|
+
// (ios / android / convex / supabase / cloudflare / docker / npm /
|
|
7
|
+
// backend / frontend / mobile / all) onto concrete build+upload
|
|
8
|
+
// commands. `yaver.deploy.json` at the repo root is authoritative;
|
|
9
|
+
// on top of (or instead of) it, a find/grep monorepo scan auto-
|
|
10
|
+
// detects app dirs at any depth and their framework (Flutter, React
|
|
11
|
+
// Native/Expo, native Swift/Kotlin, Next/Astro/Vite/SvelteKit/Nuxt/
|
|
12
|
+
// Remix jamstack, Convex, Supabase, Cloudflare/Wrangler, Docker, and
|
|
13
|
+
// publishable npm libraries). Config entries always win on name
|
|
14
|
+
// conflicts; detection fills the gaps — so the same binary works for
|
|
15
|
+
// a bespoke monorepo (Talos) and a zero-config one (yaver.io).
|
|
16
|
+
// Anything we do not recognise (legacy `-repo/-workflow` CI-trigger
|
|
17
|
+
// flags, or future `generate/ship/runs/logs/diagnose` subcommands) is
|
|
18
|
+
// left for the Go agent — index.js decides the split.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const CONFIG_FILE = 'yaver.deploy.json';
|
|
25
|
+
|
|
26
|
+
// Subcommands that are NOT ours — index.js forwards these to the Go
|
|
27
|
+
// agent untouched so we never regress the existing CI-trigger `deploy`.
|
|
28
|
+
const AGENT_SUBCOMMANDS = new Set([
|
|
29
|
+
'generate', 'ship', 'runs', 'logs', 'diagnose',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Built-in alias → canonical target(s). A value may be an array of
|
|
33
|
+
// candidates; expand() picks the first one that actually resolved in
|
|
34
|
+
// this repo (so `backend` works whether it's Convex or Supabase).
|
|
35
|
+
// Overridable by config.aliases.
|
|
36
|
+
const BUILTIN_ALIASES = {
|
|
37
|
+
backend: ['convex', 'supabase'],
|
|
38
|
+
frontend: 'cloudflare',
|
|
39
|
+
front: 'cloudflare',
|
|
40
|
+
web: 'cloudflare',
|
|
41
|
+
library: 'npm',
|
|
42
|
+
lib: 'npm',
|
|
43
|
+
publish: 'npm',
|
|
44
|
+
container: 'docker',
|
|
45
|
+
compose: 'docker',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Canonical target names we always recognise even before scanning.
|
|
49
|
+
const KNOWN_TARGETS = ['ios', 'android', 'convex', 'supabase', 'cloudflare', 'docker', 'npm'];
|
|
50
|
+
|
|
51
|
+
// Built-in groups. Overridable by config.groups. Expanded lazily so a
|
|
52
|
+
// group only pulls in targets that actually resolve.
|
|
53
|
+
const BUILTIN_GROUPS = {
|
|
54
|
+
mobile: ['ios', 'android'],
|
|
55
|
+
all: ['convex', 'cloudflare', 'ios', 'android'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const DEPLOY_HELP = `
|
|
59
|
+
yaver deploy — build + ship a monorepo app from your own machine
|
|
60
|
+
|
|
61
|
+
Targets:
|
|
62
|
+
yaver deploy ios Mobile → TestFlight (RN/Expo, Flutter, native Swift)
|
|
63
|
+
yaver deploy android Mobile → Play internal (RN/Expo, Flutter, native Kotlin)
|
|
64
|
+
yaver deploy convex Deploy the Convex backend (alias: backend)
|
|
65
|
+
yaver deploy supabase Push Supabase db + edge functions (alias: backend)
|
|
66
|
+
yaver deploy cloudflare Deploy web/jamstack to Cloudflare (alias: frontend, front, web)
|
|
67
|
+
yaver deploy docker Build/up Docker (compose or image) (alias: container, compose)
|
|
68
|
+
yaver deploy npm Publish a detected npm library (alias: library, lib, publish)
|
|
69
|
+
yaver deploy mobile ios + android
|
|
70
|
+
yaver deploy all backend → frontend → mobile (npm/docker excluded)
|
|
71
|
+
|
|
72
|
+
Inspect:
|
|
73
|
+
yaver deploy list Show every resolved target, its framework + command
|
|
74
|
+
yaver deploy <t> --dry-run Print the exact dir + command without running it
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--dry-run, --print Resolve + print, do not execute
|
|
78
|
+
--continue-on-error Run remaining targets even if one fails
|
|
79
|
+
--help Show this help
|
|
80
|
+
|
|
81
|
+
Targets come from ${CONFIG_FILE} at the repo root (authoritative).
|
|
82
|
+
A find/grep monorepo scan also auto-detects app dirs at any depth and
|
|
83
|
+
their framework; config entries win on name conflicts and detection
|
|
84
|
+
fills the gaps. Legacy CI-trigger flags (-repo/-workflow/...) and the
|
|
85
|
+
generate/ship/runs/logs/diagnose subcommands are handled by the agent.
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
// A token is "ours" (handle locally) when it is a known target/alias/
|
|
89
|
+
// group name, or a config-defined one, or list/help with no agent
|
|
90
|
+
// flags. Anything starting with '-' or in AGENT_SUBCOMMANDS is the
|
|
91
|
+
// agent's. Used by index.js to route without parsing the world.
|
|
92
|
+
function isLocalDeployToken(token, args) {
|
|
93
|
+
if (!token) return false; // bare `yaver deploy` → agent (its usage)
|
|
94
|
+
if (token.startsWith('-')) {
|
|
95
|
+
return token === '--help' || token === '-h';
|
|
96
|
+
}
|
|
97
|
+
if (AGENT_SUBCOMMANDS.has(token)) return false;
|
|
98
|
+
if (token === 'list') return true;
|
|
99
|
+
if (BUILTIN_ALIASES[token] || BUILTIN_GROUPS[token]) return true;
|
|
100
|
+
if (KNOWN_TARGETS.includes(token)) return true;
|
|
101
|
+
// Fall back to config: a name only we'd know about.
|
|
102
|
+
try {
|
|
103
|
+
const { config } = loadConfig(process.cwd());
|
|
104
|
+
if (config) {
|
|
105
|
+
if (config.targets && config.targets[token]) return true;
|
|
106
|
+
if (config.groups && config.groups[token]) return true;
|
|
107
|
+
if (config.aliases && config.aliases[token]) return true;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
/* config errors surface later in run() with a clear message */
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Walk up from `start` until a yaver.deploy.json is found. If none,
|
|
116
|
+
// stop at the git root (or filesystem root) and return that as
|
|
117
|
+
// repoRoot with config=null so auto-detection can take over.
|
|
118
|
+
function loadConfig(start) {
|
|
119
|
+
let dir = path.resolve(start);
|
|
120
|
+
let gitRoot = null;
|
|
121
|
+
// eslint-disable-next-line no-constant-condition
|
|
122
|
+
while (true) {
|
|
123
|
+
const cfgPath = path.join(dir, CONFIG_FILE);
|
|
124
|
+
if (fs.existsSync(cfgPath)) {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new Error(`${CONFIG_FILE} at ${cfgPath} is not valid JSON: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
return { repoRoot: dir, config: parsed, configPath: cfgPath };
|
|
132
|
+
}
|
|
133
|
+
if (!gitRoot && fs.existsSync(path.join(dir, '.git'))) gitRoot = dir;
|
|
134
|
+
const parent = path.dirname(dir);
|
|
135
|
+
if (parent === dir) break;
|
|
136
|
+
dir = parent;
|
|
137
|
+
}
|
|
138
|
+
return { repoRoot: gitRoot || path.resolve(start), config: null, configPath: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readJson(p) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Heavy dirs we never descend into during the scan.
|
|
150
|
+
const PRUNE_DIRS = [
|
|
151
|
+
'node_modules', '.git', 'build', 'dist', '.next', '.expo', 'Pods',
|
|
152
|
+
'.yaver', 'vendor', 'target', '.gradle', 'DerivedData', '.turbo',
|
|
153
|
+
'.cache', 'out', '.svelte-kit', '.output', 'coverage', '.venv',
|
|
154
|
+
'__pycache__', '.dart_tool',
|
|
155
|
+
];
|
|
156
|
+
// Marker files (any depth ≤ MAXDEPTH) that hint at an app + its stack.
|
|
157
|
+
const MARKER_FILES = [
|
|
158
|
+
'package.json', 'pubspec.yaml', 'convex.json',
|
|
159
|
+
'wrangler.toml', 'wrangler.jsonc', 'wrangler.json',
|
|
160
|
+
'config.toml', 'Dockerfile', 'docker-compose.yml',
|
|
161
|
+
'docker-compose.yaml', 'compose.yaml', 'compose.yml',
|
|
162
|
+
'Package.swift', 'build.gradle', 'build.gradle.kts',
|
|
163
|
+
'AndroidManifest.xml', 'netlify.toml', 'vercel.json',
|
|
164
|
+
'next.config.*', 'astro.config.*', 'svelte.config.*',
|
|
165
|
+
'nuxt.config.*', 'remix.config.*', 'gatsby-config.*', 'vite.config.*',
|
|
166
|
+
];
|
|
167
|
+
// Marker directories.
|
|
168
|
+
const MARKER_DIRS = ['convex', 'supabase', '*.xcodeproj', '*.xcworkspace'];
|
|
169
|
+
const MAXDEPTH = 6;
|
|
170
|
+
|
|
171
|
+
function nameGroup(names) {
|
|
172
|
+
const g = ['('];
|
|
173
|
+
names.forEach((n, i) => {
|
|
174
|
+
if (i) g.push('-o');
|
|
175
|
+
g.push('-name', n);
|
|
176
|
+
});
|
|
177
|
+
g.push(')');
|
|
178
|
+
return g;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// One `find`: prune heavy dirs, then print marker files and marker
|
|
182
|
+
// dirs up to MAXDEPTH. Falls back to a shallow JS walk if `find` is
|
|
183
|
+
// unavailable (e.g. minimal Windows shells).
|
|
184
|
+
function scanRepo(repoRoot) {
|
|
185
|
+
const args = [
|
|
186
|
+
repoRoot, '-maxdepth', String(MAXDEPTH),
|
|
187
|
+
...nameGroup(PRUNE_DIRS), '-prune', '-o',
|
|
188
|
+
'(', '-type', 'f', ...nameGroup(MARKER_FILES), ')', '-print', '-o',
|
|
189
|
+
'(', '-type', 'd', ...nameGroup(MARKER_DIRS), ')', '-print',
|
|
190
|
+
];
|
|
191
|
+
const res = spawnSync('find', args, { encoding: 'utf8', timeout: 20000, maxBuffer: 16 * 1024 * 1024 });
|
|
192
|
+
if (res.error || res.status == null) {
|
|
193
|
+
return jsWalk(repoRoot, MAXDEPTH);
|
|
194
|
+
}
|
|
195
|
+
return res.stdout.split('\n').filter(Boolean);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Minimal fallback walker (only marker basenames, bounded depth).
|
|
199
|
+
function jsWalk(root, depth, base = root, acc = []) {
|
|
200
|
+
if (depth < 0) return acc;
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
204
|
+
} catch {
|
|
205
|
+
return acc;
|
|
206
|
+
}
|
|
207
|
+
for (const e of entries) {
|
|
208
|
+
if (e.isDirectory() && PRUNE_DIRS.includes(e.name)) continue;
|
|
209
|
+
const full = path.join(base, e.name);
|
|
210
|
+
const isMarkerDir = MARKER_DIRS.some((m) => m.startsWith('*')
|
|
211
|
+
? e.name.endsWith(m.slice(1)) : e.name === m);
|
|
212
|
+
const isMarkerFile = MARKER_FILES.some((m) => m.startsWith('*.')
|
|
213
|
+
? e.name.endsWith(m.slice(1)) : e.name === m);
|
|
214
|
+
if ((e.isDirectory() && isMarkerDir) || (e.isFile() && isMarkerFile)) acc.push(full);
|
|
215
|
+
if (e.isDirectory()) jsWalk(root, depth - 1, full, acc);
|
|
216
|
+
}
|
|
217
|
+
return acc;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function rel(repoRoot, p) {
|
|
221
|
+
const r = path.relative(repoRoot, p);
|
|
222
|
+
return r === '' ? '.' : r;
|
|
223
|
+
}
|
|
224
|
+
function depthOf(relDir) {
|
|
225
|
+
return relDir === '.' ? 0 : relDir.split(path.sep).length;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Detect every deployable target by classifying scanned markers.
|
|
229
|
+
// Returns { [name]: spec } where spec = {dir, run, env, description,
|
|
230
|
+
// framework}. `run: null` means "detected but command unknown — user
|
|
231
|
+
// must declare it in yaver.deploy.json".
|
|
232
|
+
function autoDetectTargets(repoRoot) {
|
|
233
|
+
const markers = scanRepo(repoRoot);
|
|
234
|
+
const has = (rel2) => fs.existsSync(path.join(repoRoot, rel2));
|
|
235
|
+
const tfScript = has('scripts/deploy-testflight.sh');
|
|
236
|
+
const psScript = has('scripts/deploy-playstore.sh');
|
|
237
|
+
|
|
238
|
+
// Group markers by the directory that owns them.
|
|
239
|
+
const dirs = {}; // relDir -> { files:Set, dirs:Set }
|
|
240
|
+
const note = (d, kind, name) => {
|
|
241
|
+
const k = rel(repoRoot, d);
|
|
242
|
+
(dirs[k] || (dirs[k] = { files: new Set(), dirs: new Set() }))[kind].add(name);
|
|
243
|
+
};
|
|
244
|
+
for (const m of markers) {
|
|
245
|
+
let st;
|
|
246
|
+
try { st = fs.statSync(m); } catch { continue; }
|
|
247
|
+
if (st.isDirectory()) {
|
|
248
|
+
note(path.dirname(m), 'dirs', path.basename(m));
|
|
249
|
+
} else {
|
|
250
|
+
note(path.dirname(m), 'files', path.basename(m));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const targets = {};
|
|
255
|
+
// Keep the strongest candidate per target: prefer a repo deploy
|
|
256
|
+
// script, then a runnable command, then the shallowest dir.
|
|
257
|
+
const consider = (name, spec) => {
|
|
258
|
+
const cur = targets[name];
|
|
259
|
+
if (!cur) { targets[name] = spec; return; }
|
|
260
|
+
const score = (s) => (s.run ? 2 : 0) + (s.scriptBacked ? 1 : 0);
|
|
261
|
+
if (score(spec) > score(cur)
|
|
262
|
+
|| (score(spec) === score(cur) && depthOf(spec.dir) < depthOf(cur.dir))) {
|
|
263
|
+
targets[name] = spec;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const isFile = (relDir, base) => {
|
|
268
|
+
const e = dirs[relDir];
|
|
269
|
+
if (!e) return false;
|
|
270
|
+
for (const f of e.files) {
|
|
271
|
+
if (f === base) return true;
|
|
272
|
+
if (base.startsWith('*.') && f.endsWith(base.slice(1))) return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
};
|
|
276
|
+
const hasDir = (relDir, base) => {
|
|
277
|
+
const e = dirs[relDir];
|
|
278
|
+
if (!e) return false;
|
|
279
|
+
for (const dd of e.dirs) {
|
|
280
|
+
if (dd === base) return true;
|
|
281
|
+
if (base.startsWith('*') && dd.endsWith(base.slice(1))) return true;
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (const [relDir, sig] of Object.entries(dirs)) {
|
|
287
|
+
const abs = path.join(repoRoot, relDir);
|
|
288
|
+
const pkg = sig.files.has('package.json') ? readJson(path.join(abs, 'package.json')) : null;
|
|
289
|
+
const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
|
|
290
|
+
const scripts = (pkg && pkg.scripts) || {};
|
|
291
|
+
const pubspec = sig.files.has('pubspec.yaml');
|
|
292
|
+
let flutter = false;
|
|
293
|
+
if (pubspec) {
|
|
294
|
+
try { flutter = /\bflutter\s*:/.test(fs.readFileSync(path.join(abs, 'pubspec.yaml'), 'utf8')); }
|
|
295
|
+
catch { flutter = true; }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Mobile ───────────────────────────────────────────────
|
|
299
|
+
const isExpo = !!deps.expo;
|
|
300
|
+
const isRN = !!deps['react-native'];
|
|
301
|
+
if (flutter) {
|
|
302
|
+
consider('ios', {
|
|
303
|
+
dir: tfScript ? '.' : relDir,
|
|
304
|
+
run: tfScript ? 'bash scripts/deploy-testflight.sh' : 'flutter build ipa',
|
|
305
|
+
scriptBacked: tfScript,
|
|
306
|
+
framework: 'flutter',
|
|
307
|
+
description: tfScript ? 'Flutter iOS via scripts/deploy-testflight.sh'
|
|
308
|
+
: 'Flutter → IPA (build only; declare TestFlight upload in ' + CONFIG_FILE + ')',
|
|
309
|
+
});
|
|
310
|
+
consider('android', {
|
|
311
|
+
dir: psScript ? '.' : relDir,
|
|
312
|
+
run: psScript ? 'bash scripts/deploy-playstore.sh' : 'flutter build appbundle',
|
|
313
|
+
scriptBacked: psScript,
|
|
314
|
+
framework: 'flutter',
|
|
315
|
+
description: psScript ? 'Flutter Android via scripts/deploy-playstore.sh'
|
|
316
|
+
: 'Flutter → AAB (build only; declare Play upload in ' + CONFIG_FILE + ')',
|
|
317
|
+
});
|
|
318
|
+
} else if (isExpo || isRN) {
|
|
319
|
+
const fw = isExpo ? 'expo' : 'react-native';
|
|
320
|
+
consider('ios', {
|
|
321
|
+
dir: '.',
|
|
322
|
+
run: tfScript ? 'bash scripts/deploy-testflight.sh' : null,
|
|
323
|
+
scriptBacked: tfScript,
|
|
324
|
+
framework: fw,
|
|
325
|
+
description: tfScript ? `${fw} iOS via scripts/deploy-testflight.sh`
|
|
326
|
+
: `${fw} app — declare iOS archive+upload in ${CONFIG_FILE}`,
|
|
327
|
+
});
|
|
328
|
+
consider('android', {
|
|
329
|
+
dir: '.',
|
|
330
|
+
run: psScript ? 'bash scripts/deploy-playstore.sh' : null,
|
|
331
|
+
scriptBacked: psScript,
|
|
332
|
+
framework: fw,
|
|
333
|
+
description: psScript ? `${fw} Android via scripts/deploy-playstore.sh`
|
|
334
|
+
: `${fw} app — declare Android bundle+upload in ${CONFIG_FILE}`,
|
|
335
|
+
});
|
|
336
|
+
} else if (hasDir(relDir, '*.xcodeproj') || hasDir(relDir, '*.xcworkspace') || sig.files.has('Package.swift')) {
|
|
337
|
+
consider('ios', {
|
|
338
|
+
dir: tfScript ? '.' : relDir,
|
|
339
|
+
run: tfScript ? 'bash scripts/deploy-testflight.sh' : null,
|
|
340
|
+
scriptBacked: tfScript,
|
|
341
|
+
framework: 'swift',
|
|
342
|
+
description: tfScript ? 'Native iOS via scripts/deploy-testflight.sh'
|
|
343
|
+
: 'Native Xcode project — declare archive+upload in ' + CONFIG_FILE,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (!flutter && !isExpo && !isRN
|
|
347
|
+
&& (sig.files.has('build.gradle') || sig.files.has('build.gradle.kts'))
|
|
348
|
+
&& isFileInTree(dirs, 'AndroidManifest.xml')) {
|
|
349
|
+
consider('android', {
|
|
350
|
+
dir: psScript ? '.' : relDir,
|
|
351
|
+
run: psScript ? 'bash scripts/deploy-playstore.sh' : null,
|
|
352
|
+
scriptBacked: psScript,
|
|
353
|
+
framework: 'kotlin',
|
|
354
|
+
description: psScript ? 'Native Android via scripts/deploy-playstore.sh'
|
|
355
|
+
: 'Native Gradle project — declare bundle+upload in ' + CONFIG_FILE,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Backend: Convex / Supabase ───────────────────────────
|
|
360
|
+
// Folder-based only: `convex` as a package dep is just the client
|
|
361
|
+
// SDK (a frontend), not the backend. The backend is where the
|
|
362
|
+
// functions/config live (convex.json or a convex/ dir).
|
|
363
|
+
if (sig.files.has('convex.json') || hasDir(relDir, 'convex')) {
|
|
364
|
+
consider('convex', {
|
|
365
|
+
dir: relDir, run: 'npx convex deploy -y', scriptBacked: false,
|
|
366
|
+
framework: 'convex', description: `Convex backend (${relDir})`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (hasDir(relDir, 'supabase') || (relDir.endsWith('supabase') && sig.files.has('config.toml'))) {
|
|
370
|
+
const sbDir = relDir.endsWith('supabase') ? path.dirname(relDir) || '.' : relDir;
|
|
371
|
+
consider('supabase', {
|
|
372
|
+
dir: sbDir,
|
|
373
|
+
run: 'npx supabase db push && npx supabase functions deploy',
|
|
374
|
+
scriptBacked: false, framework: 'supabase',
|
|
375
|
+
description: `Supabase db + edge functions (${sbDir}) — verify before live use`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Web / jamstack → Cloudflare ──────────────────────────
|
|
380
|
+
const jamstack = deps.next || deps.astro || deps.vite || deps['@sveltejs/kit']
|
|
381
|
+
|| deps.nuxt || deps['@remix-run/dev'] || deps.gatsby;
|
|
382
|
+
const cfConfig = sig.files.has('wrangler.toml') || sig.files.has('wrangler.jsonc')
|
|
383
|
+
|| sig.files.has('wrangler.json') || deps.wrangler || deps['@opennextjs/cloudflare'];
|
|
384
|
+
if (jamstack || cfConfig) {
|
|
385
|
+
let run = null;
|
|
386
|
+
let desc;
|
|
387
|
+
const fw = deps.next ? 'next' : deps.astro ? 'astro' : deps.nuxt ? 'nuxt'
|
|
388
|
+
: deps['@sveltejs/kit'] ? 'sveltekit' : deps['@remix-run/dev'] ? 'remix'
|
|
389
|
+
: deps.gatsby ? 'gatsby' : deps.vite ? 'vite' : 'jamstack';
|
|
390
|
+
if (scripts.deploy) { run = 'npm run deploy'; desc = `${fw} → Cloudflare (npm run deploy)`; }
|
|
391
|
+
else if (deps['@opennextjs/cloudflare']) { run = 'npx @opennextjs/cloudflare build && npx wrangler deploy'; desc = `${fw} → Cloudflare (OpenNext)`; }
|
|
392
|
+
else if (cfConfig) { run = 'npx wrangler deploy'; desc = `${fw} → Cloudflare (wrangler)`; }
|
|
393
|
+
else { desc = `${fw} jamstack — no Cloudflare config; declare deploy in ${CONFIG_FILE}`; }
|
|
394
|
+
consider('cloudflare', { dir: relDir, run, scriptBacked: false, framework: fw, description: desc });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── npm library ──────────────────────────────────────────
|
|
398
|
+
if (pkg && pkg.private !== true && pkg.name && pkg.version
|
|
399
|
+
&& (pkg.main || pkg.module || pkg.exports || pkg.bin)
|
|
400
|
+
&& !jamstack && !isExpo && !isRN) {
|
|
401
|
+
const pubCmd = scripts.release ? 'npm run release'
|
|
402
|
+
: scripts.publish ? 'npm run publish' : 'npm publish';
|
|
403
|
+
consider('npm', {
|
|
404
|
+
dir: relDir, run: pubCmd, scriptBacked: !!(scripts.release || scripts.publish),
|
|
405
|
+
framework: 'npm', description: `npm publish ${pkg.name}@${pkg.version} (${relDir})`,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Docker ───────────────────────────────────────────────
|
|
410
|
+
const compose = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yaml', 'compose.yml']
|
|
411
|
+
.some((f) => sig.files.has(f));
|
|
412
|
+
if (compose) {
|
|
413
|
+
consider('docker', {
|
|
414
|
+
dir: relDir, run: 'docker compose up -d --build', scriptBacked: false,
|
|
415
|
+
framework: 'docker-compose', description: `Docker Compose (${relDir})`,
|
|
416
|
+
});
|
|
417
|
+
} else if (sig.files.has('Dockerfile')) {
|
|
418
|
+
const tag = (path.basename(abs) || 'app').toLowerCase().replace(/[^a-z0-9._-]/g, '-');
|
|
419
|
+
consider('docker', {
|
|
420
|
+
dir: relDir, run: `docker build -t ${tag} .`, scriptBacked: false,
|
|
421
|
+
framework: 'dockerfile', description: `Docker image build ${tag} (${relDir})`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return targets;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// True if any scanned dir holds `base` (used for AndroidManifest.xml,
|
|
430
|
+
// which lives several levels under the gradle module root).
|
|
431
|
+
function isFileInTree(dirs, base) {
|
|
432
|
+
for (const e of Object.values(dirs)) if (e.files.has(base)) return true;
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Normalise detection + config into one table. Detection is the base;
|
|
437
|
+
// config.targets overlay it (config always wins on name conflicts) so
|
|
438
|
+
// the same binary serves a bespoke monorepo and a zero-config one.
|
|
439
|
+
function resolveTable(repoRoot, config) {
|
|
440
|
+
const targets = autoDetectTargets(repoRoot);
|
|
441
|
+
|
|
442
|
+
if (config && config.targets) {
|
|
443
|
+
for (const [name, spec] of Object.entries(config.targets)) {
|
|
444
|
+
targets[name] = {
|
|
445
|
+
dir: spec.dir || '.',
|
|
446
|
+
run: spec.run || (spec.script ? `bash ${spec.script}` : null),
|
|
447
|
+
env: spec.env || {},
|
|
448
|
+
description: spec.description || name,
|
|
449
|
+
framework: spec.framework || 'config',
|
|
450
|
+
aliases: spec.aliases || [],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const aliases = {};
|
|
456
|
+
for (const [a, v] of Object.entries(BUILTIN_ALIASES)) aliases[a] = v;
|
|
457
|
+
if (config && config.aliases) for (const [a, v] of Object.entries(config.aliases)) aliases[a] = v;
|
|
458
|
+
for (const [name, spec] of Object.entries(targets)) {
|
|
459
|
+
for (const a of spec.aliases || []) aliases[a] = name;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const groups = { ...BUILTIN_GROUPS, ...(config && config.groups) };
|
|
463
|
+
|
|
464
|
+
return { targets, groups, aliases };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// requested name → ordered, deduped list of concrete target names that
|
|
468
|
+
// actually exist in the table.
|
|
469
|
+
function expand(name, table, seen = new Set(), trail = []) {
|
|
470
|
+
if (trail.includes(name)) {
|
|
471
|
+
throw new Error(`circular deploy group: ${[...trail, name].join(' → ')}`);
|
|
472
|
+
}
|
|
473
|
+
// An alias may map to several candidates; pick the first that
|
|
474
|
+
// actually resolved in this repo (backend → convex|supabase).
|
|
475
|
+
let canonical = name;
|
|
476
|
+
const aliased = table.aliases[name];
|
|
477
|
+
if (Array.isArray(aliased)) {
|
|
478
|
+
canonical = aliased.find((c) => table.targets[c] || table.groups[c]) || aliased[0];
|
|
479
|
+
} else if (aliased) {
|
|
480
|
+
canonical = aliased;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (table.targets[canonical]) {
|
|
484
|
+
if (!seen.has(canonical)) {
|
|
485
|
+
seen.add(canonical);
|
|
486
|
+
return [canonical];
|
|
487
|
+
}
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const group = table.groups[canonical] || table.groups[name];
|
|
492
|
+
if (group) {
|
|
493
|
+
const out = [];
|
|
494
|
+
for (const member of group) {
|
|
495
|
+
out.push(...expand(member, table, seen, [...trail, name]));
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return []; // unknown / not present in this repo
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function printTarget(name, spec, repoRoot) {
|
|
504
|
+
const where = path.join(repoRoot, spec.dir || '.');
|
|
505
|
+
console.log(` ${name}${spec.framework ? ` [${spec.framework}]` : ''}`);
|
|
506
|
+
console.log(` dir: ${where}`);
|
|
507
|
+
if (spec.description) console.log(` what: ${spec.description}`);
|
|
508
|
+
if (spec.run) {
|
|
509
|
+
console.log(` run: ${spec.run}`);
|
|
510
|
+
} else {
|
|
511
|
+
console.log(` run: ⚠️ not configured — ${spec.description}`);
|
|
512
|
+
}
|
|
513
|
+
if (spec.env && Object.keys(spec.env).length) {
|
|
514
|
+
console.log(` env: ${Object.keys(spec.env).join(', ')}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function runOne(name, spec, repoRoot) {
|
|
519
|
+
const cwd = path.join(repoRoot, spec.dir || '.');
|
|
520
|
+
if (!spec.run) {
|
|
521
|
+
throw new Error(`target "${name}" has no command (${spec.description}). Add it to ${CONFIG_FILE}.`);
|
|
522
|
+
}
|
|
523
|
+
if (!fs.existsSync(cwd)) {
|
|
524
|
+
throw new Error(`target "${name}" dir does not exist: ${cwd}`);
|
|
525
|
+
}
|
|
526
|
+
console.log(`\n── deploy ${name} ──`);
|
|
527
|
+
console.log(` $ (cd ${cwd} && ${spec.run})\n`);
|
|
528
|
+
const res = spawnSync('bash', ['-lc', spec.run], {
|
|
529
|
+
cwd,
|
|
530
|
+
stdio: 'inherit',
|
|
531
|
+
env: { ...process.env, ...(spec.env || {}) },
|
|
532
|
+
});
|
|
533
|
+
if (res.error) throw new Error(`failed to launch "${name}": ${res.error.message}`);
|
|
534
|
+
return res.status == null ? 1 : res.status;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function deploy(args) {
|
|
538
|
+
if (!args.length || args[0] === '--help' || args[0] === '-h') {
|
|
539
|
+
console.log(DEPLOY_HELP);
|
|
540
|
+
process.exit(0);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const dryRun = args.includes('--dry-run') || args.includes('--print');
|
|
544
|
+
const continueOnError = args.includes('--continue-on-error');
|
|
545
|
+
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
546
|
+
const requested = positionals[0];
|
|
547
|
+
|
|
548
|
+
const { repoRoot, config, configPath } = loadConfig(process.cwd());
|
|
549
|
+
const table = resolveTable(repoRoot, config);
|
|
550
|
+
|
|
551
|
+
const source = configPath
|
|
552
|
+
? path.relative(process.cwd(), configPath) || CONFIG_FILE
|
|
553
|
+
: 'auto-detection (no ' + CONFIG_FILE + ')';
|
|
554
|
+
|
|
555
|
+
if (requested === 'list' || (!requested && dryRun)) {
|
|
556
|
+
console.log(`\nDeploy targets — source: ${source}`);
|
|
557
|
+
console.log(`Repo root: ${repoRoot}\n`);
|
|
558
|
+
const names = Object.keys(table.targets);
|
|
559
|
+
if (!names.length) {
|
|
560
|
+
console.log(' (none resolved — add a ' + CONFIG_FILE + ' or check the layout)');
|
|
561
|
+
}
|
|
562
|
+
for (const n of names) printTarget(n, table.targets[n], repoRoot);
|
|
563
|
+
console.log('\nGroups:');
|
|
564
|
+
for (const [g, members] of Object.entries(table.groups)) {
|
|
565
|
+
console.log(` ${g}: ${members.join(', ')}`);
|
|
566
|
+
}
|
|
567
|
+
console.log('\nAliases:');
|
|
568
|
+
for (const [a, t] of Object.entries(table.aliases)) {
|
|
569
|
+
console.log(` ${a} → ${t}`);
|
|
570
|
+
}
|
|
571
|
+
console.log('');
|
|
572
|
+
process.exit(0);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const plan = expand(requested, table);
|
|
576
|
+
if (!plan.length) {
|
|
577
|
+
console.error(`\n❌ "${requested}" resolved to no deployable targets.`);
|
|
578
|
+
console.error(` Source: ${source}`);
|
|
579
|
+
console.error(` Known targets: ${Object.keys(table.targets).join(', ') || '(none)'}`);
|
|
580
|
+
console.error(` Run: yaver deploy list`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
console.log(`\nyaver deploy ${requested} → ${plan.join(', ')}`);
|
|
585
|
+
console.log(`Source: ${source} · Repo root: ${repoRoot}`);
|
|
586
|
+
|
|
587
|
+
if (dryRun) {
|
|
588
|
+
console.log('\n(dry-run — nothing will be executed)\n');
|
|
589
|
+
for (const n of plan) printTarget(n, table.targets[n], repoRoot);
|
|
590
|
+
console.log('');
|
|
591
|
+
process.exit(0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const results = [];
|
|
595
|
+
for (const n of plan) {
|
|
596
|
+
let status;
|
|
597
|
+
try {
|
|
598
|
+
status = runOne(n, table.targets[n], repoRoot);
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error(`\n❌ ${err.message}`);
|
|
601
|
+
status = 1;
|
|
602
|
+
}
|
|
603
|
+
results.push({ target: n, status });
|
|
604
|
+
if (status !== 0 && !continueOnError) {
|
|
605
|
+
console.error(`\n❌ ${n} failed (exit ${status}); stopping. Use --continue-on-error to override.`);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
console.log('\n── composite summary ──');
|
|
611
|
+
let worst = 0;
|
|
612
|
+
for (const r of results) {
|
|
613
|
+
console.log(` ${r.status === 0 ? '✓' : '✗'} ${r.target} (exit ${r.status})`);
|
|
614
|
+
if (r.status !== 0) worst = r.status;
|
|
615
|
+
}
|
|
616
|
+
const skipped = plan.length - results.length;
|
|
617
|
+
if (skipped > 0) console.log(` · ${skipped} target(s) skipped after failure`);
|
|
618
|
+
console.log('');
|
|
619
|
+
process.exit(worst);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
module.exports = { deploy, isLocalDeployToken, DEPLOY_HELP };
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { modules } = require('./commands/modules');
|
|
|
8
8
|
const { reset } = require('./commands/reset');
|
|
9
9
|
const { status } = require('./commands/status');
|
|
10
10
|
const { feedback } = require('./commands/feedback');
|
|
11
|
+
const { deploy, isLocalDeployToken } = require('./commands/deploy');
|
|
11
12
|
|
|
12
13
|
const PUSH_HELP = `
|
|
13
14
|
yaver push — Push existing React Native projects to the Yaver mobile host
|
|
@@ -53,6 +54,19 @@ Push-to-device commands:
|
|
|
53
54
|
yaver push reset Clear pushed bundle on the selected device
|
|
54
55
|
yaver push status Show project + device push status
|
|
55
56
|
|
|
57
|
+
Deploy commands (monorepo-aware; yaver.deploy.json + find/grep scan):
|
|
58
|
+
yaver deploy ios Mobile → TestFlight (RN/Expo, Flutter, Swift)
|
|
59
|
+
yaver deploy android Mobile → Play internal (RN/Expo, Flutter, Kotlin)
|
|
60
|
+
yaver deploy convex Deploy Convex backend (alias: backend)
|
|
61
|
+
yaver deploy supabase Supabase db + edge functions (alias: backend)
|
|
62
|
+
yaver deploy cloudflare Web/jamstack → Cloudflare (alias: frontend, front)
|
|
63
|
+
yaver deploy docker Docker compose/image (alias: container, compose)
|
|
64
|
+
yaver deploy npm Publish detected npm library (alias: lib, publish)
|
|
65
|
+
yaver deploy mobile ios + android
|
|
66
|
+
yaver deploy all backend → frontend → mobile (npm/docker excluded)
|
|
67
|
+
yaver deploy list Show resolved targets, framework + commands
|
|
68
|
+
yaver deploy <t> --dry-run Print dir + command without running
|
|
69
|
+
|
|
56
70
|
Feedback SDK commands (web and mobile SDKs are separate packages):
|
|
57
71
|
yaver feedback init Install the right feedback SDK for this project
|
|
58
72
|
yaver feedback init --platform web Force web (yaver-feedback-web)
|
|
@@ -166,6 +180,20 @@ async function runUnified(args) {
|
|
|
166
180
|
return;
|
|
167
181
|
}
|
|
168
182
|
|
|
183
|
+
if (command === 'deploy') {
|
|
184
|
+
// Friendly monorepo targets (ios/android/convex/cloudflare/
|
|
185
|
+
// mobile/all/aliases/list/--help) are handled locally. Legacy
|
|
186
|
+
// CI-trigger flags (-repo/-workflow/...) and future agent
|
|
187
|
+
// subcommands (generate/ship/runs/logs/diagnose) fall through to
|
|
188
|
+
// the Go agent untouched.
|
|
189
|
+
if (isLocalDeployToken(args[1], args)) {
|
|
190
|
+
await deploy(args.slice(1));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await runAgentCommand(args);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
169
197
|
if (command === 'npm-agent-info') {
|
|
170
198
|
const info = resolveAgentInfo();
|
|
171
199
|
console.log(JSON.stringify(info, null, 2));
|