vibeview 0.12.2 → 0.13.0

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.
@@ -56,6 +56,10 @@ function extractDetail(text) {
56
56
  }
57
57
  return void 0;
58
58
  }
59
+ async function parseJsonBody(resp) {
60
+ if (resp.status === 204 || resp.status === 205) return void 0;
61
+ return resp.json();
62
+ }
59
63
  var ApiClient = class {
60
64
  constructor(baseUrl, token) {
61
65
  this.baseUrl = baseUrl;
@@ -96,7 +100,7 @@ var ApiClient = class {
96
100
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
97
101
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
98
102
  }
99
- return resp.json();
103
+ return parseJsonBody(resp);
100
104
  }
101
105
  async listSuites(opts) {
102
106
  const params = new URLSearchParams();
@@ -168,7 +172,7 @@ var ApiClient = class {
168
172
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
169
173
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
170
174
  }
171
- return resp.json();
175
+ return parseJsonBody(resp);
172
176
  }
173
177
  /** FormData request without /ci prefix — for file uploads */
174
178
  async requestFormData(method, path, formData) {
@@ -188,7 +192,7 @@ var ApiClient = class {
188
192
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
189
193
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
190
194
  }
191
- return resp.json();
195
+ return parseJsonBody(resp);
192
196
  }
193
197
  async listApps() {
194
198
  return this.requestRaw("GET", "/apps/");
@@ -335,6 +339,41 @@ var ApiClient = class {
335
339
  }
336
340
  return { filename, path };
337
341
  }
342
+ /** GET /api/v1/build-env/[?app=]. Requires VIEWER role. Never returns a
343
+ * value — see BuildEnvVar. */
344
+ async listBuildEnvVars(app) {
345
+ const qs = app ? `?app=${encodeURIComponent(app)}` : "";
346
+ return this.requestRaw("GET", `/build-env/${qs}`);
347
+ }
348
+ /** POST /api/v1/build-env/ (JSON body) for a plain/sensitive/secret value.
349
+ * Requires ADMIN role. Upserts by (org, app, name). */
350
+ async setBuildEnvVar(body) {
351
+ return this.requestRaw("POST", "/build-env/", body);
352
+ }
353
+ /** POST /api/v1/build-env/ (multipart) for a file variable. `content` is
354
+ * sent as the raw bytes read from disk — the server does its own
355
+ * base64-encoding internally, so encoding here too would double-encode
356
+ * and corrupt the file. Requires ADMIN role. */
357
+ async setBuildEnvFileVar(body) {
358
+ const formData = new FormData();
359
+ formData.append("name", body.name);
360
+ formData.append("visibility", body.visibility);
361
+ if (body.app) formData.append("app", body.app);
362
+ const blob = new Blob([new Uint8Array(body.content)]);
363
+ formData.append("file", blob, body.filename);
364
+ return this.requestFormData("POST", "/build-env/", formData);
365
+ }
366
+ /** DELETE /api/v1/build-env/{id}. Requires ADMIN role. */
367
+ async deleteBuildEnvVar(id) {
368
+ await this.requestRaw("DELETE", `/build-env/${id}`);
369
+ }
370
+ /** GET /api/v1/build-env/pull[?app=]. Requires DEVELOPER role. The only
371
+ * endpoint that returns values — already excludes secret/file variables
372
+ * server-side. */
373
+ async pullBuildEnvVars(app) {
374
+ const qs = app ? `?app=${encodeURIComponent(app)}` : "";
375
+ return this.requestRaw("GET", `/build-env/pull${qs}`);
376
+ }
338
377
  };
339
378
  function parseContentDispositionFilename(header) {
340
379
  if (!header) return void 0;
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  resolveAuth,
7
7
  uploadAppBuild
8
- } from "./chunk-OHRTLUBD.js";
8
+ } from "./chunk-J7V3GYOI.js";
9
9
 
10
10
  // src/lib/dev-state.ts
11
11
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -854,15 +854,14 @@ function persistPickedAppId(cwd, cfg, platform, appId) {
854
854
  saveDevConfig(cwd, next);
855
855
  return next;
856
856
  }
