@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.
- package/dist/app-manifest.d.ts +31 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +122 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/bin/miosa.js +1 -0
- package/dist/bin/miosa.js.map +1 -1
- package/dist/commands/capabilities.d.ts.map +1 -1
- package/dist/commands/capabilities.js +12 -0
- package/dist/commands/capabilities.js.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +4 -34
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/new.d.ts +3 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +296 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/sandbox.d.ts.map +1 -1
- package/dist/commands/sandbox.js +173 -26
- package/dist/commands/sandbox.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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"}
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
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,
|
|
1608
|
+
await execSandbox(c, sandboxId, installCommand, remoteWorkdir, opts.timeout);
|
|
1496
1609
|
}
|
|
1497
|
-
deployStep(opts, `Starting app on port ${
|
|
1498
|
-
await execSandbox(c, sandboxId, `fuser -k ${
|
|
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,
|
|
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,
|
|
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;
|