@thebaycloud/cli 1.0.0 → 1.1.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.
package/lib/brand.js CHANGED
@@ -33,7 +33,10 @@ const fs = require("fs");
33
33
  const BRAND = "Bay";
34
34
  const CLI = "bay";
35
35
  const DOMAIN = "thebay.cloud";
36
- const DEFAULT_URL = "https://app.supersonic.cv";
36
+ // Derived from DOMAIN, which said `thebay.cloud` on the line above while this
37
+ // said `supersonic.cv`. Both answer, so nothing broke — which is exactly why it
38
+ // went unnoticed, and why every `bay` install was talking to the retiring name.
39
+ const DEFAULT_URL = `https://app.${DOMAIN}`;
37
40
 
38
41
  const NEW_DIR = path.join(os.homedir(), ".bay");
39
42
  const OLD_DIR = path.join(os.homedir(), ".supersonic");
package/lib/check.js CHANGED
@@ -203,7 +203,20 @@ function renderService(s) {
203
203
  // has no start command, no health path and no request timeout, and printing
204
204
  // the defaults for all three would describe a service that is not deployed.
205
205
  if (!s.serviceless) {
206
- lines.push(phase("start", s.lane === "container"
206
+ // Keyed off the AUTHOR's Dockerfile, not off the lane.
207
+ //
208
+ // Asking `s.lane === "container"` was right while there were four lanes.
209
+ // There are two. `container` now means "an image is built and a node runs
210
+ // it" whether the author committed a Dockerfile or `generateDockerfile`
211
+ // wrote one FROM THIS `start` COMMAND — so the old condition answered "the
212
+ // Dockerfile's own CMD" for every non-static service, about a repository
213
+ // with no Dockerfile in it, hiding a command the config states in plain
214
+ // text. In the one command whose whole job is to say what will run.
215
+ //
216
+ // `s.dockerfile` is the author's file and only ever that; resolve.ts draws
217
+ // the same line. The `image` phase above already names it, so this line
218
+ // does not repeat the path.
219
+ lines.push(phase("start", s.dockerfile
207
220
  ? "the Dockerfile's own CMD"
208
221
  : s.start || (s.runs.length ? "— (the web process below)" : "— (nothing to run: this lane needs one)")));
209
222
  // The web process's own health check is what the deploy probes, so it is
@@ -271,7 +284,7 @@ function renderCheck(configFilename, app, problems, warnings) {
271
284
  lines.push(head.startsWith("✕") ? head : `✕ ${head}`, ...rest);
272
285
  }
273
286
  lines.push("");
274
- lines.push(`${problems.length} problem${problems.length === 1 ? "" : "s"} — nothing was deployed, and nothing would be.`);
287
+ lines.push(`${problems.length} problem${problems.length === 1 ? "" : "s"} — nothing was shipped, and nothing would be.`);
275
288
  } else {
276
289
  lines.push("✓ nothing here fails before the build. No GCP was touched.");
277
290
  }
package/lib/confirm.js CHANGED
@@ -36,7 +36,7 @@ function deletionRefusal(app, args) {
36
36
 
37
37
  return [
38
38
  `${app} would be deleted, and so would its DATA: the database, the storage`,
39
- "bucket, the images and the deploy history all go with it.",
39
+ "bucket, the images and the ship history all go with it.",
40
40
  "",
41
41
  "This cannot be undone, and there is no prompt to say yes to — run",
42
42
  ` bay delete ${app} --yes`,
package/lib/draft.js CHANGED
@@ -478,7 +478,7 @@ function renderDraft(configFilename, config, unknowns) {
478
478
  // confidence answer. Presenting its output as a finding rather than a draft is
479
479
  // how that answer became a deploy.
480
480
  lines.push("This is a draft, from a detector that has been confidently wrong before.");
481
- lines.push(`Read it, fix what is wrong, then: supersonic check`);
481
+ lines.push(`Read it, fix what is wrong, then: bay check`);
482
482
  return lines;
483
483
  }
484
484
 
package/lib/home.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Where the session lives, and what the variables are called, across a rename.
5
+ *
6
+ * Both answers changed at once — `~/.supersonic` became `~/.bay`, `SUPERSONIC_*`
7
+ * became `BAY_*` — and both have the same failure mode if they change carelessly:
8
+ * the CLI comes up signed out. Not broken, not erroring; signed out, with a
9
+ * perfectly good token sitting in a file it stopped looking at, and an agent
10
+ * mid-task being told to open a browser.
11
+ *
12
+ * Pure, and taking the home directory and an existence check as arguments, so
13
+ * the migration can be tested without touching anyone's real home.
14
+ */
15
+
16
+ /**
17
+ * The config directory: the new name if it is there, the old name if IT is, the
18
+ * new name otherwise.
19
+ *
20
+ * Deliberately not "read both and merge". Two files that disagree about which
21
+ * account you are would be resolved by whichever line ran first, and the symptom
22
+ * — deploying to the wrong account — is one nobody would trace back to here. One
23
+ * directory wins outright, and an existing user keeps writing to the file they
24
+ * already have.
25
+ */
26
+ function configDirIn(home, exists, join) {
27
+ const next = join(home, ".bay");
28
+ const prev = join(home, ".supersonic");
29
+ if (exists(next)) return next;
30
+ if (exists(prev)) return prev;
31
+ return next;
32
+ }
33
+
34
+ /**
35
+ * `BAY_<NAME>`, falling back to `SUPERSONIC_<NAME>`.
36
+ *
37
+ * The new name wins when both are set: somebody who exported both is midway
38
+ * through a migration, and the value they added last is the one they mean. An
39
+ * empty string is treated as unset, which is what a shell gives you for an
40
+ * exported-but-never-assigned variable.
41
+ */
42
+ function envVarFrom(env, name) {
43
+ return (env["BAY_" + name] || env["SUPERSONIC_" + name] || "").trim();
44
+ }
45
+
46
+ module.exports = { configDirIn, envVarFrom };
package/lib/resolver.js CHANGED
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * Loaded lazily and through a named function rather than at require() time: the
13
13
  * bundle is generated, an old global install can be missing it, and a missing
14
- * bundle must fail `supersonic check` with a sentence naming the fix rather than
15
- * crashing `supersonic logs`, which does not use it at all.
14
+ * bundle must fail `bay check` with a sentence naming the fix rather than
15
+ * crashing `bay logs`, which does not use it at all.
16
16
  */
17
17
  const path = require("node:path");
18
18
 
@@ -37,7 +37,7 @@ function resolver() {
37
37
  * The stack detector, as the async `Detect` that inferAppConfig expects.
38
38
  *
39
39
  * Synchronous in the CLI — there is one process, one repo, and nothing else
40
- * waiting on the event loop. The control plane spawns the deploy-agent as a
40
+ * waiting on the event loop. The control plane spawns the detector as a
41
41
  * subprocess for the same job, which is why the interface is a promise at all;
42
42
  * matching the interface rather than the transport is what lets both callers
43
43
  * share inferAppConfig unchanged.
package/lib/rows.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Query results as a table a terminal can hold.
5
+ *
6
+ * `bay db <app> <table>` returns real production rows, and a row is not a
7
+ * shape this CLI controls: one column is an id, the next is a 4KB blob of JSON,
8
+ * and printing them raw turns a 12-row answer into four screens of wrapped text
9
+ * with no visible columns at all. So every cell is flattened to one line and
10
+ * clipped, and the width is the widest cell that survived.
11
+ *
12
+ * Clipping is marked with `…` rather than being silent. A truncated value that
13
+ * looks whole is worse than no value: it is the one a person copies.
14
+ *
15
+ * `--json` exists for everything this drops. That is the contract — this is the
16
+ * human reading, `--json` is the data.
17
+ */
18
+
19
+ /** null and undefined are not the string "null" — they are the empty cell. */
20
+ function cell(v, max) {
21
+ if (v === null || v === undefined) return "";
22
+ const s = typeof v === "object" ? JSON.stringify(v) : String(v);
23
+ // Tabs and newlines inside a value would break the alignment of every row
24
+ // after them, so they become a single space before anything is measured.
25
+ const flat = s.replace(/\s+/g, " ").trim();
26
+ return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
27
+ }
28
+
29
+ /**
30
+ * @param {string[]} columns
31
+ * @param {object[]} rows
32
+ * @param {{max?: number}} [opts] max characters per cell (default 40)
33
+ * @returns {string[]} lines: a header, a rule, then one line per row
34
+ */
35
+ function renderTable(columns, rows, opts = {}) {
36
+ const max = opts.max ?? 40;
37
+ const cols = columns || [];
38
+ if (!cols.length) return [];
39
+
40
+ const body = (rows || []).map((r) =>
41
+ // Arrays as well as objects: `SELECT` results arrive keyed by column name,
42
+ // but a caller holding tuples should not have to convert them first.
43
+ cols.map((c, i) => cell(Array.isArray(r) ? r[i] : r?.[c], max))
44
+ );
45
+
46
+ const width = cols.map((c, i) =>
47
+ Math.max(cell(c, max).length, ...body.map((r) => r[i].length), 1)
48
+ );
49
+
50
+ // The last column is not padded. Trailing spaces are invisible until somebody
51
+ // pipes this into a diff, and every line would carry them.
52
+ const line = (parts) => parts.map((p, i) => (i === parts.length - 1 ? p : p.padEnd(width[i]))).join(" ").trimEnd();
53
+
54
+ return [
55
+ line(cols.map((c) => cell(c, max))),
56
+ line(width.map((w) => "─".repeat(w))),
57
+ ...body.map(line),
58
+ ];
59
+ }
60
+
61
+ module.exports = { renderTable, cell };
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * What `bay share` was asked to do, decided before anything is sent.
5
+ *
6
+ * Access is the one command in this CLI where a misread argument does damage
7
+ * that is not visible afterwards: `share app add acme.com` meant as "everyone at
8
+ * my company" and read as an email address would fail loudly, which is fine —
9
+ * but read the other way round it would let a whole domain into an app the
10
+ * owner meant to send to one person, and nothing on screen would look wrong.
11
+ * So the reading is a pure function with tests, and the network call is what
12
+ * happens after it.
13
+ *
14
+ * The two audiences are told apart by shape, not by a flag:
15
+ *
16
+ * ada@acme.com an address -> one person, invited by email
17
+ * @acme.com a rule -> everyone whose address ends in it
18
+ * acme.com the same rule -> the @ is how people write it, not a requirement
19
+ *
20
+ * A bare word with no dot is neither, and is refused rather than guessed at.
21
+ */
22
+
23
+ /** The three visibilities the control plane accepts, in the order they widen. */
24
+ const VISIBILITIES = ["private", "shared", "public"];
25
+
26
+ /**
27
+ * @param {string} who
28
+ * @returns {{audience: "email"|"domain", value: string}|null}
29
+ */
30
+ function audienceOf(who) {
31
+ const w = String(who ?? "").trim().toLowerCase();
32
+ if (!w) return null;
33
+
34
+ // An @ anywhere but the front makes it an address. RFC 5321 caps a mailbox at
35
+ // 254 characters; past that it is not an address that could ever be delivered
36
+ // to, and the server refuses it — better said here, where the reason fits on
37
+ // one line.
38
+ if (w.includes("@") && !w.startsWith("@")) {
39
+ if (w.length > 254 || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(w)) return null;
40
+ return { audience: "email", value: w };
41
+ }
42
+
43
+ const domain = w.replace(/^@/, "");
44
+ if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(domain)) return null;
45
+ return { audience: "domain", value: domain };
46
+ }
47
+
48
+ /**
49
+ * @param {string[]} rest the arguments after the app name
50
+ * @returns {{kind: "show"}
51
+ * |{kind: "visibility", visibility: string}
52
+ * |{kind: "add"|"remove", audience: "email"|"domain", value: string}
53
+ * |{kind: "error", why: string}}
54
+ */
55
+ function parseShare(rest) {
56
+ const [verb, ...targets] = (rest || []).filter((t) => t !== undefined && t !== null);
57
+ if (!verb) return { kind: "show" };
58
+
59
+ const v = String(verb).trim().toLowerCase();
60
+ if (VISIBILITIES.includes(v)) return { kind: "visibility", visibility: v };
61
+
62
+ if (v === "add" || v === "remove" || v === "rm") {
63
+ const kind = v === "rm" ? "remove" : v;
64
+ if (!targets.length) {
65
+ return { kind: "error", why: `who? usage: bay share <app> ${kind} <email|@domain>` };
66
+ }
67
+ const audience = audienceOf(targets[0]);
68
+ if (!audience) {
69
+ return {
70
+ kind: "error",
71
+ why: `"${targets[0]}" is neither an email address nor a domain — write ada@acme.com for one person, or @acme.com for everyone there`,
72
+ };
73
+ }
74
+ return { kind, ...audience };
75
+ }
76
+
77
+ return {
78
+ kind: "error",
79
+ why: `unknown: share ${v}\nusage: bay share <app> [private|shared|public | add <email|@domain> | remove <email|@domain>]`,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * The request body for an add/remove, in the shape the control plane's share
85
+ * route reads. Kept beside the parser because the four field names — addEmail,
86
+ * addDomain, removeEmail, removeDomain — are the only thing connecting the two.
87
+ */
88
+ function shareBody(action) {
89
+ const suffix = action.audience === "email" ? "Email" : "Domain";
90
+ return { [action.kind + suffix]: action.value };
91
+ }
92
+
93
+ module.exports = { parseShare, audienceOf, shareBody, VISIBILITIES };
package/lib/who.js CHANGED
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * Who is shipping, as declared — never as inferred.
4
4
  *
5
- * An agent sets BAY_WHO=agent (SUPERSONIC_WHO still works). Nothing else is consulted: a TTY check
6
- * would call CI an agent, and the platform would then draw a figure that was
7
- * never there. The absence of a name is a fact; a wrong name is a lie.
5
+ * An agent sets BAY_WHO=agent (SUPERSONIC_WHO is still read, because it is what
6
+ * the agent prompts written before the rename say). Nothing else is consulted: a
7
+ * TTY check would call CI an agent, and the platform would then draw a figure
8
+ * that was never there. The absence of a name is a fact; a wrong name is a lie.
8
9
  */
9
10
  function whoHeader(env) {
10
11
  const v = String(env.BAY_WHO || env.SUPERSONIC_WHO || "").trim().toLowerCase();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@thebaycloud/cli",
3
- "version": "1.0.0",
4
- "description": "Deploy & debug Bay apps from your coding agent the agent-native CLI.",
3
+ "version": "1.1.0",
4
+ "description": "Ship & debug Bay apps from your coding agent \u2014 the agent-native CLI.",
5
5
  "bin": {
6
- "bay": "index.js",
7
- "supersonic": "index.js"
6
+ "bay": "./index.js",
7
+ "supersonic": "./index.js"
8
8
  },
9
9
  "type": "commonjs",
10
10
  "engines": {
@@ -21,16 +21,16 @@
21
21
  "bundle-detector": "node scripts/bundle-detector.mjs",
22
22
  "bundle-resolver": "node scripts/bundle-resolver.mjs",
23
23
  "bundle": "npm run bundle-detector && npm run bundle-resolver",
24
- "test": "node --test 'test/**/*.test.js'",
24
+ "test": "node --test",
25
25
  "prepublishOnly": "npm run bundle && npm test"
26
26
  },
27
27
  "keywords": [
28
28
  "deploy",
29
29
  "cloud",
30
30
  "gcp",
31
+ "cloud-run",
31
32
  "vibecoding",
32
- "bay",
33
- "coding-agent"
33
+ "bay"
34
34
  ],
35
35
  "homepage": "https://thebay.cloud",
36
36
  "repository": {
@@ -39,10 +39,10 @@
39
39
  "directory": "packages/cli"
40
40
  },
41
41
  "license": "MIT",
42
- "bugs": {
43
- "url": "https://github.com/The-Red-Onion/supersonic/issues"
44
- },
45
42
  "publishConfig": {
46
43
  "access": "public"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/thebaycloud/bay/issues"
47
47
  }
48
48
  }
package/vendor/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Generated. Do not edit — `npm run bundle` rebuilds all of it.
2
2
 
3
- - detector.js — scripts/bundle-detector.mjs, from services/deploy-agent
3
+ - detector.js — scripts/bundle-detector.mjs, from packages/detector
4
4
  - resolve.js — scripts/bundle-resolver.mjs, from apps/web/lib
5
5
  - inputs.json — every repository file esbuild inlined into each bundle, from its metafile.
6
6
  test/vendor.test.js hashes these to prove the bundles are not stale.
@@ -1,4 +1,4 @@
1
- // supersonic-vendor-stamp 541ed09e927ae777
1
+ // supersonic-vendor-stamp 90d7b95c32307760
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -349,7 +349,7 @@ function provisionPlan(s) {
349
349
  plan.push("Apply security baseline (secret scan, rate limits, WAF)");
350
350
  plan.push("Enable daily backups");
351
351
  if (s.secretsNeeded.length) plan.push(`Ask user for secrets: ${s.secretsNeeded.join(", ")}`);
352
- plan.push(`Deploy to Cloud Run on :${s.port} \u2192 <slug>.supersonic.cv`);
352
+ plan.push(`Deploy to Cloud Run on :${s.port} \u2192 <slug>.thebay.cloud`);
353
353
  return plan;
354
354
  }
355
355
  function cloneToTemp(url) {
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "detector.js": [
3
3
  "apps/web/lib/plan-deps.ts",
4
- "services/deploy-agent/src/index.ts"
4
+ "packages/detector/src/index.ts"
5
5
  ],
6
6
  "resolve.js": [
7
7
  "apps/web/lib/app-config.ts",
8
8
  "apps/web/lib/db-address.ts",
9
9
  "apps/web/lib/detect.ts",
10
10
  "apps/web/lib/dockerfile-context.ts",
11
+ "apps/web/lib/env-owner.ts",
11
12
  "apps/web/lib/infer-services.ts",
12
13
  "apps/web/lib/lanes.ts",
13
14
  "apps/web/lib/plan-deps.ts",
package/vendor/resolve.js CHANGED
@@ -1,4 +1,4 @@
1
- // supersonic-vendor-stamp d638cb7c23598653
1
+ // supersonic-vendor-stamp 60282bc736f6efbe
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -118,15 +118,17 @@ function withScale(over) {
118
118
  return { ...DEFAULT_SCALE, ...declared };
119
119
  }
120
120
 
121
+ // lib/env-owner.ts
122
+ var ALWAYS_OWNED_PREFIXES = [/^BAY_/, /^SUPERSONIC_/];
123
+ var ALWAYS_OWNED_EXACT = /* @__PURE__ */ new Set(["PORT"]);
124
+ var DATABASE_OWNED_PREFIXES = [/^POSTGRES_/, /^PG/, /^DB_/];
125
+ var DATABASE_OWNED_EXACT = new Set(databaseEnvNames());
126
+
121
127
  // lib/app-config.ts
122
128
  var CONFIG_FILENAME = "supersonic.json";
123
129
  var ConfigError = class extends Error {
124
130
  };
125
131
  var LANGUAGES = /* @__PURE__ */ new Set(["node", "python", "static", "other"]);
126
- var ALWAYS_OWNED_PREFIXES = [/^SUPERSONIC_/];
127
- var ALWAYS_OWNED_EXACT = /* @__PURE__ */ new Set(["PORT"]);
128
- var DATABASE_OWNED_PREFIXES = [/^POSTGRES_/, /^PG/, /^DB_/];
129
- var DATABASE_OWNED_EXACT = new Set(databaseEnvNames());
130
132
  function platformOwned(name, database) {
131
133
  if (ALWAYS_OWNED_EXACT.has(name) || ALWAYS_OWNED_PREFIXES.some((re) => re.test(name))) return true;
132
134
  if (database?.provider !== "managed") return false;
@@ -576,14 +578,14 @@ function runtimeMismatch(manifests) {
576
578
  if (py) {
577
579
  const need = lowestAccepted(py);
578
580
  if (need && below(RUNTIME_VERSIONS.python, need)) {
579
- return `this app needs Python ${py} and the runner has ${RUNTIME_VERSIONS.python} \u2014 widen requires-python in pyproject.toml to accept ${RUNTIME_VERSIONS.python}, or wait for the runner to move. Nothing in the code can fix this one.`;
581
+ return `this app needs Python ${py} and the platform has ${RUNTIME_VERSIONS.python} \u2014 widen requires-python in pyproject.toml to accept ${RUNTIME_VERSIONS.python}, or wait for the platform to move. Nothing in the code can fix this one.`;
580
582
  }
581
583
  }
582
584
  const engines = manifests.packageJson?.engines?.node;
583
585
  if (engines) {
584
586
  const need = lowestAccepted(engines);
585
587
  if (need && below(RUNTIME_VERSIONS.node, need)) {
586
- return `this app needs Node ${engines} and the runner has ${RUNTIME_VERSIONS.node} \u2014 widen engines.node in package.json to accept ${RUNTIME_VERSIONS.node}, or wait for the runner to move. Nothing in the code can fix this one.`;
588
+ return `this app needs Node ${engines} and the platform has ${RUNTIME_VERSIONS.node} \u2014 widen engines.node in package.json to accept ${RUNTIME_VERSIONS.node}, or wait for the platform to move. Nothing in the code can fix this one.`;
587
589
  }
588
590
  }
589
591
  return null;