857
+ var CREATE_NEW_APP = "__vibeview_create_new_app__";
857
858
  async function resolveCloudAppIds(cwd, cfgRef, platforms, opts, listApps) {
859
+ const createFromArtifact = /* @__PURE__ */ new Set();
858
860
  const missing = platforms.filter((p) => !cfgRef.current?.platforms?.[p]?.appId);
859
- if (missing.length === 0) return;
861
+ if (missing.length === 0) return createFromArtifact;
860
862
  if (opts.json || !process.stdin.isTTY) {
861
- fail(
862
- opts,
863
- `no app configured for ${missing[0]}: cloud builds attach to an existing app. Run \`vibeview build --platform ${missing[0]}\` once to build and upload locally, or \`vibeview upload-app\`, then retry --cloud`,
864
- "config_required"
865
- );
863
+ for (const platform of missing) createFromArtifact.add(platform);
864
+ return createFromArtifact;
866
865
  }
867
866
  let allApps;
868
867
  try {
@@ -876,21 +875,40 @@ async function resolveCloudAppIds(cwd, cfgRef, platforms, opts, listApps) {
876
875
  for (const platform of missing) {
877
876
  const apps = appsForPlatform(allApps, platform);
878
877
  if (apps.length === 0) {
879
- fail(
880
- opts,
881
- `${noAppsMessage(platform, cfgRef.current?.platforms?.[platform]?.build !== void 0)} Then retry \`vibeview build --cloud\`.`,
882
- "config_required"
883
- );
878
+ createFromArtifact.add(platform);
879
+ if (!opts.json) {
880
+ console.error(
881
+ chalk.dim(`no ${platform} apps yet \u2014 this build will create one from the artifact`)
882
+ );
883
+ }
884
+ continue;
884
885
  }
885
886
  const appId = await select2({
886
887
  message: platforms.length > 1 ? `Which app should these ${platform} builds attach to?` : "Which app should these builds attach to?",
887
888
  // No platform suffix on the rows: the list is already filtered to this
888
889
  // one platform, so `(ios)` on every row would be pure noise (same
889
890
  // reasoning as dev.ts's picker).
890
- choices: apps.map((a) => ({ value: a.public_id, name: a.name }))
891
+ choices: [
892
+ ...apps.map((a) => ({ value: a.public_id, name: a.name })),
893
+ { value: CREATE_NEW_APP, name: "Create a new app from this build" }
894
+ ]
891
895
  });
896
+ if (appId === CREATE_NEW_APP) {
897
+ createFromArtifact.add(platform);
898
+ continue;
899
+ }
892
900
  cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, appId);
893
901
  }
902
+ return createFromArtifact;
903
+ }
904
+ function persistResolvedAppIds(cwd, cfgRef, createFromArtifact, finished) {
905
+ for (const b of finished) {
906
+ if (!b.app_public_id) continue;
907
+ const platform = b.platform;
908
+ if (!createFromArtifact.has(platform)) continue;
909
+ if (cfgRef.current?.platforms?.[platform]?.appId) continue;
910
+ cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, b.app_public_id);
911
+ }
894
912
  }
