@miosa/cli 1.0.35 → 1.0.37

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,296 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { isJsonMode } from "./util.js";
5
+ import { UserError } from "../errors.js";
6
+ const STARTERS = {
7
+ nextjs: {
8
+ id: "nextjs",
9
+ name: "Next.js App",
10
+ description: "App Router starter for page/template based apps",
11
+ files: [
12
+ {
13
+ path: "miosa.app.yml",
14
+ content: `template: nextjs
15
+ workdir: /workspace
16
+ install: npm install
17
+ dev: npm run dev -- -H 0.0.0.0 -p 3000
18
+ build: npm run build
19
+ start: npm start
20
+ port: 3000
21
+ readiness:
22
+ path: /
23
+ `,
24
+ },
25
+ {
26
+ path: "package.json",
27
+ content: JSON.stringify({
28
+ scripts: {
29
+ dev: "next dev",
30
+ build: "next build",
31
+ start: "next start -H 0.0.0.0 -p 3000",
32
+ },
33
+ dependencies: {
34
+ "@types/node": "latest",
35
+ "@types/react": "latest",
36
+ "@types/react-dom": "latest",
37
+ next: "latest",
38
+ react: "latest",
39
+ "react-dom": "latest",
40
+ typescript: "latest",
41
+ },
42
+ devDependencies: {},
43
+ }, null, 2) + "\n",
44
+ },
45
+ {
46
+ path: "app/page.tsx",
47
+ content: `export default function Page() {
48
+ return (
49
+ <main style={{ fontFamily: "Inter, system-ui, sans-serif", padding: 40 }}>
50
+ <h1>MIOSA app</h1>
51
+ <p>Build in a sandbox, preview instantly, publish when ready.</p>
52
+ </main>
53
+ );
54
+ }
55
+ `,
56
+ },
57
+ {
58
+ path: "app/layout.tsx",
59
+ content: `export const metadata = { title: "MIOSA app" };
60
+
61
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
62
+ return (
63
+ <html lang="en">
64
+ <body>{children}</body>
65
+ </html>
66
+ );
67
+ }
68
+ `,
69
+ },
70
+ { path: "tsconfig.json", content: "{}\n" },
71
+ ],
72
+ next: [
73
+ "miosa sandbox deploy . --wait",
74
+ "miosa sandbox publish <sandbox-id> --slug my-app --wait",
75
+ ],
76
+ },
77
+ "nextjs-postgres": {
78
+ id: "nextjs-postgres",
79
+ name: "Next.js + Postgres App",
80
+ description: "Next.js starter that expects DATABASE_URL at runtime",
81
+ files: [
82
+ {
83
+ path: "miosa.app.yml",
84
+ content: `template: nextjs-postgres
85
+ workdir: /workspace
86
+ install: npm install
87
+ dev: npm run dev -- -H 0.0.0.0 -p 3000
88
+ build: npm run build
89
+ start: npm start
90
+ port: 3000
91
+ readiness:
92
+ path: /
93
+ `,
94
+ },
95
+ {
96
+ path: "package.json",
97
+ content: JSON.stringify({
98
+ scripts: {
99
+ dev: "next dev",
100
+ build: "next build",
101
+ start: "next start -H 0.0.0.0 -p 3000",
102
+ },
103
+ dependencies: {
104
+ "@types/node": "latest",
105
+ "@types/react": "latest",
106
+ "@types/react-dom": "latest",
107
+ next: "latest",
108
+ pg: "latest",
109
+ react: "latest",
110
+ "react-dom": "latest",
111
+ typescript: "latest",
112
+ },
113
+ devDependencies: {},
114
+ }, null, 2) + "\n",
115
+ },
116
+ {
117
+ path: "app/page.tsx",
118
+ content: `export default function Page() {
119
+ return (
120
+ <main style={{ fontFamily: "Inter, system-ui, sans-serif", padding: 40 }}>
121
+ <h1>MIOSA Next.js + Postgres</h1>
122
+ <p>DATABASE_URL is {process.env.DATABASE_URL ? "configured" : "not configured"}.</p>
123
+ </main>
124
+ );
125
+ }
126
+ `,
127
+ },
128
+ {
129
+ path: "app/layout.tsx",
130
+ content: `export const metadata = { title: "MIOSA Postgres app" };
131
+
132
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
133
+ return (
134
+ <html lang="en">
135
+ <body>{children}</body>
136
+ </html>
137
+ );
138
+ }
139
+ `,
140
+ },
141
+ { path: "tsconfig.json", content: "{}\n" },
142
+ ],
143
+ next: [
144
+ "miosa databases create --engine postgres --wait",
145
+ "miosa sandbox deploy . --template nextjs-postgres --wait",
146
+ "miosa sandbox publish <sandbox-id> --database existing:<db-id> --slug my-app --wait",
147
+ ],
148
+ },
149
+ "vite-react": {
150
+ id: "vite-react",
151
+ name: "Vite React App",
152
+ description: "Fast frontend app starter",
153
+ files: [
154
+ {
155
+ path: "miosa.app.yml",
156
+ content: `template: vite-react
157
+ workdir: /workspace
158
+ install: npm install
159
+ dev: npm run dev -- --host 0.0.0.0 --port 5173
160
+ build: npm run build
161
+ output: dist
162
+ port: 5173
163
+ readiness:
164
+ path: /
165
+ `,
166
+ },
167
+ {
168
+ path: "package.json",
169
+ content: JSON.stringify({
170
+ scripts: { dev: "vite", build: "vite build", preview: "vite preview" },
171
+ dependencies: {
172
+ "@vitejs/plugin-react": "latest",
173
+ vite: "latest",
174
+ react: "latest",
175
+ "react-dom": "latest",
176
+ typescript: "latest",
177
+ },
178
+ devDependencies: {},
179
+ }, null, 2) + "\n",
180
+ },
181
+ { path: "index.html", content: `<div id="root"></div><script type="module" src="/src/App.tsx"></script>\n` },
182
+ {
183
+ path: "src/App.tsx",
184
+ content: `import { createRoot } from "react-dom/client";
185
+
186
+ function App() {
187
+ return <main style={{ padding: 40, fontFamily: "system-ui" }}>MIOSA Vite app</main>;
188
+ }
189
+
190
+ createRoot(document.getElementById("root")!).render(<App />);
191
+ `,
192
+ },
193
+ ],
194
+ next: ["miosa sandbox deploy . --wait"],
195
+ },
196
+ fastapi: {
197
+ id: "fastapi",
198
+ name: "FastAPI App",
199
+ description: "Python API starter",
200
+ files: [
201
+ {
202
+ path: "miosa.app.yml",
203
+ content: `template: fastapi
204
+ workdir: /workspace
205
+ install: pip install -r requirements.txt
206
+ dev: uvicorn main:app --host 0.0.0.0 --port 8000
207
+ build: "true"
208
+ start: uvicorn main:app --host 0.0.0.0 --port 8000
209
+ port: 8000
210
+ readiness:
211
+ path: /
212
+ `,
213
+ },
214
+ { path: "requirements.txt", content: "fastapi\nuvicorn[standard]\n" },
215
+ {
216
+ path: "main.py",
217
+ content: `from fastapi import FastAPI
218
+
219
+ app = FastAPI()
220
+
221
+ @app.get("/")
222
+ def read_root():
223
+ return {"ok": True, "app": "miosa-fastapi"}
224
+ `,
225
+ },
226
+ ],
227
+ next: ["miosa sandbox deploy . --wait"],
228
+ },
229
+ static: {
230
+ id: "static",
231
+ name: "Static Site",
232
+ description: "Plain HTML/CSS/JS starter",
233
+ files: [
234
+ {
235
+ path: "miosa.app.yml",
236
+ content: `template: static-site
237
+ workdir: /workspace
238
+ dev: python3 -m http.server 5173 --bind 0.0.0.0
239
+ output: /workspace
240
+ port: 5173
241
+ readiness:
242
+ path: /
243
+ `,
244
+ },
245
+ { path: "index.html", content: "<h1>MIOSA static site</h1>\n" },
246
+ ],
247
+ next: ["miosa sandbox deploy . --wait"],
248
+ },
249
+ };
250
+ export function register(program) {
251
+ program
252
+ .command("new <template> [dir]")
253
+ .description("Create a MIOSA app starter with miosa.app.yml defaults")
254
+ .option("--force", "Write into a non-empty directory")
255
+ .option("--json", "Output as JSON")
256
+ .action((template, dir = template, opts) => {
257
+ const starter = STARTERS[template];
258
+ if (!starter) {
259
+ throw new UserError(`Unknown app starter: ${template}`, `Use one of: ${Object.keys(STARTERS).join(", ")}`);
260
+ }
261
+ const target = path.resolve(dir);
262
+ if (fs.existsSync(target) && fs.readdirSync(target).length > 0 && !opts.force) {
263
+ throw new UserError(`Directory is not empty: ${target}`, "Pass --force to write starter files anyway.");
264
+ }
265
+ for (const file of starter.files) {
266
+ const fullPath = path.join(target, file.path);
267
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
268
+ if (fs.existsSync(fullPath) && !opts.force) {
269
+ throw new UserError(`File already exists: ${fullPath}`);
270
+ }
271
+ fs.writeFileSync(fullPath, file.content);
272
+ }
273
+ const result = {
274
+ ok: true,
275
+ data: {
276
+ template: starter.id,
277
+ name: starter.name,
278
+ dir: target,
279
+ files: starter.files.map((file) => file.path),
280
+ next: starter.next,
281
+ },
282
+ };
283
+ if (isJsonMode(opts)) {
284
+ console.log(JSON.stringify(result, null, 2));
285
+ return;
286
+ }
287
+ console.log();
288
+ console.log(chalk.green(`Created ${starter.name}`));
289
+ console.log(chalk.dim(target));
290
+ console.log();
291
+ for (const cmd of starter.next)
292
+ console.log(` ${chalk.cyan(cmd)}`);
293
+ console.log();
294
+ });
295
+ }
296
+ //# sourceMappingURL=new.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"new.js","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwCzC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2zD/C"}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgDzC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA67D/C"}
@@ -7,6 +7,7 @@ import * as http from "node:http";
7
7
  import * as https from "node:https";
