vibeview 0.27.1 → 0.28.1
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/dist/{chunk-7IOGMM5W.js → chunk-3DKY443B.js} +154 -123
- package/dist/{chunk-FTTWLCTE.js → chunk-PBMB6TG4.js} +120 -15
- package/dist/cli.js +386 -90
- package/dist/{server-ZIUZ7U5T.js → server-BGQJOVSN.js} +298 -53
- package/dist/{upload-app-MDKTSG4O.js → upload-app-FEZG23NU.js} +1 -1
- package/package.json +2 -2
- package/skills/vibeview-agent/SKILL.md +10 -5
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
errorExitCode,
|
|
9
9
|
formatUtcTimestamp,
|
|
10
10
|
loadConfig,
|
|
11
|
+
prepareUploadArtifact,
|
|
11
12
|
queueReasonCopy,
|
|
13
|
+
resolveArtifact,
|
|
12
14
|
resolveAuth,
|
|
13
15
|
uploadAppBuild
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PBMB6TG4.js";
|
|
15
17
|
|
|
16
18
|
// src/lib/dev-config.ts
|
|
17
19
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -166,6 +168,30 @@ function validateBuildEnv(env, platformKey) {
|
|
|
166
168
|
function saveDevConfig(cwd, cfg) {
|
|
167
169
|
writeFileSync(join(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
|
|
168
170
|
}
|
|
171
|
+
function describeConfigChanges(before, after) {
|
|
172
|
+
const flat = (value, prefix, out) => {
|
|
173
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
174
|
+
for (const [k, v] of Object.entries(value)) {
|
|
175
|
+
flat(v, prefix ? `${prefix}.${k}` : k, out);
|
|
176
|
+
}
|
|
177
|
+
} else if (value !== void 0) {
|
|
178
|
+
out.set(prefix, typeof value === "string" ? value : JSON.stringify(value));
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
};
|
|
182
|
+
const a = flat(before, "", /* @__PURE__ */ new Map());
|
|
183
|
+
const b = flat(after, "", /* @__PURE__ */ new Map());
|
|
184
|
+
const changes = [];
|
|
185
|
+
for (const [key, next] of b) {
|
|
186
|
+
const prev = a.get(key);
|
|
187
|
+
if (prev === void 0) changes.push(`${key} (added) ${next}`);
|
|
188
|
+
else if (prev !== next) changes.push(`${key} ${prev} \u2192 ${next}`);
|
|
189
|
+
}
|
|
190
|
+
for (const [key, prev] of a) {
|
|
191
|
+
if (!b.has(key)) changes.push(`${key} (removed, was ${prev})`);
|
|
192
|
+
}
|
|
193
|
+
return changes;
|
|
194
|
+
}
|
|
169
195
|
function resolveMetro(cfg, platform, portFlag) {
|
|
170
196
|
const entry = cfg?.platforms?.[platform];
|
|
171
197
|
return {
|
|
@@ -223,7 +249,7 @@ function buildKind(isDebug) {
|
|
|
223
249
|
if (isDebug === false) return "release";
|
|
224
250
|
return "unknown";
|
|
225
251
|
}
|
|
226
|
-
function checkDevBuild(app, buildId, platform) {
|
|
252
|
+
function checkDevBuild(app, buildId, platform, opts = {}) {
|
|
227
253
|
if (platform === "roku") {
|
|
228
254
|
throw new Error(
|
|
229
255
|
"a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
|
|
@@ -240,7 +266,7 @@ function checkDevBuild(app, buildId, platform) {
|
|
|
240
266
|
`build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
|
|
241
267
|
);
|
|
242
268
|
}
|
|
243
|
-
if (build.is_debug !== true) {
|
|
269
|
+
if (!opts.standalone && build.is_debug !== true) {
|
|
244
270
|
const why = build.is_debug === false ? "it runs standalone (a release build, or one with an embedded JS bundle)" : "it could not be identified as a Metro debug build";
|
|
245
271
|
throw new Error(
|
|
246
272
|
`build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs ${platformWithArticle(platform)} debug simulator/emulator build without an embedded JS bundle (see \`vibeview list-builds ${app.public_id}\`)`
|
|
@@ -318,18 +344,31 @@ function isPidAlive(pid) {
|
|
|
318
344
|
return err.code === "EPERM";
|
|
319
345
|
}
|
|
320
346
|
}
|
|
347
|
+
var WHOLE_DIR_LINE = /^(\/?)\.vibeview\/?$/;
|
|
321
348
|
function ensureGitignored(cwd) {
|
|
322
349
|
try {
|
|
323
350
|
const gitignore = join2(cwd, ".gitignore");
|
|
324
351
|
if (!existsSync(gitignore)) return;
|
|
325
352
|
const content = readFileSync2(gitignore, "utf-8");
|
|
326
|
-
const
|
|
327
|
-
|
|
353
|
+
const lines = content.split("\n");
|
|
354
|
+
const trimmed = lines.map((l) => l.trim());
|
|
355
|
+
if (trimmed.some((l) => /^\/?\.vibeview\/\*$/.test(l))) return;
|
|
356
|
+
const i = trimmed.findIndex((l) => WHOLE_DIR_LINE.test(l));
|
|
357
|
+
if (i !== -1) {
|
|
358
|
+
const anchor = WHOLE_DIR_LINE.exec(trimmed[i])[1];
|
|
359
|
+
lines.splice(i, 1, `${anchor}.vibeview/*`, `!${anchor}.vibeview/test-spec.json`);
|
|
360
|
+
writeFileSync2(gitignore, lines.join("\n"));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
328
363
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
329
|
-
writeFileSync2(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
364
|
+
writeFileSync2(
|
|
365
|
+
gitignore,
|
|
366
|
+
`${content}${prefix}
|
|
367
|
+
# VibeView local session state (test-spec.json is committed for CI)
|
|
368
|
+
.vibeview/*
|
|
369
|
+
!.vibeview/test-spec.json
|
|
370
|
+
`
|
|
371
|
+
);
|
|
333
372
|
} catch {
|
|
334
373
|
}
|
|
335
374
|
}
|
|
@@ -383,81 +422,24 @@ import { createInterface } from "readline";
|
|
|
383
422
|
import { select as select2 } from "@inquirer/prompts";
|
|
384
423
|
import chalk3 from "chalk";
|
|
385
424
|
|
|
386
|
-
// src/lib/artifact.ts
|
|
387
|
-
import { existsSync as existsSync2, mkdtempSync, readdirSync, rmSync as rmSync2, statSync } from "fs";
|
|
388
|
-
import { spawnSync } from "child_process";
|
|
389
|
-
import { tmpdir } from "os";
|
|
390
|
-
import { basename, dirname as dirname2, isAbsolute, join as join3 } from "path";
|
|
391
|
-
function resolveArtifact(cwd, pattern) {
|
|
392
|
-
const abs = isAbsolute(pattern) ? pattern : join3(cwd, pattern);
|
|
393
|
-
if (!basename(abs).includes("*")) {
|
|
394
|
-
if (!existsSync2(abs)) {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`artifact not found: ${pattern} (looked at ${abs}) \u2014 did the build produce it?`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
return abs;
|
|
400
|
-
}
|
|
401
|
-
const dir = dirname2(abs);
|
|
402
|
-
const re = new RegExp("^" + basename(abs).split("*").map(escapeRegExp).join(".*") + "$");
|
|
403
|
-
let entries;
|
|
404
|
-
try {
|
|
405
|
-
entries = readdirSync(dir);
|
|
406
|
-
} catch {
|
|
407
|
-
throw new Error(`artifact not found: ${pattern} (directory ${dir} does not exist)`);
|
|
408
|
-
}
|
|
409
|
-
const matches = entries.filter((e) => re.test(e)).map((e) => join3(dir, e)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
410
|
-
if (matches.length === 0) {
|
|
411
|
-
throw new Error(`artifact not found: no match for ${pattern} in ${dir}`);
|
|
412
|
-
}
|
|
413
|
-
return matches[0];
|
|
414
|
-
}
|
|
415
|
-
function escapeRegExp(s) {
|
|
416
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
417
|
-
}
|
|
418
|
-
function prepareUploadArtifact(path) {
|
|
419
|
-
if (!(statSync(path).isDirectory() && path.endsWith(".app"))) {
|
|
420
|
-
return { uploadPath: path, cleanup: () => {
|
|
421
|
-
} };
|
|
422
|
-
}
|
|
423
|
-
const tmp = mkdtempSync(join3(tmpdir(), "vibeview-zip-"));
|
|
424
|
-
const out = join3(tmp, `${basename(path)}.zip`);
|
|
425
|
-
const res = spawnSync("zip", ["-qry", out, basename(path)], { cwd: dirname2(path) });
|
|
426
|
-
if (res.error) {
|
|
427
|
-
rmSync2(tmp, { recursive: true, force: true });
|
|
428
|
-
const errorCode = res.error.code;
|
|
429
|
-
const errorMsg = res.error.message;
|
|
430
|
-
throw new Error(
|
|
431
|
-
`failed to zip ${path}: spawn zip ${errorCode} \u2014 ${errorMsg} \u2014 is the \`zip\` command installed?`
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
if (res.status !== 0) {
|
|
435
|
-
rmSync2(tmp, { recursive: true, force: true });
|
|
436
|
-
throw new Error(
|
|
437
|
-
`failed to zip ${path}: ${res.stderr?.toString().trim() || `zip exited ${res.status}`}`
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
return { uploadPath: out, cleanup: () => rmSync2(tmp, { recursive: true, force: true }) };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
425
|
// src/lib/dev-setup.ts
|
|
444
426
|
import chalk2 from "chalk";
|
|
445
427
|
import { input, select } from "@inquirer/prompts";
|
|
446
428
|
|
|
447
429
|
// src/lib/project-detect.ts
|
|
448
|
-
import { existsSync as
|
|
449
|
-
import { join as
|
|
430
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
431
|
+
import { join as join3 } from "path";
|
|
450
432
|
import ignore from "ignore";
|
|
451
433
|
function readJson(cwd, name) {
|
|
452
434
|
try {
|
|
453
|
-
return JSON.parse(readFileSync3(
|
|
435
|
+
return JSON.parse(readFileSync3(join3(cwd, name), "utf-8"));
|
|
454
436
|
} catch {
|
|
455
437
|
return null;
|
|
456
438
|
}
|
|
457
439
|
}
|
|
458
440
|
function isFile(path) {
|
|
459
441
|
try {
|
|
460
|
-
return
|
|
442
|
+
return statSync(path).isFile();
|
|
461
443
|
} catch {
|
|
462
444
|
return false;
|
|
463
445
|
}
|
|
@@ -465,19 +447,19 @@ function isFile(path) {
|
|
|
465
447
|
function detectFlavor(cwd) {
|
|
466
448
|
let deps = {};
|
|
467
449
|
try {
|
|
468
|
-
const pkg = JSON.parse(readFileSync3(
|
|
450
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf-8"));
|
|
469
451
|
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
470
452
|
} catch {
|
|
471
453
|
}
|
|
472
454
|
if ("expo" in deps) return "expo";
|
|
473
|
-
if (
|
|
474
|
-
if (isFile(
|
|
455
|
+
if (existsSync2(join3(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
|
|
456
|
+
if (isFile(join3(cwd, "manifest"))) return "roku";
|
|
475
457
|
return "unknown";
|
|
476
458
|
}
|
|
477
459
|
function nativeDirShipped(cwd, dir) {
|
|
478
|
-
if (!
|
|
460
|
+
if (!existsSync2(join3(cwd, dir))) return false;
|
|
479
461
|
try {
|
|
480
|
-
const ig = ignore().add(readFileSync3(
|
|
462
|
+
const ig = ignore().add(readFileSync3(join3(cwd, ".gitignore"), "utf-8"));
|
|
481
463
|
return !ig.ignores(`${dir}/`);
|
|
482
464
|
} catch {
|
|
483
465
|
return true;
|
|
@@ -493,7 +475,7 @@ function isManagedExpo(cwd, platform) {
|
|
|
493
475
|
}
|
|
494
476
|
function iosWorkspaceName(cwd) {
|
|
495
477
|
try {
|
|
496
|
-
const found =
|
|
478
|
+
const found = readdirSync(join3(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
|
|
497
479
|
return found ? found.replace(/\.xcworkspace$/, "") : null;
|
|
498
480
|
} catch {
|
|
499
481
|
return null;
|
|
@@ -502,9 +484,9 @@ function iosWorkspaceName(cwd) {
|
|
|
502
484
|
var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
|
|
503
485
|
var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
|
|
504
486
|
function installCommand(cwd) {
|
|
505
|
-
if (
|
|
506
|
-
if (
|
|
507
|
-
if (
|
|
487
|
+
if (existsSync2(join3(cwd, "yarn.lock"))) return "yarn install --immutable";
|
|
488
|
+
if (existsSync2(join3(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
|
|
489
|
+
if (existsSync2(join3(cwd, "package-lock.json"))) return "npm ci";
|
|
508
490
|
return "yarn install --immutable";
|
|
509
491
|
}
|
|
510
492
|
function managedExpoAndroidCommand(cwd) {
|
|
@@ -518,7 +500,7 @@ var ROKU_ZIP_COMMAND = "zip -r out/channel.zip manifest source components images
|
|
|
518
500
|
function rokuDefaults(cwd) {
|
|
519
501
|
const pkg = readJson(cwd, "package.json");
|
|
520
502
|
const scripts = pkg?.scripts ?? {};
|
|
521
|
-
const runner =
|
|
503
|
+
const runner = existsSync2(join3(cwd, "yarn.lock")) ? "yarn" : "npm run";
|
|
522
504
|
const packageScript = Object.entries(scripts).find(
|
|
523
505
|
([name, body]) => !name.startsWith("deploy") && (name.startsWith("package") || body.includes("--package-only") || body.includes("zipPackage"))
|
|
524
506
|
);
|
|
@@ -576,7 +558,7 @@ function expoSanitizedName(name) {
|
|
|
576
558
|
return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
577
559
|
}
|
|
578
560
|
function managedExpoIosName(cwd) {
|
|
579
|
-
if (
|
|
561
|
+
if (existsSync2(join3(cwd, "app.config.js")) || existsSync2(join3(cwd, "app.config.ts"))) {
|
|
580
562
|
return null;
|
|
581
563
|
}
|
|
582
564
|
const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
|
|
@@ -721,9 +703,9 @@ async function promptMetroCommand(cwd, platform) {
|
|
|
721
703
|
|
|
722
704
|
// src/lib/source-pack.ts
|
|
723
705
|
import { execFile, spawn } from "child_process";
|
|
724
|
-
import { lstatSync, mkdtempSync
|
|
725
|
-
import { tmpdir
|
|
726
|
-
import { join as
|
|
706
|
+
import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
707
|
+
import { tmpdir } from "os";
|
|
708
|
+
import { join as join4 } from "path";
|
|
727
709
|
import ignore2 from "ignore";
|
|
728
710
|
var NotAGitRepoError = class extends Error {
|
|
729
711
|
constructor(message) {
|
|
@@ -749,7 +731,7 @@ async function listSourceFiles(projectRoot) {
|
|
|
749
731
|
const stdout = await runGitLsFiles(projectRoot);
|
|
750
732
|
const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
|
|
751
733
|
let filtered = rawPaths;
|
|
752
|
-
const vvignorePath =
|
|
734
|
+
const vvignorePath = join4(projectRoot, ".vvignore");
|
|
753
735
|
if (statOrNull(vvignorePath)?.isFile()) {
|
|
754
736
|
const ig = ignore2().add(readFileSync4(vvignorePath, "utf-8"));
|
|
755
737
|
filtered = filtered.filter((p) => !ig.ignores(p));
|
|
@@ -757,7 +739,7 @@ async function listSourceFiles(projectRoot) {
|
|
|
757
739
|
const result = [];
|
|
758
740
|
for (const relPath of filtered) {
|
|
759
741
|
if (isForceExcluded(relPath)) continue;
|
|
760
|
-
const st = statOrNull(
|
|
742
|
+
const st = statOrNull(join4(projectRoot, relPath));
|
|
761
743
|
if (!st) continue;
|
|
762
744
|
result.push(relPath);
|
|
763
745
|
}
|
|
@@ -807,7 +789,7 @@ async function packSource(projectRoot, files, opts = {}) {
|
|
|
807
789
|
let totalBytes = 0;
|
|
808
790
|
const sized = [];
|
|
809
791
|
for (const relPath of files) {
|
|
810
|
-
const bytes = lstatSync(
|
|
792
|
+
const bytes = lstatSync(join4(projectRoot, relPath)).size;
|
|
811
793
|
totalBytes += bytes;
|
|
812
794
|
sized.push({ path: relPath, bytes });
|
|
813
795
|
}
|
|
@@ -818,13 +800,13 @@ async function packSource(projectRoot, files, opts = {}) {
|
|
|
818
800
|
largest
|
|
819
801
|
);
|
|
820
802
|
}
|
|
821
|
-
const tmpDir =
|
|
822
|
-
const cleanup = () =>
|
|
803
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
|
|
804
|
+
const cleanup = () => rmSync2(tmpDir, { recursive: true, force: true });
|
|
823
805
|
try {
|
|
824
806
|
const supportsNull = await probeTarNullSupport();
|
|
825
|
-
const tarPath =
|
|
807
|
+
const tarPath = join4(tmpDir, "source.tgz");
|
|
826
808
|
if (supportsNull) {
|
|
827
|
-
const listPath =
|
|
809
|
+
const listPath = join4(tmpDir, "files.list");
|
|
828
810
|
writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
|
|
829
811
|
await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
|
|
830
812
|
} else {
|
|
@@ -834,12 +816,12 @@ async function packSource(projectRoot, files, opts = {}) {
|
|
|
834
816
|
`cannot pack source: filename contains a newline (${JSON.stringify(badName)}) and the installed tar doesn't support --null -T; rename the file or install a tar that supports --null`
|
|
835
817
|
);
|
|
836
818
|
}
|
|
837
|
-
const listPath =
|
|
819
|
+
const listPath = join4(tmpDir, "files.list");
|
|
838
820
|
writeFileSync3(listPath, files.map((f) => `${f}
|
|
839
821
|
`).join(""));
|
|
840
822
|
await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
|
|
841
823
|
}
|
|
842
|
-
const sizeBytes =
|
|
824
|
+
const sizeBytes = statSync2(tarPath).size;
|
|
843
825
|
return { tarPath, sizeBytes, cleanup };
|
|
844
826
|
} catch (err) {
|
|
845
827
|
cleanup();
|
|
@@ -855,12 +837,12 @@ var nullSupportCache = null;
|
|
|
855
837
|
function probeTarNullSupport() {
|
|
856
838
|
if (!nullSupportCache) {
|
|
857
839
|
nullSupportCache = new Promise((resolve) => {
|
|
858
|
-
const tmp =
|
|
859
|
-
const listPath =
|
|
840
|
+
const tmp = mkdtempSync(join4(tmpdir(), "vibeview-tar-probe-"));
|
|
841
|
+
const listPath = join4(tmp, "empty.list");
|
|
860
842
|
writeFileSync3(listPath, "");
|
|
861
|
-
const out =
|
|
843
|
+
const out = join4(tmp, "probe.tgz");
|
|
862
844
|
execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
|
|
863
|
-
|
|
845
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
864
846
|
resolve(!err);
|
|
865
847
|
});
|
|
866
848
|
});
|
|
@@ -1543,7 +1525,9 @@ async function waitForCloudBuilds(builds, opts, deps) {
|
|
|
1543
1525
|
id: final.id,
|
|
1544
1526
|
status: final.status,
|
|
1545
1527
|
cost_micro: final.cost_micro,
|
|
1546
|
-
error_summary: final.error_summary
|
|
1528
|
+
error_summary: final.error_summary,
|
|
1529
|
+
app_build_id: final.app_build_id ?? null,
|
|
1530
|
+
app_public_id: final.app_public_id ?? null
|
|
1547
1531
|
});
|
|
1548
1532
|
finalized.add(build.id);
|
|
1549
1533
|
}
|
|
@@ -1731,7 +1715,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
|
|
|
1731
1715
|
// src/commands/dev.ts
|
|
1732
1716
|
import { spawn as spawn4 } from "child_process";
|
|
1733
1717
|
import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
|
|
1734
|
-
import { dirname as
|
|
1718
|
+
import { dirname as dirname4 } from "path";
|
|
1735
1719
|
import { createInterface as createInterface3 } from "readline";
|
|
1736
1720
|
import { select as select3 } from "@inquirer/prompts";
|
|
1737
1721
|
import chalk4 from "chalk";
|
|
@@ -1926,15 +1910,15 @@ import { spawn as spawn3 } from "child_process";
|
|
|
1926
1910
|
import { createInterface as createInterface2 } from "readline";
|
|
1927
1911
|
|
|
1928
1912
|
// src/lib/metro-state.ts
|
|
1929
|
-
import { existsSync as
|
|
1930
|
-
import { dirname as
|
|
1913
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1914
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1931
1915
|
var STATE_DIR2 = ".vibeview";
|
|
1932
1916
|
var STATE_FILE2 = "metro-state.json";
|
|
1933
1917
|
function statePath2(cwd) {
|
|
1934
|
-
return
|
|
1918
|
+
return join5(cwd, STATE_DIR2, STATE_FILE2);
|
|
1935
1919
|
}
|
|
1936
1920
|
function metroLogPath(cwd) {
|
|
1937
|
-
return
|
|
1921
|
+
return join5(cwd, STATE_DIR2, "metro.log");
|
|
1938
1922
|
}
|
|
1939
1923
|
function isPidAlive2(pid) {
|
|
1940
1924
|
try {
|
|
@@ -1946,7 +1930,7 @@ function isPidAlive2(pid) {
|
|
|
1946
1930
|
}
|
|
1947
1931
|
function writeMetroState(cwd, state) {
|
|
1948
1932
|
const path = statePath2(cwd);
|
|
1949
|
-
mkdirSync2(
|
|
1933
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1950
1934
|
writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
|
|
1951
1935
|
}
|
|
1952
1936
|
function readMetroState(cwd) {
|
|
@@ -1964,7 +1948,7 @@ function readMetroState(cwd) {
|
|
|
1964
1948
|
}
|
|
1965
1949
|
function clearMetroState(cwd) {
|
|
1966
1950
|
const path = statePath2(cwd);
|
|
1967
|
-
if (
|
|
1951
|
+
if (existsSync3(path)) rmSync3(path, { force: true });
|
|
1968
1952
|
}
|
|
1969
1953
|
function killOwnedMetro(cwd) {
|
|
1970
1954
|
const state = readMetroState(cwd);
|
|
@@ -2072,7 +2056,7 @@ async function waitFor(cond, timeoutMs, everyMs) {
|
|
|
2072
2056
|
|
|
2073
2057
|
// src/lib/reload-loop.ts
|
|
2074
2058
|
import { watch } from "fs";
|
|
2075
|
-
import { basename
|
|
2059
|
+
import { basename, dirname as dirname3 } from "path";
|
|
2076
2060
|
function createReloadLoop(deps) {
|
|
2077
2061
|
let inFlight = null;
|
|
2078
2062
|
let pendingFollowUp = false;
|
|
@@ -2153,16 +2137,16 @@ function isIgnoredWatchPath(relPath, artifact, userIgnore = []) {
|
|
|
2153
2137
|
if (IGNORED_FILES.includes(rel)) return true;
|
|
2154
2138
|
if (userIgnore.some((entry) => isUnderPath(rel, entry))) return true;
|
|
2155
2139
|
const artifactPattern = artifact.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2156
|
-
const artifactDir =
|
|
2140
|
+
const artifactDir = dirname3(artifactPattern);
|
|
2157
2141
|
if (artifactDir !== "." && (rel === artifactDir || rel.startsWith(`${artifactDir}/`))) {
|
|
2158
2142
|
return true;
|
|
2159
2143
|
}
|
|
2160
|
-
if (
|
|
2161
|
-
const name =
|
|
2162
|
-
const re = new RegExp(`^${name.split("*").map(
|
|
2163
|
-
return re.test(
|
|
2144
|
+
if (dirname3(rel) !== artifactDir) return false;
|
|
2145
|
+
const name = basename(artifactPattern);
|
|
2146
|
+
const re = new RegExp(`^${name.split("*").map(escapeRegExp).join(".*")}$`);
|
|
2147
|
+
return re.test(basename(rel));
|
|
2164
2148
|
}
|
|
2165
|
-
function
|
|
2149
|
+
function escapeRegExp(s) {
|
|
2166
2150
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2167
2151
|
}
|
|
2168
2152
|
function watchProjectFiles(cwd, artifact, userIgnore = []) {
|
|
@@ -2198,7 +2182,7 @@ function emit(json, evt) {
|
|
|
2198
2182
|
async function startDevSession(cfg, onEvent = () => {
|
|
2199
2183
|
}, onTunnelState) {
|
|
2200
2184
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2201
|
-
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
|
|
2185
|
+
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
|
|
2202
2186
|
const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
|
|
2203
2187
|
const created = await api.createSession(
|
|
2204
2188
|
devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
|
|
@@ -2286,7 +2270,7 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2286
2270
|
reportedWarnings.add(w);
|
|
2287
2271
|
onEvent({ event: "warning", message: w });
|
|
2288
2272
|
}
|
|
2289
|
-
return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings };
|
|
2273
|
+
return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings, session: active };
|
|
2290
2274
|
} catch (err) {
|
|
2291
2275
|
await stopSession(1);
|
|
2292
2276
|
throw err;
|
|
@@ -2297,8 +2281,9 @@ function devSessionCreateBody(cfg) {
|
|
|
2297
2281
|
device_type: PLATFORM_DEVICE[cfg.platform].deviceType,
|
|
2298
2282
|
device_category: cfg.deviceCategory,
|
|
2299
2283
|
app_id: cfg.appId,
|
|
2300
|
-
// Roku has no JS bundle to serve, so there is nothing to tunnel
|
|
2301
|
-
|
|
2284
|
+
// Roku has no JS bundle to serve, so there is nothing to tunnel; a
|
|
2285
|
+
// standalone session runs the build as it is, without Metro.
|
|
2286
|
+
tunnel: PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone,
|
|
2302
2287
|
...cfg.model ? { model: cfg.model } : {},
|
|
2303
2288
|
...cfg.buildId ? { build_id: cfg.buildId } : {}
|
|
2304
2289
|
};
|
|
@@ -2312,10 +2297,36 @@ function resolveDeviceModel(pools, platform, model) {
|
|
|
2312
2297
|
const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
|
|
2313
2298
|
if (match) return match;
|
|
2314
2299
|
const names = [...new Set(candidates.map((p) => p.model))].sort();
|
|
2300
|
+
const guess = closestModelName(wanted, names);
|
|
2315
2301
|
throw new Error(
|
|
2316
|
-
names.length ? `no ${platform} device model named "${model}" \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
|
|
2302
|
+
names.length ? `no ${platform} device model named "${model}"` + (guess ? ` (did you mean "${guess}"?)` : "") + ` \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
|
|
2317
2303
|
);
|
|
2318
2304
|
}
|
|
2305
|
+
function closestModelName(wanted, names) {
|
|
2306
|
+
const squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2307
|
+
const words = (s) => new Set(s.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
2308
|
+
const w = squash(wanted);
|
|
2309
|
+
if (!w) return null;
|
|
2310
|
+
const unique = (list) => list.length === 1 ? list[0] : null;
|
|
2311
|
+
const same = names.filter((n) => squash(n) === w);
|
|
2312
|
+
if (same.length) return unique(same);
|
|
2313
|
+
const containing = names.filter((n) => squash(n).includes(w) || w.includes(squash(n)));
|
|
2314
|
+
if (containing.length) return unique(containing);
|
|
2315
|
+
const wantedWords = words(wanted);
|
|
2316
|
+
let best = 0;
|
|
2317
|
+
let bestNames = [];
|
|
2318
|
+
for (const n of names) {
|
|
2319
|
+
let shared = 0;
|
|
2320
|
+
for (const word of words(n)) if (wantedWords.has(word)) shared++;
|
|
2321
|
+
if (shared > best) {
|
|
2322
|
+
best = shared;
|
|
2323
|
+
bestNames = [n];
|
|
2324
|
+
} else if (shared === best && shared > 0) {
|
|
2325
|
+
bestNames.push(n);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return best >= 2 ? unique(bestNames) : null;
|
|
2329
|
+
}
|
|
2319
2330
|
var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
|
|
2320
2331
|
function findBlockingWarning(info) {
|
|
2321
2332
|
return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
|
|
@@ -2607,6 +2618,12 @@ ${hint}`);
|
|
|
2607
2618
|
});
|
|
2608
2619
|
const commitConfig = () => {
|
|
2609
2620
|
saveDevConfig(process.cwd(), cfg);
|
|
2621
|
+
if (existing) {
|
|
2622
|
+
const changes = describeConfigChanges(existing, cfg);
|
|
2623
|
+
if (changes.length) {
|
|
2624
|
+
console.error(chalk4.dim(`Updated vibeview.json: ${changes.join(", ")}`));
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2610
2627
|
if (isFirstRun) {
|
|
2611
2628
|
console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
|
|
2612
2629
|
const hint = fillLaterHint(
|
|
@@ -2726,7 +2743,7 @@ async function runDevInProcess(opts) {
|
|
|
2726
2743
|
const captureMetroLine = (line) => {
|
|
2727
2744
|
try {
|
|
2728
2745
|
if (!metroLogStream) {
|
|
2729
|
-
mkdirSync3(
|
|
2746
|
+
mkdirSync3(dirname4(metroLogPath(process.cwd())), { recursive: true });
|
|
2730
2747
|
metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
|
|
2731
2748
|
}
|
|
2732
2749
|
metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
|
|
@@ -3078,20 +3095,34 @@ function filterDeviceModels(models, opts) {
|
|
|
3078
3095
|
os_version: m.os_version,
|
|
3079
3096
|
device_type: m.device_type,
|
|
3080
3097
|
category: m.category,
|
|
3081
|
-
form_factor: m.form_factor ?? null
|
|
3098
|
+
form_factor: m.form_factor ?? null,
|
|
3099
|
+
// MCP only: an agent has no other way to see whether a model is free.
|
|
3100
|
+
...opts.availability && typeof m.free === "number" && typeof m.total === "number" ? { free: m.free, total: m.total } : {}
|
|
3082
3101
|
}));
|
|
3083
3102
|
}
|
|
3084
3103
|
var MODEL_HEADERS = ["MODEL", "OS", "TYPE", "CATEGORY", "FORM FACTOR"];
|
|
3085
|
-
function formatDeviceModelsHuman(models) {
|
|
3104
|
+
function formatDeviceModelsHuman(models, opts = {}) {
|
|
3086
3105
|
if (models.length === 0) {
|
|
3087
3106
|
return "No device models available right now.\n";
|
|
3088
3107
|
}
|
|
3089
|
-
const
|
|
3108
|
+
const headers = [...MODEL_HEADERS, ...opts.availability ? ["FREE", "BUSY"] : []];
|
|
3109
|
+
const rows = models.map((m) => [
|
|
3110
|
+
m.model,
|
|
3111
|
+
m.os_version,
|
|
3112
|
+
m.device_type,
|
|
3113
|
+
m.category,
|
|
3114
|
+
m.form_factor ?? "-",
|
|
3115
|
+
...opts.availability ? availabilityCells(m) : []
|
|
3116
|
+
]).sort(
|
|
3090
3117
|
(a, b) => a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
|
|
3091
3118
|
);
|
|
3092
|
-
const widths =
|
|
3119
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3093
3120
|
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
3094
|
-
return [chalk5.bold(fmt(
|
|
3121
|
+
return [chalk5.bold(fmt(headers)), ...rows.map(fmt)].join("\n") + "\n";
|
|
3122
|
+
}
|
|
3123
|
+
function availabilityCells(m) {
|
|
3124
|
+
if (typeof m.free !== "number" || typeof m.total !== "number") return ["?", "?"];
|
|
3125
|
+
return [String(m.free), String(Math.max(0, m.total - m.free))];
|
|
3095
3126
|
}
|
|
3096
3127
|
|
|
3097
3128
|
// src/agent/registry.ts
|