895
913
  function describeAllExclusions(configured) {
896
914
  if (configured.length === 0) return "no platforms are configured";
@@ -957,7 +975,7 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
957
975
  const { token, baseUrl } = resolveAuth(opts);
958
976
  const client = new ApiClient(baseUrl, token);
959
977
  const d = deps ?? defaultCloudBuildDeps(client);
960
- await resolveCloudAppIds(cwd, cfgRef, platforms, opts, d.listApps);
978
+ const createFromArtifact = await resolveCloudAppIds(cwd, cfgRef, platforms, opts, d.listApps);
961
979
  const cfg = cfgRef.current;
962
980
  for (const p of platforms) {
963
981
  try {
@@ -1005,10 +1023,12 @@ ${offenders}`);
1005
1023
  const build = entry.build;
1006
1024
  return {
1007
1025
  platform: p,
1008
- // Non-null assertion is safe: resolveCloudAppIds ran for every entry
1009
- // in `platforms` above — it either persisted a picked appId onto the
1010
- // entry or failed the whole command.
1011
- app_public_id: entry.appId,
1026
+ // OMITTED, never invented, for a platform whose app the artifact will
1027
+ // create (resolveCloudAppIds). Otherwise the non-null assertion is
1028
+ // safe: resolveCloudAppIds ran for every entry in `platforms` above —
1029
+ // it either persisted a picked appId onto the entry, marked the
1030
+ // platform create-from-artifact, or failed the whole command.
1031
+ ...createFromArtifact.has(p) ? {} : { app_public_id: entry.appId },
1012
1032
  command: build.command,
1013
1033
  artifact_path: build.artifact,
1014
1034
  ...build.env ? { env: build.env } : {}
@@ -1040,6 +1060,7 @@ ${offenders}`);
1040
1060
  return;
1041
1061
  }
1042
1062
  const finished = await waitForCloudBuilds(created, opts, d);
1063
+ persistResolvedAppIds(cwd, cfgRef, createFromArtifact, finished.values());
1043
1064
  let downloadFailed = false;
1044
1065
  if (opts.download) {
1045
1066
  for (const b of finished.values()) {
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  readDevState,
14
14
  readMetroState,
15
15
  runAgentVerb
16
- } from "./chunk-OYLVR6XA.js";
16
+ } from "./chunk-YFXMXO5T.js";
17
17
  import {
18
18
  ApiClient,
19
19
  EXIT_INFRA_ERROR,
@@ -25,7 +25,7 @@ import {
25
25
  resolveAuth,
26
26
  saveConfig,
27
27
  uploadApp
28
- } from "./chunk-OHRTLUBD.js";
28
+ } from "./chunk-J7V3GYOI.js";
29
29
 
30
30
  // src/cli.ts
31
31
  import { createRequire } from "module";
@@ -1353,8 +1353,233 @@ function printError(opts, err) {
1353
1353
  console.error(chalk11.red(`error: ${message}`));
1354
1354
  }
1355
1355
 
1356
- // src/lib/formatters/devices.ts
1356
+ // src/commands/env.ts
1357
+ import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
1358
+ import { basename } from "path";
1357
1359
  import chalk12 from "chalk";
1360
+ import { password } from "@inquirer/prompts";
1361
+
1362
+ // src/lib/gitignore.ts
1363
+ import { execFile } from "child_process";
1364
+ function isGitIgnored(path, cwd = process.cwd()) {
1365
+ return new Promise((resolvePromise) => {
1366
+ execFile("git", ["check-ignore", "-q", path], { cwd }, (error) => {
1367
+ resolvePromise(!error);
1368
+ });
1369
+ });
1370
+ }
1371
+
1372
+ // src/commands/env.ts
1373
+ function clientFor(opts) {
1374
+ const { token, baseUrl } = resolveAuth(opts);
1375
+ return new ApiClient(baseUrl, token);
1376
+ }
1377
+ async function writeSecureFile(path, content) {
1378
+ const fh = await open2(path, "w", 384);
1379
+ try {
1380
+ await fh.writeFile(content, "utf-8");
1381
+ } finally {
1382
+ await fh.close();
1383
+ }
1384
+ await chmod(path, 384);
1385
+ }
1386
+ function defaultDeps2(client) {
1387
+ return {
1388
+ listVars: (app) => client.listBuildEnvVars(app),
1389
+ setVar: (body) => client.setBuildEnvVar(body),
1390
+ setFileVar: (body) => client.setBuildEnvFileVar(body),
1391
+ rmVar: async ({ name, app }) => {
1392
+ const { variables } = await client.listBuildEnvVars(app);
1393
+ const wantApp = app ?? null;
1394
+ const match = variables.find((v) => v.name === name && (v.app_public_id ?? null) === wantApp);
1395
+ if (!match) {
1396
+ const otherScope = variables.find((v) => v.name === name);
1397
+ if (otherScope) {
1398
+ throw new Error(
1399
+ `No variable named "${name}" in ${app ? `app "${app}"` : "the org-wide scope"}. It exists ${otherScope.app_public_id ? `for app "${otherScope.app_public_id}"` : "org-wide"} \u2014 re-run ${otherScope.app_public_id ? `with --app ${otherScope.app_public_id}` : "without --app"} to remove that one.`
1400
+ );
1401
+ }
1402
+ throw new Error(`No variable named "${name}" found${app ? ` for app "${app}"` : ""}.`);
1403
+ }
1404
+ await client.deleteBuildEnvVar(match.id);
1405
+ },
1406
+ pullVars: (app) => client.pullBuildEnvVars(app),
1407
+ // `hidden` is always true in practice (env set only prompts for secret
1408
+ // values), but the parameter exists so a future caller/test can't
1409
+ // accidentally get echoed input by omitting it.
1410
+ prompt: ({ message }) => password({ message }),
1411
+ isTTY: Boolean(process.stdin.isTTY),
1412
+ isIgnored: (path) => isGitIgnored(path),
1413
+ writeFile: writeSecureFile,
1414
+ readFile: (path) => readFileAsync(path),
1415
+ warn: (message) => console.error(chalk12.yellow(`warning: ${message}`))
1416
+ };
1417
+ }
1418
+ async function envList(opts, deps) {
1419
+ const d = deps ?? defaultDeps2(clientFor(opts));
1420
+ try {
1421
+ const { variables } = await d.listVars(opts.app);
1422
+ if (opts.json) {
1423
+ console.log(JSON.stringify(variables, null, 2));
1424
+ } else {
1425
+ process.stdout.write(formatEnvVarsHuman(variables));
1426
+ }
1427
+ process.exitCode = 0;
1428
+ } catch (err) {
1429
+ printError2(opts, err);
1430
+ process.exitCode = 1;
1431
+ }
1432
+ }
1433
+ function formatEnvVarsHuman(variables) {
1434
+ if (variables.length === 0) {
1435
+ return "No build variables found.\n";
1436
+ }
1437
+ const HEADERS3 = ["NAME", "VISIBILITY", "SCOPE", "HINT"];
1438
+ const rows = variables.map((v) => [
1439
+ v.name,
1440
+ v.is_file ? `${v.visibility} (file)` : v.visibility,
1441
+ v.app_public_id ?? "org-wide",
1442
+ v.value_hint ?? ""
1443
+ ]);
1444
+ const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1445
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1446
+ const join2 = (cells) => cells.join(" ").trimEnd();
1447
+ const lines = [chalk12.bold(join2(padCells(HEADERS3)))];
1448
+ for (const row of rows) lines.push(join2(padCells(row)));
1449
+ return lines.join("\n") + "\n";
1450
+ }
1451
+ async function envSet(name, value, opts, deps) {
1452
+ const d = deps ?? defaultDeps2(clientFor(opts));
1453
+ let resolvedValue = value;
1454
+ if (resolvedValue === void 0 && !opts.file) {
1455
+ if (opts.json || !d.isTTY) {
1456
+ throw new Error(
1457
+ "A value is required: pass it as the VALUE argument, or run interactively to be prompted."
1458
+ );
1459
+ }
1460
+ }
1461
+ try {
1462
+ const visibility = resolveVisibility(opts);
1463
+ if (opts.file) {
1464
+ const content = await d.readFile(opts.file);
1465
+ const filename = basename(opts.file);
1466
+ const result2 = await d.setFileVar({ name, content, filename, visibility, app: opts.app });
1467
+ printSetResult(opts, result2);
1468
+ process.exitCode = 0;
1469
+ return;
1470
+ }
1471
+ if (resolvedValue === void 0) {
1472
+ resolvedValue = await d.prompt({ message: `Value for ${name}`, hidden: true });
1473
+ }
1474
+ const result = await d.setVar({ name, value: resolvedValue, visibility, app: opts.app });
1475
+ printSetResult(opts, result);
1476
+ process.exitCode = 0;
1477
+ } catch (err) {
1478
+ printError2(opts, err);
1479
+ process.exitCode = 1;
1480
+ }
1481
+ }
1482
+ function resolveVisibility(opts) {
1483
+ const chosen = [
1484
+ opts.secret && "secret",
1485
+ opts.sensitive && "sensitive",
1486
+ opts.plain && "plain"
1487
+ ].filter((v) => Boolean(v));
1488
+ if (chosen.length > 1) {
1489
+ throw new Error("Pass at most one of --secret, --sensitive, --plain.");
1490
+ }
1491
+ return chosen[0] ?? "secret";
1492
+ }
1493
+ function printSetResult(opts, result) {
1494
+ if (opts.json) {
1495
+ console.log(JSON.stringify(result));
1496
+ } else {
1497
+ console.log(chalk12.green(`\u2713 set ${result.name} (${result.visibility})`));
1498
+ }
1499
+ }
1500
+ async function envRm(name, opts, deps) {
1501
+ const d = deps ?? defaultDeps2(clientFor(opts));
1502
+ try {
1503
+ await d.rmVar({ name, app: opts.app });
1504
+ if (opts.json) {
1505
+ console.log(JSON.stringify({ event: "deleted", name }));
1506
+ } else {
1507
+ console.log(chalk12.green(`\u2713 removed ${name}`));
1508
+ }
1509
+ process.exitCode = 0;
1510
+ } catch (err) {
1511
+ printError2(opts, err);
1512
+ process.exitCode = 1;
1513
+ }
1514
+ }
1515
+ async function envPull(opts, deps) {
1516
+ const d = deps ?? defaultDeps2(clientFor(opts));
1517
+ const outPath = opts.out ?? ".env";
1518
+ try {
1519
+ const { variables } = await d.pullVars(opts.app);
1520
+ const ignored = await d.isIgnored(outPath);
1521
+ if (!ignored) {
1522
+ d.warn(
1523
+ `${outPath} is not covered by .gitignore \u2014 writing credentials into a tracked file risks committing them. Add it to .gitignore.`
1524
+ );
1525
+ }
1526
+ for (const v of variables) {
1527
+ if (isLossyEnvValue(v.value)) {
1528
+ const why = v.value.includes("\r") ? "contains a carriage return, which the .env format normalizes away" : "contains a single quote, a double quote and a backtick, which no .env quoting form can carry together";
1529
+ d.warn(
1530
+ `${v.name} ${why} \u2014 the written value differs from the stored one. Read this variable from the API instead of ${outPath}.`
1531
+ );
1532
+ }
1533
+ }
1534
+ const content = formatDotEnv(variables);
1535
+ await d.writeFile(outPath, content);
1536
+ if (opts.json) {
1537
+ console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
1538
+ } else {
1539
+ console.log(chalk12.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1540
+ }
1541
+ process.exitCode = 0;
1542
+ } catch (err) {
1543
+ printError2(opts, err);
1544
+ process.exitCode = 1;
1545
+ }
1546
+ }
1547
+ function isLossyEnvValue(value) {
1548
+ if (value.includes("\r")) return true;
1549
+ return value.includes("'") && value.includes('"') && value.includes("`");
1550
+ }
1551
+ function formatDotEnv(variables) {
1552
+ const lines = [
1553
+ "# Generated by `vibeview env pull` \u2014 do not commit.",
1554
+ "# Values are quoted/escaped only where the .env format requires it."
1555
+ ];
1556
+ for (const v of variables) {
1557
+ lines.push(`${v.name}=${escapeEnvValue(v.value)}`);
1558
+ }
1559
+ return lines.join("\n") + "\n";
1560
+ }
1561
+ function escapeEnvValue(value) {
1562
+ if (value === "") return "''";
1563
+ if (!value.includes("'")) {
1564
+ if (!/[\s"'#\\=`]/.test(value)) return value;
1565
+ return `'${value}'`;
1566
+ }
1567
+ if (!value.includes('"') && !value.includes("\\")) {
1568
+ return `"${value}"`;
1569
+ }
1570
+ if (!value.includes("`")) {
1571
+ return `\`${value}\``;
1572
+ }
1573
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1574
+ }
1575
+ function printError2(opts, err) {
1576
+ const message = err instanceof Error ? err.message : String(err);
1577
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1578
+ console.error(chalk12.red(`error: ${message}`));
1579
+ }
1580
+
1581
+ // src/lib/formatters/devices.ts
1582
+ import chalk13 from "chalk";
1358
1583
  var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
1359
1584
  function formatDevicesHuman(devices) {
1360
1585
  if (devices.length === 0) {
@@ -1370,9 +1595,9 @@ function formatDevicesHuman(devices) {
1370
1595
  ]);
1371
1596
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1372
1597
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
1373
- const lines = [chalk12.bold(fmt(HEADERS2))];
1598
+ const lines = [chalk13.bold(fmt(HEADERS2))];
1374
1599
  for (const row of rows.sort(rowSort)) {
1375
- const statusColored = row[5] === "available" ? chalk12.green(row[5]) : row[5] === "busy" ? chalk12.yellow(row[5]) : chalk12.dim(row[5]);
1600
+ const statusColored = row[5] === "available" ? chalk13.green(row[5]) : row[5] === "busy" ? chalk13.yellow(row[5]) : chalk13.dim(row[5]);
1376
1601
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
1377
1602
  }
1378
1603
  return lines.join("\n") + "\n";
@@ -1462,7 +1687,7 @@ import { dirname, join } from "path";
1462
1687
  import { fileURLToPath } from "url";
1463
1688
  import { spawnSync } from "child_process";
1464
1689
  import { confirm } from "@inquirer/prompts";
1465
- import chalk13 from "chalk";
1690
+ import chalk14 from "chalk";
1466
1691
  function resolveShippedSkillPath(fromDir) {
1467
1692
  for (const rel of ["..", join("..", "..")]) {
1468
1693
  const candidate = join(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -1490,23 +1715,23 @@ function registerMcp() {
1490
1715
  encoding: "utf8"
1491
1716
  });
1492
1717
  if (res.status === 0) {
1493
- console.log(chalk13.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
1718
+ console.log(chalk14.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
1494
1719
  return;
1495
1720
  }
1496
1721
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
1497
1722
  if (out.includes("already exists")) {
1498
- console.log(chalk13.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
1723
+ console.log(chalk14.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
1499
1724
  return;
1500
1725
  }
1501
- console.error(chalk13.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
1502
- console.error(chalk13.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
1726
+ console.error(chalk14.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
1727
+ console.error(chalk14.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
1503
1728
  }
1504
1729
  async function agentSetup(opts) {
1505
1730
  const moduleDir = dirname(fileURLToPath(import.meta.url));
1506
1731
  const shipped = resolveShippedSkillPath(moduleDir);
1507
1732
  if (!shipped) {
1508
1733
  console.error(
1509
- chalk13.red(
1734
+ chalk14.red(
1510
1735
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
1511
1736
  )
1512
1737
  );
@@ -1515,9 +1740,9 @@ async function agentSetup(opts) {
1515
1740
  }
1516
1741
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
1517
1742
  const result = installSkill(shipped, targetDir);
1518
- console.log(chalk13.green(`\u2713 vibeview-agent skill ${result}: ${join(targetDir, "SKILL.md")}`));
1743
+ console.log(chalk14.green(`\u2713 vibeview-agent skill ${result}: ${join(targetDir, "SKILL.md")}`));
1519
1744
  console.log(
1520
- chalk13.dim(
1745
+ chalk14.dim(
1521
1746
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
1522
1747
  )
1523
1748
  );
@@ -1525,7 +1750,7 @@ async function agentSetup(opts) {
1525
1750
  if (!claudeCliAvailable()) {
1526
1751
  if (opts.mcp === true) {
1527
1752
  console.error(
1528
- chalk13.yellow(
1753
+ chalk14.yellow(
1529
1754
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
1530
1755
  )
1531
1756
  );
@@ -1539,7 +1764,7 @@ async function agentSetup(opts) {
1539
1764
  }
1540
1765
  console.log(
1541
1766
  `
1542
- Suggested line for your project's ${chalk13.bold("CLAUDE.md")} (or ${chalk13.bold(
1767
+ Suggested line for your project's ${chalk14.bold("CLAUDE.md")} (or ${chalk14.bold(
1543
1768
  "AGENTS.md"
1544
1769
  )} for other agents):
1545
1770
  `
@@ -1547,7 +1772,7 @@ Suggested line for your project's ${chalk13.bold("CLAUDE.md")} (or ${chalk13.bol
1547
1772
  console.log(` ${CLAUDE_MD_SUGGESTION}
1548
1773
  `);
1549
1774
  console.log(
1550
- chalk13.dim(
1775
+ chalk14.dim(
1551
1776
  "Other agents (Cursor, Copilot, Codex, ...): point them at the skill file above, or\nadd the MCP server per client \u2014 see https://vibeview.io/docs/agent-control"
1552
1777
  )
1553
1778
  );
@@ -1712,12 +1937,24 @@ builds.command("list").description("List cloud builds in your organization").opt
1712
1937
  builds.command("logs <id>").description("Print a cloud build's log output").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print NDJSON log/outcome events instead of raw text", false).option("--follow", "Keep polling until the build reaches a terminal status", false).action((id, opts) => buildsLogs(id, opts));
1713
1938
  builds.command("download <id>").description("Download a succeeded cloud build's artifact").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the saved filename/path as one JSON line", false).option("--out <dir>", "Destination directory (default: current directory)").action((id, opts) => buildsDownload(id, opts));
1714
1939
  builds.command("cancel <id>").description("Cancel a queued or running cloud build").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the resulting build summary as one JSON line", false).action((id, opts) => buildsCancel(id, opts));
1940
+ var env = program.command("env").description("Manage cloud build environment variables and secrets");
1941
+ env.command("list").description("List build variables and secrets (never prints a value)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide variables only)").option("--json", "Print the raw variable list as a JSON array", false).action((opts) => envList(opts));
1942
+ env.command("set <name> [value]").description(
1943
+ "Set a build variable or secret. Defaults to --secret (never retrievable via the API) unless --plain or --sensitive is passed explicitly."
1944
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide)").option("--secret", "Store as secret (default): never returned by the API, including `env pull`").option(
1945
+ "--sensitive",
1946
+ "Store as sensitive: value_hint only in `env list`, full value via `env pull`"
1947
+ ).option("--plain", "Store as plain: value returned by `env pull`, unmasked").option("--file <path>", "Upload a file as the value (e.g. a keystore) instead of a text value").option("--json", "Print the resulting variable as one JSON line", false).action((name, value, opts) => envSet(name, value, opts));
1948
+ env.command("rm <name>").description("Remove a build variable or secret").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide)").option("--json", "Print the result as one JSON line", false).action((name, opts) => envRm(name, opts));
1949
+ env.command("pull").description(
1950
+ "Write plain/sensitive variable values to a .env file (secrets and files are never included)"
1951
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide variables only)").option("--out <path>", "Output file path (default: .env)").option("--json", "Print the result as one JSON line", false).action((opts) => envPull(opts));
1715
1952
  program.command("dev-status").description("Show the status of a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStatus(opts));
1716
1953
  program.command("dev-stop").description("Stop a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStop(opts));
1717
1954
  program.command("mcp").description(
1718
1955
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
1719
1956
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
1720
- const { runMcpServer } = await import("./server-DRMLNXEZ.js");
1957
+ const { runMcpServer } = await import("./server-JA5A5PY6.js");
1721
1958
  await runMcpServer(opts);
1722
1959
  });
1723
1960
  program.command("agent-setup").description(
@@ -11,11 +11,11 @@ import {
11
11
  readDevState,
12
12
  renderEnvelope,
13
13
  startDevSession
14
- } from "./chunk-OYLVR6XA.js";
14
+ } from "./chunk-YFXMXO5T.js";
15
15
  import {
16
16
  ApiClient,
17
17
  resolveAuth
18
- } from "./chunk-OHRTLUBD.js";
18
+ } from "./chunk-J7V3GYOI.js";
19
19
 
20
20
  // src/mcp/server.ts
21
21
  import { writeFileSync } from "fs";
@@ -239,7 +239,7 @@ async function handleUploadApp(args, deps) {
239
239
  }
240
240
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
241
241
  const upload = deps.uploadApp ?? (async (f, n) => {
242
- const { uploadAppBuild } = await import("./upload-app-G5SEYBTU.js");
242
+ const { uploadAppBuild } = await import("./upload-app-ZAWCZFQE.js");
243
243
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
244
244
  return result2;
245
245
  });
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  uploadApp,
4
4
  uploadAppBuild
5
- } from "./chunk-OHRTLUBD.js";
5
+ } from "./chunk-J7V3GYOI.js";
6
6
  export {
7
7
  uploadApp,
8
8
  uploadAppBuild
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,6 +38,7 @@
38
38
  "devDependencies": {
39
39
  "@types/node": "^20.0.0",
40
40
  "@types/ws": "^8.5.0",
41
+ "dotenv": "^16.4.5",
41
42
  "tsup": "^8.0.0",
42
43
  "typescript": "^5.0.0",
43
44
  "vitest": "^3.0.0"