@thebaycloud/cli 1.0.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.
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Turn the words after `--` into one command for a remote `sh`.
5
+ *
6
+ * Two shapes reach this, and joining them the same way breaks one of them:
7
+ *
8
+ * bay exec app -- "ls | wc -l" one argument, already a shell
9
+ * snippet — the pipe is meant
10
+ * bay exec app -- python -c 'print(1)' argv, and the local shell has
11
+ * already removed the quotes
12
+ *
13
+ * A plain `.join(" ")` served the first and silently corrupted the second. The
14
+ * server base64s whatever it is given and pipes it to `sh`, so `print(1)`
15
+ * arrives bare and sh answers `Syntax error: "(" unexpected`. Anything with a
16
+ * parenthesis, a quote, a `$`, a `;` or a space inside one argv word was
17
+ * affected, which is most non-trivial one-liners.
18
+ *
19
+ * One argument stays raw, because the user wrote shell and means it. Several
20
+ * arguments are quoted individually, because the shell already split them and
21
+ * the quotes it removed have to be put back. `docker exec` and `kubectl exec`
22
+ * draw the line in the same place.
23
+ */
24
+ function joinExecArgs(words) {
25
+ const w = Array.isArray(words) ? words : [];
26
+ if (w.length <= 1) return String(w[0] ?? "").trim();
27
+ return w.map(shellQuote).join(" ").trim();
28
+ }
29
+
30
+ /**
31
+ * POSIX single-quoting. Everything is literal inside '…', and a single quote is
32
+ * itself escaped by closing the string, adding \' , and reopening.
33
+ */
34
+ function shellQuote(word) {
35
+ const s = String(word);
36
+ if (s !== "" && /^[A-Za-z0-9_@%+=:,.\/-]+$/.test(s)) return s;
37
+ return "'" + s.replace(/'/g, "'\\''") + "'";
38
+ }
39
+
40
+ module.exports = { joinExecArgs, shellQuote };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Deciding whether to build here or in the cloud, and hashing what we built.
4
+ *
5
+ * The decision is a pure function so it can be tested without running builds, and so
6
+ * the reason for every choice is written down in one place rather than spread through
7
+ * the deploy command.
8
+ */
9
+ const crypto = require("node:crypto");
10
+ const fs = require("node:fs");
11
+ const path = require("node:path");
12
+
13
+ /**
14
+ * What to do with a project, given what the detector said about it.
15
+ *
16
+ * `container` never builds locally: its artifact is an image, not a directory, and we
17
+ * are not building images on someone else's machine.
18
+ */
19
+ function planFor(stack) {
20
+ const serve = stack && stack.serve;
21
+ if (!serve || serve.mode !== "static") {
22
+ return { mode: "cloud", reason: "this app runs a server, so it is built in the cloud" };
23
+ }
24
+ const outputDir = String(serve.outputDir || ".");
25
+ if (!stack.buildCommand) {
26
+ // Nothing to build — the folder already is the site.
27
+ return { mode: "upload", outputDir, reason: "no build step — uploading the folder as-is" };
28
+ }
29
+ return {
30
+ mode: "build",
31
+ outputDir,
32
+ installCommand: stack.installCommand || null,
33
+ buildCommand: stack.buildCommand,
34
+ };
35
+ }
36
+
37
+ /** Files under a directory, relative and slash-separated, sorted. */
38
+ function listFiles(root, dir = root, out = []) {
39
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
40
+ const full = path.join(dir, item.name);
41
+ if (item.isDirectory()) listFiles(root, full, out);
42
+ else if (item.isFile()) out.push(path.relative(root, full).split(path.sep).join("/"));
43
+ }
44
+ return out.sort();
45
+ }
46
+
47
+ /**
48
+ * Content hash of a built directory. Must match apps/web/lib/dirhash.ts exactly —
49
+ * the two are compared across the wire, and a mismatch in either direction silently
50
+ * disables the skip rather than failing loudly.
51
+ */
52
+ function hashDir(root) {
53
+ const h = crypto.createHash("sha256");
54
+ for (const rel of listFiles(root)) {
55
+ h.update(rel);
56
+ h.update("\0");
57
+ h.update(fs.readFileSync(path.join(root, rel)));
58
+ h.update("\0");
59
+ }
60
+ return h.digest("hex");
61
+ }
62
+
63
+ /** True when the build output exists and has something in it. */
64
+ function hasOutput(dir) {
65
+ try {
66
+ return fs.statSync(dir).isDirectory() && listFiles(dir).length > 0;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Warn when the local Node major differs from what the project asks for. This is the
74
+ * single most likely cause of "it built on my machine and broke in production", and a
75
+ * word at build time is worth more than a support conversation afterwards.
76
+ */
77
+ function nodeVersionWarning(stack, localVersion = process.version) {
78
+ const runtime = String((stack && stack.runtime) || "");
79
+ if (!runtime.startsWith("node:")) return null;
80
+ const want = runtime.slice(5).match(/\d+/);
81
+ const have = String(localVersion).match(/\d+/);
82
+ if (!want || !have || want[0] === have[0]) return null;
83
+ return `this project targets Node ${want[0]} but you are on Node ${have[0]} — building anyway`;
84
+ }
85
+
86
+ module.exports = { planFor, hashDir, listFiles, hasOutput, nodeVersionWarning };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * The one door between the CLI and the control plane's resolution logic.
4
+ *
5
+ * `vendor/resolve.js` is apps/web/lib/{resolve,app-config,infer-services,
6
+ * repo-facts,lanes,plan-deps}.ts compiled by scripts/bundle-resolver.mjs — the
7
+ * same source the server runs, not a port of it. Everything in this package that
8
+ * needs to know what a repository deploys to goes through here, so there is
9
+ * exactly one answer to that question in the repository and no way for a second
10
+ * one to appear without deleting this file.
11
+ *
12
+ * Loaded lazily and through a named function rather than at require() time: the
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.
16
+ */
17
+ const path = require("node:path");
18
+
19
+ let cached = null;
20
+
21
+ /** The bundled resolver, or a thrown error saying how to build it. */
22
+ function resolver() {
23
+ if (cached) return cached;
24
+ try {
25
+ cached = require("../vendor/resolve.js");
26
+ } catch (e) {
27
+ throw new Error(
28
+ "this build of the CLI has no bundled resolver (vendor/resolve.js).\n" +
29
+ " From a checkout: npm run bundle -w supersonic-cli\n" +
30
+ ` Underlying error: ${e && e.message ? e.message : String(e)}`,
31
+ );
32
+ }
33
+ return cached;
34
+ }
35
+
36
+ /**
37
+ * The stack detector, as the async `Detect` that inferAppConfig expects.
38
+ *
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
41
+ * subprocess for the same job, which is why the interface is a promise at all;
42
+ * matching the interface rather than the transport is what lets both callers
43
+ * share inferAppConfig unchanged.
44
+ */
45
+ function detector() {
46
+ const { detectStack } = require("../vendor/detector.js");
47
+ return async (absoluteDir) => detectStack(absoluteDir);
48
+ }
49
+
50
+ /**
51
+ * Resolve a directory the way a deploy will, with inference as the fallback.
52
+ *
53
+ * The detector is passed in every time. resolve() without one throws on a repo
54
+ * that has no supersonic.json — correct for the server, which must not guess
55
+ * silently, and useless for a CLI whose entire job here is to show the user what
56
+ * the guess would be before it costs eleven minutes.
57
+ */
58
+ async function resolveHere(dir) {
59
+ const r = resolver();
60
+ return r.resolve(dir, detector());
61
+ }
62
+
63
+ module.exports = { resolver, detector, resolveHere, BUNDLE: path.join(__dirname, "..", "vendor", "resolve.js") };
package/lib/who.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * Who is shipping, as declared — never as inferred.
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.
8
+ */
9
+ function whoHeader(env) {
10
+ const v = String(env.BAY_WHO || env.SUPERSONIC_WHO || "").trim().toLowerCase();
11
+ return v === "you" || v === "agent" || v === "platform" ? v : "someone";
12
+ }
13
+
14
+ module.exports = { whoHeader };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@thebaycloud/cli",
3
+ "version": "1.0.0",
4
+ "description": "Deploy & debug Bay apps from your coding agent — the agent-native CLI.",
5
+ "bin": {
6
+ "bay": "index.js",
7
+ "supersonic": "index.js"
8
+ },
9
+ "type": "commonjs",
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "lib/",
16
+ "vendor/",
17
+ "README.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "bundle-detector": "node scripts/bundle-detector.mjs",
22
+ "bundle-resolver": "node scripts/bundle-resolver.mjs",
23
+ "bundle": "npm run bundle-detector && npm run bundle-resolver",
24
+ "test": "node --test 'test/**/*.test.js'",
25
+ "prepublishOnly": "npm run bundle && npm test"
26
+ },
27
+ "keywords": [
28
+ "deploy",
29
+ "cloud",
30
+ "gcp",
31
+ "vibecoding",
32
+ "bay",
33
+ "coding-agent"
34
+ ],
35
+ "homepage": "https://thebay.cloud",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/thebaycloud/bay.git",
39
+ "directory": "packages/cli"
40
+ },
41
+ "license": "MIT",
42
+ "bugs": {
43
+ "url": "https://github.com/The-Red-Onion/supersonic/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ Generated. Do not edit — `npm run bundle` rebuilds all of it.
2
+
3
+ - detector.js — scripts/bundle-detector.mjs, from services/deploy-agent
4
+ - resolve.js — scripts/bundle-resolver.mjs, from apps/web/lib
5
+ - inputs.json — every repository file esbuild inlined into each bundle, from its metafile.
6
+ test/vendor.test.js hashes these to prove the bundles are not stale.
@@ -0,0 +1,423 @@
1
+ // supersonic-vendor-stamp 541ed09e927ae777
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ astroServe: () => astroServe,
24
+ detectStack: () => detectStack,
25
+ dockerfile: () => dockerfile,
26
+ installFor: () => installFor,
27
+ nextServe: () => nextServe,
28
+ provisionPlan: () => provisionPlan
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var import_node_fs = require("node:fs");
32
+ var import_node_path = require("node:path");
33
+ var import_node_os = require("node:os");
34
+ var import_node_child_process = require("node:child_process");
35
+ var import_node_url = require("node:url");
36
+
37
+ // ../../apps/web/lib/plan-deps.ts
38
+ var RUNTIME_VERSIONS = { python: "3.14", node: "24" };
39
+
40
+ // src/index.ts
41
+ var import_meta = {};
42
+ var CONTAINER = { mode: "container" };
43
+ function installFor(pm, hasNpmLock) {
44
+ switch (pm) {
45
+ case "pnpm":
46
+ return "pnpm i --frozen-lockfile";
47
+ case "yarn":
48
+ return "yarn --frozen-lockfile";
49
+ case "bun":
50
+ return "bun install";
51
+ default:
52
+ return hasNpmLock ? "npm ci" : "npm install";
53
+ }
54
+ }
55
+ function astroServe(configSource) {
56
+ const src = configSource ?? "";
57
+ const hasAdapter = /adapter\s*:/.test(src) || /@astrojs\/(node|vercel|netlify|cloudflare|deno)/.test(src);
58
+ return hasAdapter ? CONTAINER : { mode: "static", outputDir: "dist" };
59
+ }
60
+ function nextServe(configSource) {
61
+ const src = configSource ?? "";
62
+ return /output\s*:\s*["'`]export["'`]/.test(src) ? { mode: "static", outputDir: "out" } : CONTAINER;
63
+ }
64
+ function read(dir, file) {
65
+ const p = (0, import_node_path.join)(dir, file);
66
+ return (0, import_node_fs.existsSync)(p) ? (0, import_node_fs.readFileSync)(p, "utf8") : null;
67
+ }
68
+ function has(dir, file) {
69
+ return (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, file));
70
+ }
71
+ var SDK_SECRETS = {
72
+ stripe: "STRIPE_SECRET_KEY",
73
+ openai: "OPENAI_API_KEY",
74
+ "@anthropic-ai/sdk": "ANTHROPIC_API_KEY",
75
+ "@google/generative-ai": "GEMINI_API_KEY",
76
+ resend: "RESEND_API_KEY",
77
+ "@sendgrid/mail": "SENDGRID_API_KEY",
78
+ twilio: "TWILIO_AUTH_TOKEN",
79
+ "aws-sdk": "AWS_ACCESS_KEY_ID"
80
+ };
81
+ function detectStack(dir) {
82
+ const notes = [];
83
+ const pkg = read(dir, "package.json");
84
+ if (pkg) return detectNode(dir, pkg, notes);
85
+ if (has(dir, "requirements.txt") || has(dir, "pyproject.toml") || has(dir, "manage.py"))
86
+ return detectPython(dir, notes);
87
+ if (has(dir, "go.mod")) return detectGo(notes);
88
+ if (has(dir, "Gemfile")) return detectRuby(dir, notes);
89
+ if (has(dir, "composer.json")) return detectPhp(dir, notes);
90
+ if (has(dir, "index.html")) return staticSite(notes);
91
+ notes.push("No recognized manifest \u2014 treating as a static site.");
92
+ return staticSite(notes);
93
+ }
94
+ function detectNode(dir, pkgRaw, notes) {
95
+ let pkg = {};
96
+ try {
97
+ pkg = JSON.parse(pkgRaw);
98
+ } catch {
99
+ notes.push("package.json is not valid JSON.");
100
+ }
101
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
102
+ const scripts = pkg.scripts || {};
103
+ const dep = (n) => n in deps;
104
+ const language = dep("typescript") || has(dir, "tsconfig.json") ? "TypeScript" : "JavaScript";
105
+ const pm = has(dir, "pnpm-lock.yaml") ? "pnpm" : has(dir, "yarn.lock") ? "yarn" : has(dir, "bun.lockb") ? "bun" : "npm";
106
+ const installCommand = installFor(pm, has(dir, "package-lock.json"));
107
+ let framework = "Node";
108
+ let buildCommand = scripts.build ? `${pm} run build` : null;
109
+ let startCommand = scripts.start ? `${pm} start` : "node index.js";
110
+ let port = 3e3;
111
+ let serve = CONTAINER;
112
+ if (dep("next")) {
113
+ framework = "Next.js";
114
+ buildCommand = `${pm} run build`;
115
+ startCommand = `${pm} start`;
116
+ serve = nextServe(nextConfig(dir));
117
+ } else if (dep("@remix-run/node") || dep("@remix-run/react")) {
118
+ framework = "Remix";
119
+ buildCommand = `${pm} run build`;
120
+ startCommand = `${pm} start`;
121
+ } else if (dep("nuxt")) {
122
+ framework = "Nuxt";
123
+ buildCommand = `${pm} run build`;
124
+ startCommand = "node .output/server/index.mjs";
125
+ } else if (dep("@sveltejs/kit")) {
126
+ framework = "SvelteKit";
127
+ buildCommand = `${pm} run build`;
128
+ startCommand = "node build";
129
+ } else if (dep("astro")) {
130
+ framework = "Astro";
131
+ buildCommand = `${pm} run build`;
132
+ startCommand = "node ./dist/server/entry.mjs";
133
+ port = 4321;
134
+ serve = astroServe(astroConfig(dir));
135
+ } else if (dep("@nestjs/core")) {
136
+ framework = "NestJS";
137
+ buildCommand = `${pm} run build`;
138
+ startCommand = "node dist/main.js";
139
+ } else if (dep("vite")) {
140
+ framework = "Vite (SPA)";
141
+ buildCommand = `${pm} run build`;
142
+ startCommand = "(static)";
143
+ port = 80;
144
+ serve = { mode: "static", outputDir: "dist" };
145
+ notes.push("SPA \u2014 served as static assets behind the CDN.");
146
+ } else if (dep("react-scripts")) {
147
+ framework = "Create React App";
148
+ buildCommand = `${pm} run build`;
149
+ startCommand = "(static)";
150
+ port = 80;
151
+ serve = { mode: "static", outputDir: "build" };
152
+ } else if (dep("express") || dep("fastify") || dep("koa")) {
153
+ framework = dep("express") ? "Express" : dep("fastify") ? "Fastify" : "Koa";
154
+ }
155
+ let engine = null;
156
+ let via = null;
157
+ if (dep("@prisma/client") || dep("prisma") || has(dir, "prisma/schema.prisma")) {
158
+ via = "Prisma";
159
+ engine = prismaEngine(dir) ?? "postgres";
160
+ } else if (dep("drizzle-orm")) {
161
+ via = "Drizzle";
162
+ engine = dep("mysql2") ? "mysql" : dep("better-sqlite3") ? "sqlite" : "postgres";
163
+ } else if (dep("mongoose")) {
164
+ via = "Mongoose";
165
+ engine = "mongodb";
166
+ } else if (dep("typeorm")) {
167
+ via = "TypeORM";
168
+ engine = dep("mysql2") || dep("mysql") ? "mysql" : "postgres";
169
+ } else if (dep("sequelize")) {
170
+ via = "Sequelize";
171
+ engine = dep("mysql2") || dep("mysql") ? "mysql" : "postgres";
172
+ } else if (dep("pg") || dep("postgres")) {
173
+ via = "pg";
174
+ engine = "postgres";
175
+ } else if (dep("mysql2") || dep("mysql")) {
176
+ via = "mysql";
177
+ engine = "mysql";
178
+ }
179
+ const cache = dep("redis") || dep("ioredis") ? "redis" : null;
180
+ const secretsNeeded = Object.keys(SDK_SECRETS).filter(dep).map((k) => SDK_SECRETS[k]);
181
+ const nodeMajor = pkg.engines?.node?.match(/\d+/)?.[0] ?? "22";
182
+ return {
183
+ language,
184
+ framework,
185
+ packageManager: pm,
186
+ runtime: `node:${nodeMajor}`,
187
+ installCommand,
188
+ buildCommand,
189
+ startCommand,
190
+ port,
191
+ database: { engine, via },
192
+ cache,
193
+ secretsNeeded,
194
+ serve,
195
+ confidence: framework === "Node" ? 0.6 : 0.95,
196
+ notes
197
+ };
198
+ }
199
+ function nextConfig(dir) {
200
+ for (const f of ["next.config.js", "next.config.mjs", "next.config.ts"]) {
201
+ const s = read(dir, f);
202
+ if (s) return s;
203
+ }
204
+ return null;
205
+ }
206
+ function astroConfig(dir) {
207
+ for (const f of ["astro.config.mjs", "astro.config.js", "astro.config.ts"]) {
208
+ const s = read(dir, f);
209
+ if (s) return s;
210
+ }
211
+ return null;
212
+ }
213
+ function prismaEngine(dir) {
214
+ const s = read(dir, "prisma/schema.prisma");
215
+ if (!s) return null;
216
+ const p = s.match(/provider\s*=\s*"(\w+)"/)?.[1];
217
+ return p === "postgresql" ? "postgres" : p === "mysql" ? "mysql" : p === "sqlite" ? "sqlite" : p === "mongodb" ? "mongodb" : "postgres";
218
+ }
219
+ function detectPython(dir, notes) {
220
+ const reqs = (read(dir, "requirements.txt") || "") + (read(dir, "pyproject.toml") || "");
221
+ const m = (s) => new RegExp(s, "i").test(reqs);
222
+ let framework = "Python";
223
+ let startCommand = "python app.py";
224
+ let port = 8e3;
225
+ if (has(dir, "manage.py") || m("django")) {
226
+ framework = "Django";
227
+ startCommand = "gunicorn ${MODULE}.wsgi --bind 0.0.0.0:8000";
228
+ notes.push("Set ${MODULE} to your Django project package.");
229
+ } else if (m("fastapi")) {
230
+ framework = "FastAPI";
231
+ startCommand = "uvicorn main:app --host 0.0.0.0 --port 8000";
232
+ } else if (m("flask")) {
233
+ framework = "Flask";
234
+ startCommand = "gunicorn app:app --bind 0.0.0.0:8000";
235
+ }
236
+ let engine = null;
237
+ let via = null;
238
+ if (m("psycopg") || m("django")) {
239
+ engine = "postgres";
240
+ via = m("psycopg") ? "psycopg" : "Django ORM";
241
+ } else if (m("sqlalchemy")) {
242
+ engine = "postgres";
243
+ via = "SQLAlchemy";
244
+ } else if (m("pymysql") || m("mysqlclient")) {
245
+ engine = "mysql";
246
+ via = "mysql";
247
+ } else if (m("pymongo")) {
248
+ engine = "mongodb";
249
+ via = "pymongo";
250
+ }
251
+ const cache = m("redis") ? "redis" : null;
252
+ const secretsNeeded = [];
253
+ if (m("openai")) secretsNeeded.push("OPENAI_API_KEY");
254
+ if (m("stripe")) secretsNeeded.push("STRIPE_SECRET_KEY");
255
+ return {
256
+ language: "Python",
257
+ framework,
258
+ packageManager: "pip",
259
+ runtime: `python:${RUNTIME_VERSIONS.python}`,
260
+ installCommand: "pip install --no-cache-dir -r requirements.txt",
261
+ buildCommand: null,
262
+ startCommand,
263
+ port,
264
+ database: { engine, via },
265
+ cache,
266
+ secretsNeeded,
267
+ serve: CONTAINER,
268
+ confidence: framework === "Python" ? 0.6 : 0.9,
269
+ notes
270
+ };
271
+ }
272
+ function detectGo(notes) {
273
+ return { language: "Go", framework: "Go", packageManager: "go", runtime: "golang:1.22", installCommand: "go mod download", buildCommand: "go build -o server ./...", startCommand: "./server", port: 8080, database: { engine: null, via: null }, cache: null, secretsNeeded: [], serve: CONTAINER, confidence: 0.75, notes };
274
+ }
275
+ function detectRuby(dir, notes) {
276
+ const rails = (read(dir, "Gemfile") || "").match(/rails/i) != null;
277
+ return { language: "Ruby", framework: rails ? "Rails" : "Ruby", packageManager: "bundler", runtime: "ruby:3.3", installCommand: "bundle install", buildCommand: rails ? "bundle exec rails assets:precompile" : null, startCommand: rails ? "bundle exec rails server -b 0.0.0.0" : "ruby app.rb", port: 3e3, database: { engine: rails ? "postgres" : null, via: rails ? "ActiveRecord" : null }, cache: null, secretsNeeded: [], serve: CONTAINER, confidence: rails ? 0.9 : 0.6, notes };
278
+ }
279
+ function detectPhp(dir, notes) {
280
+ const laravel = (read(dir, "composer.json") || "").match(/laravel\/framework/i) != null;
281
+ return { language: "PHP", framework: laravel ? "Laravel" : "PHP", packageManager: "composer", runtime: "php:8.3", installCommand: "composer install --no-dev", buildCommand: null, startCommand: laravel ? "php artisan serve --host 0.0.0.0 --port 8000" : "php -S 0.0.0.0:8000", port: 8e3, database: { engine: laravel ? "mysql" : null, via: laravel ? "Eloquent" : null }, cache: null, secretsNeeded: [], serve: CONTAINER, confidence: laravel ? 0.9 : 0.6, notes };
282
+ }
283
+ function staticSite(notes) {
284
+ return { language: "Static", framework: "Static site", packageManager: null, runtime: "nginx", installCommand: null, buildCommand: null, startCommand: "(nginx)", port: 80, database: { engine: null, via: null }, cache: null, secretsNeeded: [], serve: { mode: "static", outputDir: "." }, confidence: 0.8, notes };
285
+ }
286
+ function dockerfile(s) {
287
+ if (s.runtime.startsWith("node")) return nodeDockerfile(s);
288
+ if (s.runtime.startsWith("python")) return pythonDockerfile(s);
289
+ if (s.runtime === "nginx") return `FROM nginx:alpine
290
+ COPY . /usr/share/nginx/html
291
+ EXPOSE 80
292
+ `;
293
+ const v = s.runtime.split(":")[1] ?? "latest";
294
+ const base = s.runtime.split(":")[0];
295
+ return `FROM ${base}:${v}
296
+ WORKDIR /app
297
+ COPY . .
298
+ ${s.installCommand ? `RUN ${s.installCommand}
299
+ ` : ""}${s.buildCommand ? `RUN ${s.buildCommand}
300
+ ` : ""}ENV PORT=${s.port}
301
+ EXPOSE ${s.port}
302
+ CMD ${s.startCommand}
303
+ `;
304
+ }
305
+ function nodeDockerfile(s) {
306
+ const v = s.runtime.split(":")[1];
307
+ return [
308
+ `FROM node:${v}-slim AS deps`,
309
+ `WORKDIR /app`,
310
+ `COPY package*.json pnpm-lock.yaml* yarn.lock* ./`,
311
+ `RUN ${s.installCommand}`,
312
+ ``,
313
+ `FROM node:${v}-slim AS build`,
314
+ `WORKDIR /app`,
315
+ `COPY --from=deps /app/node_modules ./node_modules`,
316
+ `COPY . .`,
317
+ s.buildCommand ? `RUN ${s.buildCommand}` : `# no build step`,
318
+ ``,
319
+ `FROM node:${v}-slim`,
320
+ `WORKDIR /app`,
321
+ `ENV NODE_ENV=production PORT=${s.port}`,
322
+ `COPY --from=build /app ./`,
323
+ `EXPOSE ${s.port}`,
324
+ `CMD ["sh","-c","${s.startCommand}"]`,
325
+ ``
326
+ ].join("\n");
327
+ }
328
+ function pythonDockerfile(s) {
329
+ const v = s.runtime.split(":")[1];
330
+ return [
331
+ `FROM python:${v}-slim`,
332
+ `WORKDIR /app`,
333
+ `COPY requirements.txt ./`,
334
+ `RUN ${s.installCommand}`,
335
+ `COPY . .`,
336
+ `ENV PORT=${s.port}`,
337
+ `EXPOSE ${s.port}`,
338
+ `CMD ["sh","-c","${s.startCommand}"]`,
339
+ ``
340
+ ].join("\n");
341
+ }
342
+ function provisionPlan(s) {
343
+ const plan = [];
344
+ if (s.database.engine) plan.push(`Provision ${s.database.engine} (detected via ${s.database.via})`);
345
+ if (s.cache) plan.push(`Provision ${s.cache} cache`);
346
+ plan.push("Wire managed auth (Identity Platform)");
347
+ plan.push("Wire transactional email + object storage + CDN");
348
+ plan.push("Embed analytics (PostHog)");
349
+ plan.push("Apply security baseline (secret scan, rate limits, WAF)");
350
+ plan.push("Enable daily backups");
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`);
353
+ return plan;
354
+ }
355
+ function cloneToTemp(url) {
356
+ const dir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "ss-"));
357
+ (0, import_node_child_process.execSync)(`git clone --depth 1 ${url} ${dir}`, { stdio: "pipe" });
358
+ return dir;
359
+ }
360
+ function printSummary(stack) {
361
+ const line = "\u2500".repeat(52);
362
+ const db = stack.database.engine ? `${stack.database.engine} (${stack.database.via})` : "none";
363
+ console.log(line);
364
+ console.log(` DETECTED ${stack.framework} \xB7 ${stack.language} \xB7 ${Math.round(stack.confidence * 100)}% confidence`);
365
+ console.log(line);
366
+ console.log(` runtime ${stack.runtime}`);
367
+ if (stack.packageManager) console.log(` pkg mgr ${stack.packageManager}`);
368
+ if (stack.installCommand) console.log(` install ${stack.installCommand}`);
369
+ if (stack.buildCommand) console.log(` build ${stack.buildCommand}`);
370
+ console.log(` start ${stack.startCommand}`);
371
+ console.log(` port ${stack.port}`);
372
+ console.log(` database ${db}`);
373
+ if (stack.cache) console.log(` cache ${stack.cache}`);
374
+ console.log(` secrets ${stack.secretsNeeded.length ? stack.secretsNeeded.join(", ") : "none needed"}`);
375
+ if (stack.notes.length) stack.notes.forEach((n) => console.log(` note ${n}`));
376
+ console.log(line);
377
+ console.log(" PROVISION PLAN");
378
+ provisionPlan(stack).forEach((p, i) => console.log(` ${String(i + 1).padStart(2, "0")} ${p}`));
379
+ console.log(line);
380
+ }
381
+ function main() {
382
+ const args = process.argv.slice(2);
383
+ const target = args.find((a) => !a.startsWith("--")) ?? ".";
384
+ let dir = target;
385
+ if (/^(https?:\/\/|git@)/.test(target)) {
386
+ try {
387
+ dir = cloneToTemp(target);
388
+ } catch (e) {
389
+ console.error(`Clone failed: ${e.message}`);
390
+ process.exit(1);
391
+ }
392
+ }
393
+ if (!(0, import_node_fs.existsSync)(dir)) {
394
+ console.error(`Path not found: ${dir}`);
395
+ process.exit(1);
396
+ }
397
+ const stack = detectStack(dir);
398
+ if (args.includes("--api")) {
399
+ process.stdout.write(JSON.stringify({ stack, provisionPlan: provisionPlan(stack) }));
400
+ return;
401
+ }
402
+ if (args.includes("--emit")) {
403
+ process.stdout.write(dockerfile(stack));
404
+ return;
405
+ }
406
+ printSummary(stack);
407
+ if (args.includes("--dockerfile")) {
408
+ console.log("\nDockerfile\n" + "\u2500".repeat(52) + "\n" + dockerfile(stack));
409
+ }
410
+ if (args.includes("--json")) {
411
+ console.log("\n" + JSON.stringify({ stack, dockerfile: dockerfile(stack), provisionPlan: provisionPlan(stack) }, null, 2));
412
+ }
413
+ }
414
+ if (import_meta.url === (0, import_node_url.pathToFileURL)(process.argv[1] ?? "").href) main();
415
+ // Annotate the CommonJS export names for ESM import in node:
416
+ 0 && (module.exports = {
417
+ astroServe,
418
+ detectStack,
419
+ dockerfile,
420
+ installFor,
421
+ nextServe,
422
+ provisionPlan
423
+ });