8
8
  import chalk from "chalk";
9
9
  import WebSocket from "ws";
10
+ import { loadAppManifest, manifestPort, manifestProbePath, manifestStartCommand, parseAppManifest, } from "../app-manifest.js";
10
11
  import { detectFramework } from "../framework-detector.js";
11
12
  import { addDataOption, client, apiPath, deleteAndPrint, enc, getAndPrint, postAndPrint, printValue, runAction, unwrap, } from "./enterprise-util.js";
12
13
  import { loadConfig } from "../config.js";
@@ -491,9 +492,12 @@ export function register(program) {
491
492
  .option("--start <command>", "Start command to run inside /workspace")
492
493
  .option("--install-command <command>", "Install command to run before start")
493
494
  .option("--no-install", "Skip automatic dependency install")
495
+ .option("--source <source>", "Source: git:https://... or tarball:https://... for repo-backed preview deploy")
496
+ .option("--revision <revision>", "Git revision/branch for --source git:...")
497
+ .option("--depth <n>", "Git clone depth for --source git:...", parseIntegerOption)
494
498
  .option("--wait", "Wait until the public preview returns a good HTTP status")
495
499
  .option("--timeout <duration>", "Wait timeout, e.g. 180s or 3m", parseDurationSec, 180)
496
- .option("--probe-path <path>", "HTTP path to probe", "/")
500
+ .option("--probe-path <path>", "HTTP path to probe")
497
501
  .option("--json", "Output as JSON")
498
502
  .action((localDir = ".", opts) => runAction(async () => {
499
503
  const result = await deploySandbox(localDir, opts);
@@ -723,6 +727,91 @@ export function register(program) {
723
727
  const lines = Array.isArray(result["lines"]) ? result["lines"] : [];
724
728
  process.stdout.write(lines.join("\n") + (lines.length > 0 ? "\n" : ""));
725
729
  }));
730
+ const env = sandbox
731
+ .command("env")
732
+ .description("Manage encrypted environment variables for a sandbox");
733
+ env
734
+ .command("list <sandbox-id>")
735
+ .description("List sandbox env var names and masked previews")
736
+ .option("--json", "Output as JSON")
737
+ .action((id, opts) => runAction(async () => {
738
+ const result = unwrap(await client().apiGet(apiPath(`/sandboxes/${enc(id)}/env`)));
739
+ if (isJsonMode(opts)) {
740
+ console.log(JSON.stringify(result, null, 2));
741
+ return;
742
+ }
743
+ const rows = Array.isArray(result) ? result : [];
744
+ if (rows.length === 0) {
745
+ console.log(chalk.dim("No sandbox env vars."));
746
+ return;
747
+ }
748
+ renderTable(rows, [
749
+ { header: "NAME", key: "name", width: 32 },
750
+ { header: "VALUE", key: "preview", width: 24 },
751
+ { header: "UPDATED", key: "updated_at", width: 28 },
752
+ ]);
753
+ }));
754
+ env
755
+ .command("set <sandbox-id> <pairs...>")
756
+ .description("Set encrypted sandbox env vars as KEY=VALUE")
757
+ .option("--json", "Output as JSON")
758
+ .action((id, pairs, opts) => runAction(async () => {
759
+ const vars = Object.entries(parseEnvPairs(pairs)).map(([key, value]) => ({
760
+ key,
761
+ value,
762
+ }));
763
+ const result = unwrap(await client().apiPut(apiPath(`/sandboxes/${enc(id)}/env`), {
764
+ vars,
765
+ }));
766
+ if (isJsonMode(opts)) {
767
+ console.log(JSON.stringify(result, null, 2));
768
+ return;
769
+ }
770
+ console.log(chalk.green(`Set ${vars.length} sandbox env var(s).`));
771
+ }));
772
+ env
773
+ .command("delete <sandbox-id> <key>")
774
+ .alias("unset")
775
+ .description("Delete an encrypted sandbox env var")
776
+ .option("--json", "Output as JSON")
777
+ .action((id, key, opts) => runAction(async () => {
778
+ const result = unwrap(await client().apiDelete(apiPath(`/sandboxes/${enc(id)}/env/${enc(key)}`)));
779
+ if (isJsonMode(opts)) {
780
+ console.log(JSON.stringify(result, null, 2));
781
+ return;
782
+ }
783
+ console.log(chalk.green(`Deleted ${key}.`));
784
+ }));
785
+ env
786
+ .command("sync <sandbox-id>")
787
+ .description("Sync encrypted sandbox env vars into the running VM")
788
+ .option("--json", "Output as JSON")
789
+ .action((id, opts) => runAction(async () => {
790
+ const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/env/sync`), {}));
791
+ if (isJsonMode(opts)) {
792
+ console.log(JSON.stringify(result, null, 2));
793
+ return;
794
+ }
795
+ const status = result && typeof result === "object" && "status" in result
796
+ ? String(result["status"])
797
+ : "ok";
798
+ console.log(chalk.green(`Sandbox env sync ${status}.`));
799
+ }));
800
+ const sandboxDb = sandbox
801
+ .command("db")
802
+ .description("Attach managed databases to sandboxes");
803
+ sandboxDb
804
+ .command("attach <sandbox-id> <database-id>")
805
+ .description("Attach a managed database and persist DATABASE_URL env vars")
806
+ .option("--json", "Output as JSON")
807
+ .action((id, databaseId, opts) => runAction(async () => {
808
+ const result = unwrap(await client().apiPost(apiPath(`/sandboxes/${enc(id)}/database`), { database_id: databaseId }));
809
+ if (isJsonMode(opts)) {
810
+ console.log(JSON.stringify(result, null, 2));
811
+ return;
812
+ }
813
+ console.log(chalk.green(`Attached database ${databaseId} to sandbox ${id}.`));
814
+ }));
726
815
  sandbox
727
816
  .command("doctor <sandbox-id>")
728
817
  .description("Diagnose sandbox app readiness across sandbox state, internal HTTP, public route, and TLS/edge reachability")
@@ -1464,42 +1553,66 @@ function validateServiceName(name) {
1464
1553
  }
1465
1554
  async function deploySandbox(localDir, opts) {
1466
1555
  const sourceDir = path.resolve(localDir);
1467
- if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
1556
+ const sourceBacked = !!opts.source;
1557
+ if (!sourceBacked && (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory())) {
1468
1558
  throw new UserError(`Local directory not found: ${sourceDir}`);
1469
1559
  }
1470
1560
  const c = client();
1471
- const detection = detectFramework(sourceDir);
1472
- const port = opts.port ?? opts.publishPort ?? detection?.port ?? 5173;
1473
- const start = opts.start ?? defaultStartCommand(detection?.framework, port);
1474
- const installCommand = opts.install === false
1475
- ? null
1476
- : (opts.installCommand ?? defaultInstallCommand(sourceDir));
1561
+ let appManifest = sourceBacked ? null : loadAppManifest(sourceDir)?.manifest ?? null;
1562
+ const detection = sourceBacked ? null : detectFramework(sourceDir);
1563
+ const port = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? detection?.port ?? 5173;
1564
+ const probePath = opts.probePath ?? manifestProbePath(appManifest) ?? "/";
1565
+ let remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
1566
+ const start = opts.start ??
1567
+ manifestStartCommand(appManifest) ??
1568
+ defaultStartCommand(detection?.framework ?? appManifest?.framework, port);
1477
1569
  const sandboxId = opts.sandbox ??
1478
1570
  (deployStep(opts, "Creating sandbox"),
1479
- await createSandboxForDeploy(c, opts.template ?? "miosa-sandbox", opts.name));
1571
+ await createSandboxForDeploy(c, opts.template ?? appManifest?.template ?? "miosa-sandbox", opts.name, {
1572
+ source: opts.source,
1573
+ revision: opts.revision,
1574
+ depth: opts.depth,
1575
+ }));
1480
1576
  deployStep(opts, "Waiting for sandbox");
1481
1577
  await waitForSandboxRunning(c, sandboxId, Math.min(opts.timeout, 120));
1482
- deployStep(opts, "Uploading files");
1483
- const archivePath = createDeployArchive(sourceDir);
1484
- const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
1485
- try {
1486
- await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
1578
+ if (sourceBacked) {
1579
+ deployStep(opts, "Waiting for source import");
1580
+ appManifest = await readRemoteAppManifest(c, sandboxId, opts.timeout);
1581
+ remoteWorkdir = normalizeRemoteWorkdir(appManifest?.workdir ?? "/workspace");
1487
1582
  }
1488
- finally {
1489
- fs.rmSync(archivePath, { force: true });
1583
+ else {
1584
+ deployStep(opts, "Uploading files");
1585
+ const archivePath = createDeployArchive(sourceDir);
1586
+ const remoteArchive = `/tmp/miosa-deploy-${Date.now()}.tgz`;
1587
+ try {
1588
+ await uploadFileToSandbox(c, sandboxId, archivePath, remoteArchive);
1589
+ deployStep(opts, "Extracting workspace");
1590
+ await execSandbox(c, sandboxId, `mkdir -p ${shellQuote(remoteWorkdir)} && tar -xzf ${shellQuote(remoteArchive)} -C ${shellQuote(remoteWorkdir)}`, "/");
1591
+ }
1592
+ finally {
1593
+ fs.rmSync(archivePath, { force: true });
1594
+ }
1490
1595
  }
1491
- deployStep(opts, "Extracting workspace");
1492
- await execSandbox(c, sandboxId, `mkdir -p /workspace && tar -xzf ${shellQuote(remoteArchive)} -C /workspace`, "/");
1596
+ const resolvedPort = opts.port ?? opts.publishPort ?? manifestPort(appManifest) ?? port;
1597
+ const resolvedProbePath = opts.probePath ?? manifestProbePath(appManifest) ?? probePath;
1598
+ const resolvedStart = opts.start ??
1599
+ manifestStartCommand(appManifest) ??
1600
+ start;
1601
+ const installCommand = opts.install === false
1602
+ ? null
1603
+ : (opts.installCommand ??
1604
+ (appManifest?.install === false ? null : appManifest?.install) ??
1605
+ (sourceBacked ? "npm install" : defaultInstallCommand(sourceDir)));
1493
1606
  if (installCommand) {
1494
1607
  deployStep(opts, `Installing dependencies: ${installCommand}`);
1495
- await execSandbox(c, sandboxId, installCommand, "/workspace", opts.timeout);
1608
+ await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
1496
1609
  }
1497
- deployStep(opts, `Starting app on port ${port}`);
1498
- await execSandbox(c, sandboxId, `fuser -k ${port}/tcp >/dev/null 2>&1 || true; nohup sh -lc ${shellQuote(start)} > ${shellQuote(`/tmp/miosa-app-${port}.log`)} 2>&1 & echo $!`, "/workspace");
1610
+ deployStep(opts, `Starting app on port ${resolvedPort}`);
1611
+ await execSandbox(c, sandboxId, `fuser -k ${resolvedPort}/tcp >/dev/null 2>&1 || true; nohup sh -lc ${shellQuote(resolvedStart)} > ${shellQuote(`/tmp/miosa-app-${resolvedPort}.log`)} 2>&1 & echo $!`, remoteWorkdir);
1499
1612
  deployStep(opts, "Checking internal app readiness");
1500
- const internal = await waitForInternalHttp(c, sandboxId, port, opts.probePath, Math.min(opts.timeout, 60));
1613
+ const internal = await waitForInternalHttp(c, sandboxId, resolvedPort, resolvedProbePath, Math.min(opts.timeout, 60));
1501
1614
  deployStep(opts, "Creating public preview route");
1502
- const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port, title: "app preview" });
1615
+ const exposed = await c.apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/expose`), { port: resolvedPort, title: "app preview" });
1503
1616
  const previewUrl = extractUrl(unwrap(exposed));
1504
1617
  if (!previewUrl) {
1505
1618
  throw new UserError("Sandbox expose did not return a preview URL.");
@@ -1507,11 +1620,11 @@ async function deploySandbox(localDir, opts) {
1507
1620
  if (opts.wait)
1508
1621
  deployStep(opts, "Checking public preview readiness");
1509
1622
  const edge = opts.wait
1510
- ? await waitForPublicPreview(previewUrl, opts.probePath, opts.timeout)
1623
+ ? await waitForPublicPreview(previewUrl, resolvedProbePath, opts.timeout)
1511
1624
  : { ok: false, status: null };
1512
1625
  return {
1513
1626
  sandbox_id: sandboxId,
1514
- port,
1627
+ port: resolvedPort,
1515
1628
  preview_url: previewUrl,
1516
1629
  preview_ready: edge.ok,
1517
1630
  internal_status: internal.status,
@@ -1688,16 +1801,43 @@ function renderDoctorReport(report) {
1688
1801
  console.log();
1689
1802
  }
1690
1803
  }
1691
- async function createSandboxForDeploy(c, template, name) {
1804
+ async function createSandboxForDeploy(c, template, name, source) {
1692
1805
  const body = { template_id: template };
1693
1806
  if (name)
1694
1807
  body["name"] = name;
1808
+ if (source?.source)
1809
+ body["source"] = source.source;
1810
+ if (source?.revision)
1811
+ body["revision"] = source.revision;
1812
+ if (source?.depth != null)
1813
+ body["depth"] = source.depth;
1695
1814
  const created = unwrap(await c.apiPost(apiPath("/sandboxes"), body));
1696
1815
  const sandboxId = typeof created["id"] === "string" ? created["id"] : "";
1697
1816
  if (!sandboxId)
1698
1817
  throw new UserError("Sandbox create did not return an id.");
1699
1818
  return sandboxId;
1700
1819
  }
1820
+ async function readRemoteAppManifest(c, sandboxId, timeoutSec) {
1821
+ const deadline = Date.now() + Math.min(timeoutSec, 120) * 1000;
1822
+ while (Date.now() < deadline) {
1823
+ for (const filename of ["miosa.app.yml", "miosa.app.yaml", "miosa.app.json"]) {
1824
+ const result = await c
1825
+ .apiPost(apiPath(`/sandboxes/${enc(sandboxId)}/exec`), {
1826
+ command: `test -f /workspace/${filename} && cat /workspace/${filename}`,
1827
+ cwd: "/workspace",
1828
+ timeout: 10,
1829
+ })
1830
+ .then(unwrap)
1831
+ .catch(() => null);
1832
+ const row = asRecord(result);
1833
+ if (Number(row?.["exit_code"] ?? 1) === 0 && typeof row?.["stdout"] === "string") {
1834
+ return parseAppManifest(filename, row["stdout"]);
1835
+ }
1836
+ }
1837
+ await sleep(1500);
1838
+ }
1839
+ return null;
1840
+ }
1701
1841
  async function waitForSandboxRunning(c, sandboxId, timeoutSec) {
1702
1842
  const deadline = Date.now() + timeoutSec * 1000;
1703
1843
  while (Date.now() < deadline) {
@@ -1879,6 +2019,13 @@ function defaultStartCommand(framework, port) {
1879
2019
  return `python3 -m http.server ${port} --bind 0.0.0.0`;
1880
2020
  return `npm run dev -- --host 0.0.0.0 --port ${port}`;
1881
2021
  }
2022
+ function normalizeRemoteWorkdir(value) {
2023
+ if (!value || value === ".")
2024
+ return "/workspace";
2025
+ if (!value.startsWith("/"))
2026
+ return `/workspace/${value.replace(/^\.\//, "")}`;
2027
+ return value;
2028
+ }
1882
2029
  function extractUrl(value) {
1883
2030
  if (!value || typeof value !== "object" || Array.isArray(value))
1884
2031
  return null;