instavm 0.15.0 → 0.16.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/README.md +49 -2
- package/dist/cli.js +2449 -228
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -35,8 +35,9 @@ __export(cli_exports, {
|
|
|
35
35
|
runCli: () => runCli
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(cli_exports);
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var import_fs5 = __toESM(require("fs"));
|
|
39
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
40
|
+
var import_path14 = __toESM(require("path"));
|
|
40
41
|
var import_child_process = require("child_process");
|
|
41
42
|
var import_commander = require("commander");
|
|
42
43
|
|
|
@@ -179,7 +180,7 @@ var HTTPClient = class {
|
|
|
179
180
|
timeout: config.timeout,
|
|
180
181
|
headers: {
|
|
181
182
|
"Content-Type": "application/json",
|
|
182
|
-
"User-Agent": "instavm-js-sdk/0.
|
|
183
|
+
"User-Agent": "instavm-js-sdk/0.16.0"
|
|
183
184
|
}
|
|
184
185
|
});
|
|
185
186
|
this.setupInterceptors();
|
|
@@ -958,8 +959,8 @@ function encodePathSegment(value) {
|
|
|
958
959
|
}
|
|
959
960
|
return encodeURIComponent(segment);
|
|
960
961
|
}
|
|
961
|
-
function encodePathSegments(
|
|
962
|
-
const segments =
|
|
962
|
+
function encodePathSegments(path6) {
|
|
963
|
+
const segments = path6.replace(/^\/+/, "").split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
963
964
|
if (segments.some((segment) => segment === "..")) {
|
|
964
965
|
throw new Error("Path traversal not allowed in proxy path");
|
|
965
966
|
}
|
|
@@ -1262,10 +1263,10 @@ var ComputerUseManager = class {
|
|
|
1262
1263
|
{ "X-API-Key": this.httpClient.apiKey }
|
|
1263
1264
|
);
|
|
1264
1265
|
}
|
|
1265
|
-
async proxy(sessionId,
|
|
1266
|
+
async proxy(sessionId, path6, method = "GET", options = {}) {
|
|
1266
1267
|
this.ensureCloud("Computer-use proxy");
|
|
1267
1268
|
const safeSessionId = encodePathSegment(sessionId);
|
|
1268
|
-
const cleanPath = encodePathSegments(
|
|
1269
|
+
const cleanPath = encodePathSegments(path6);
|
|
1269
1270
|
return this.httpClient.request({
|
|
1270
1271
|
method,
|
|
1271
1272
|
url: `/v1/computeruse/${safeSessionId}/${cleanPath}`,
|
|
@@ -1274,26 +1275,26 @@ var ComputerUseManager = class {
|
|
|
1274
1275
|
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1275
1276
|
});
|
|
1276
1277
|
}
|
|
1277
|
-
async get(sessionId,
|
|
1278
|
-
return this.proxy(sessionId,
|
|
1278
|
+
async get(sessionId, path6, params) {
|
|
1279
|
+
return this.proxy(sessionId, path6, "GET", { params });
|
|
1279
1280
|
}
|
|
1280
|
-
async post(sessionId,
|
|
1281
|
-
return this.proxy(sessionId,
|
|
1281
|
+
async post(sessionId, path6, body) {
|
|
1282
|
+
return this.proxy(sessionId, path6, "POST", { body });
|
|
1282
1283
|
}
|
|
1283
|
-
async put(sessionId,
|
|
1284
|
-
return this.proxy(sessionId,
|
|
1284
|
+
async put(sessionId, path6, body) {
|
|
1285
|
+
return this.proxy(sessionId, path6, "PUT", { body });
|
|
1285
1286
|
}
|
|
1286
|
-
async patch(sessionId,
|
|
1287
|
-
return this.proxy(sessionId,
|
|
1287
|
+
async patch(sessionId, path6, body) {
|
|
1288
|
+
return this.proxy(sessionId, path6, "PATCH", { body });
|
|
1288
1289
|
}
|
|
1289
|
-
async delete(sessionId,
|
|
1290
|
-
return this.proxy(sessionId,
|
|
1290
|
+
async delete(sessionId, path6, params) {
|
|
1291
|
+
return this.proxy(sessionId, path6, "DELETE", { params });
|
|
1291
1292
|
}
|
|
1292
|
-
async options(sessionId,
|
|
1293
|
-
return this.proxy(sessionId,
|
|
1293
|
+
async options(sessionId, path6, params) {
|
|
1294
|
+
return this.proxy(sessionId, path6, "OPTIONS", { params });
|
|
1294
1295
|
}
|
|
1295
|
-
async head(sessionId,
|
|
1296
|
-
return this.proxy(sessionId,
|
|
1296
|
+
async head(sessionId, path6, params) {
|
|
1297
|
+
return this.proxy(sessionId, path6, "HEAD", { params });
|
|
1297
1298
|
}
|
|
1298
1299
|
/**
|
|
1299
1300
|
* Get the VNC WebSocket URL for a computer-use session.
|
|
@@ -1624,12 +1625,12 @@ var VolumesManager = class {
|
|
|
1624
1625
|
formData
|
|
1625
1626
|
);
|
|
1626
1627
|
}
|
|
1627
|
-
async downloadFile(volumeId,
|
|
1628
|
+
async downloadFile(volumeId, path6) {
|
|
1628
1629
|
this.ensureCloud("Volume file download");
|
|
1629
1630
|
const safeVolumeId = encodePathSegment(volumeId);
|
|
1630
1631
|
const response = await this.httpClient.post(
|
|
1631
1632
|
`/v1/volumes/${safeVolumeId}/files/download`,
|
|
1632
|
-
{ path:
|
|
1633
|
+
{ path: path6 },
|
|
1633
1634
|
{ "X-API-Key": this.httpClient.apiKey }
|
|
1634
1635
|
);
|
|
1635
1636
|
return {
|
|
@@ -2050,7 +2051,7 @@ var InstaVM = class {
|
|
|
2050
2051
|
async getCurrentUser() {
|
|
2051
2052
|
this.ensureNotLocal("User profile lookup");
|
|
2052
2053
|
return this.httpClient.get(
|
|
2053
|
-
"/v1/me",
|
|
2054
|
+
"/v1/users/me",
|
|
2054
2055
|
void 0,
|
|
2055
2056
|
{ "X-API-Key": this.httpClient.apiKey }
|
|
2056
2057
|
);
|
|
@@ -2291,6 +2292,14 @@ var InstaVM = class {
|
|
|
2291
2292
|
}
|
|
2292
2293
|
};
|
|
2293
2294
|
|
|
2295
|
+
// src/cli/cookbook.ts
|
|
2296
|
+
var import_fs3 = __toESM(require("fs"));
|
|
2297
|
+
var import_os2 = __toESM(require("os"));
|
|
2298
|
+
var import_path12 = __toESM(require("path"));
|
|
2299
|
+
var import_crypto = __toESM(require("crypto"));
|
|
2300
|
+
var import_readline = __toESM(require("readline"));
|
|
2301
|
+
var import_yaml = require("yaml");
|
|
2302
|
+
|
|
2294
2303
|
// src/cli/config.ts
|
|
2295
2304
|
var import_fs2 = __toESM(require("fs"));
|
|
2296
2305
|
var import_os = __toESM(require("os"));
|
|
@@ -2527,225 +2536,2295 @@ function updateProfileSettings(options) {
|
|
|
2527
2536
|
return saveConfig(config, configPath);
|
|
2528
2537
|
}
|
|
2529
2538
|
|
|
2530
|
-
// src/cli.ts
|
|
2531
|
-
var
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
clientFactory: (apiKey, options) => new InstaVM(apiKey, options),
|
|
2538
|
-
promptSecret,
|
|
2539
|
-
readStdin,
|
|
2540
|
-
writeFile: (outputPath, content) => {
|
|
2541
|
-
import_fs3.default.writeFileSync(outputPath, content);
|
|
2539
|
+
// src/cli/cookbook.ts
|
|
2540
|
+
var CookbookError = class extends Error {
|
|
2541
|
+
};
|
|
2542
|
+
var CookbookDeployError = class extends CookbookError {
|
|
2543
|
+
constructor(message, payload) {
|
|
2544
|
+
super(message);
|
|
2545
|
+
this.payload = payload;
|
|
2542
2546
|
}
|
|
2543
2547
|
};
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2548
|
+
var DEFAULT_COOKBOOK_REPO_URL = "https://github.com/instavm/cookbooks.git";
|
|
2549
|
+
var DEFAULT_COOKBOOK_REF = "main";
|
|
2550
|
+
var DEFAULT_REMOTE_ROOT = ".instavm-cookbooks";
|
|
2551
|
+
var DEFAULT_SOURCE_EXCLUDES = [
|
|
2552
|
+
".git",
|
|
2553
|
+
"__pycache__",
|
|
2554
|
+
".pytest_cache",
|
|
2555
|
+
".venv",
|
|
2556
|
+
"venv",
|
|
2557
|
+
"node_modules",
|
|
2558
|
+
".next",
|
|
2559
|
+
"dist",
|
|
2560
|
+
"build",
|
|
2561
|
+
".DS_Store"
|
|
2562
|
+
];
|
|
2563
|
+
function getCookbookCacheDir(homeDir = import_os2.default.homedir()) {
|
|
2564
|
+
return import_path12.default.join(getConfigDir(homeDir), "cookbooks");
|
|
2547
2565
|
}
|
|
2548
|
-
function
|
|
2549
|
-
|
|
2550
|
-
deps.stdout.write(`${line}
|
|
2551
|
-
`);
|
|
2552
|
-
}
|
|
2566
|
+
function getCookbookRepoDir(homeDir = import_os2.default.homedir()) {
|
|
2567
|
+
return import_path12.default.join(getCookbookCacheDir(homeDir), "repo");
|
|
2553
2568
|
}
|
|
2554
|
-
function
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2569
|
+
function resolveCookbookRoot(deps, progress, homeDir = import_os2.default.homedir()) {
|
|
2570
|
+
const override = process.env.INSTAVM_COOKBOOKS_DIR;
|
|
2571
|
+
if (override) {
|
|
2572
|
+
const resolved = import_path12.default.resolve(override);
|
|
2573
|
+
if (!import_fs3.default.existsSync(resolved)) {
|
|
2574
|
+
throw new CookbookError(`Cookbook directory override does not exist: ${resolved}`);
|
|
2575
|
+
}
|
|
2576
|
+
return resolved;
|
|
2559
2577
|
}
|
|
2560
|
-
return
|
|
2578
|
+
return syncCookbookRepo(deps, progress, homeDir);
|
|
2561
2579
|
}
|
|
2562
|
-
function
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2580
|
+
function syncCookbookRepo(deps, progress, homeDir = import_os2.default.homedir()) {
|
|
2581
|
+
const repoUrl = process.env.INSTAVM_COOKBOOKS_REPO || DEFAULT_COOKBOOK_REPO_URL;
|
|
2582
|
+
const repoRef = process.env.INSTAVM_COOKBOOKS_REF || DEFAULT_COOKBOOK_REF;
|
|
2583
|
+
const repoDir = getCookbookRepoDir(homeDir);
|
|
2584
|
+
import_fs3.default.mkdirSync(import_path12.default.dirname(repoDir), { recursive: true });
|
|
2585
|
+
if (progress) {
|
|
2586
|
+
progress(`Syncing cookbook catalog from ${repoUrl}`);
|
|
2587
|
+
}
|
|
2588
|
+
if (!import_fs3.default.existsSync(import_path12.default.join(repoDir, ".git"))) {
|
|
2589
|
+
runCommand(
|
|
2590
|
+
deps,
|
|
2591
|
+
"git",
|
|
2592
|
+
["clone", "--depth", "1", "--branch", repoRef, repoUrl, repoDir],
|
|
2593
|
+
"Failed to clone the cookbook catalog."
|
|
2594
|
+
);
|
|
2595
|
+
return repoDir;
|
|
2567
2596
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2597
|
+
runCommand(deps, "git", ["-C", repoDir, "fetch", "--depth", "1", "origin", repoRef], "Failed to update the cookbook catalog.");
|
|
2598
|
+
runCommand(deps, "git", ["-C", repoDir, "checkout", repoRef], "Failed to check out the cookbook catalog branch.");
|
|
2599
|
+
runCommand(deps, "git", ["-C", repoDir, "reset", "--hard", "FETCH_HEAD"], "Failed to reset the cookbook catalog to the latest remote state.");
|
|
2600
|
+
return repoDir;
|
|
2601
|
+
}
|
|
2602
|
+
function discoverCookbooks(root) {
|
|
2603
|
+
if (!import_fs3.default.existsSync(root)) {
|
|
2604
|
+
return [];
|
|
2570
2605
|
}
|
|
2571
|
-
return
|
|
2606
|
+
return import_fs3.default.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && import_fs3.default.existsSync(import_path12.default.join(root, entry.name, "instavm.yaml"))).sort((a, b) => a.name.localeCompare(b.name)).map((entry) => loadManifest(import_path12.default.join(root, entry.name, "instavm.yaml")));
|
|
2572
2607
|
}
|
|
2573
|
-
function
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2608
|
+
function loadManifest(manifestPath) {
|
|
2609
|
+
let raw;
|
|
2610
|
+
try {
|
|
2611
|
+
raw = (0, import_yaml.parse)(import_fs3.default.readFileSync(manifestPath, "utf8"));
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
throw new CookbookError(`Invalid cookbook manifest: ${manifestPath}`);
|
|
2577
2614
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
["gb", 1024 ** 3],
|
|
2581
|
-
["mb", 1024 ** 2],
|
|
2582
|
-
["kb", 1024],
|
|
2583
|
-
["b", 1]
|
|
2584
|
-
];
|
|
2585
|
-
for (const [suffix, multiplier] of units) {
|
|
2586
|
-
if (raw.endsWith(suffix) && raw !== suffix) {
|
|
2587
|
-
const number = raw.slice(0, -suffix.length).trim();
|
|
2588
|
-
return Math.floor(Number.parseFloat(number) * multiplier);
|
|
2589
|
-
}
|
|
2615
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2616
|
+
throw new CookbookError(`Manifest root must be an object: ${manifestPath}`);
|
|
2590
2617
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
let amount = Number(value || 0);
|
|
2595
|
-
for (const unit of ["B", "KB", "MB", "GB", "TB"]) {
|
|
2596
|
-
if (amount < 1024 || unit === "TB") {
|
|
2597
|
-
if (unit === "B") {
|
|
2598
|
-
return `${Math.trunc(amount)}${unit}`;
|
|
2599
|
-
}
|
|
2600
|
-
return `${amount.toFixed(1)}${unit}`;
|
|
2618
|
+
const requireObject = (value, field) => {
|
|
2619
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2620
|
+
throw new CookbookError(`Manifest field \`${field}\` must be an object.`);
|
|
2601
2621
|
}
|
|
2602
|
-
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
const pairs = {};
|
|
2608
|
-
for (const entry of values || []) {
|
|
2609
|
-
const separatorIndex = entry.indexOf("=");
|
|
2610
|
-
if (separatorIndex < 1) {
|
|
2611
|
-
throw new Error(`Expected KEY=VALUE, got: ${entry}`);
|
|
2622
|
+
return value;
|
|
2623
|
+
};
|
|
2624
|
+
const requireString = (value, field) => {
|
|
2625
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2626
|
+
throw new CookbookError(`Manifest field \`${field}\` must be a non-empty string.`);
|
|
2612
2627
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2628
|
+
return value.trim();
|
|
2629
|
+
};
|
|
2630
|
+
const requireInt = (value, field) => {
|
|
2631
|
+
if (!Number.isInteger(value)) {
|
|
2632
|
+
throw new CookbookError(`Manifest field \`${field}\` must be an integer.`);
|
|
2617
2633
|
}
|
|
2618
|
-
|
|
2634
|
+
return Number(value);
|
|
2635
|
+
};
|
|
2636
|
+
const requiredFields = [
|
|
2637
|
+
"schema_version",
|
|
2638
|
+
"slug",
|
|
2639
|
+
"title",
|
|
2640
|
+
"version",
|
|
2641
|
+
"summary",
|
|
2642
|
+
"category",
|
|
2643
|
+
"runtime",
|
|
2644
|
+
"deploy",
|
|
2645
|
+
"vm",
|
|
2646
|
+
"app",
|
|
2647
|
+
"run",
|
|
2648
|
+
"secrets",
|
|
2649
|
+
"post_deploy_notes"
|
|
2650
|
+
];
|
|
2651
|
+
const missing = requiredFields.filter((field) => !(field in raw));
|
|
2652
|
+
if (missing.length > 0) {
|
|
2653
|
+
throw new CookbookError(`Manifest is missing required fields: ${missing.join(", ")}`);
|
|
2654
|
+
}
|
|
2655
|
+
const deploy = requireObject(raw.deploy, "deploy");
|
|
2656
|
+
const vm = requireObject(raw.vm, "vm");
|
|
2657
|
+
const app = requireObject(raw.app, "app");
|
|
2658
|
+
const run = requireObject(raw.run, "run");
|
|
2659
|
+
const secrets = Array.isArray(raw.secrets) ? raw.secrets : [];
|
|
2660
|
+
const manifest = {
|
|
2661
|
+
path: import_path12.default.dirname(manifestPath),
|
|
2662
|
+
schema_version: requireInt(raw.schema_version, "schema_version"),
|
|
2663
|
+
slug: requireString(raw.slug, "slug"),
|
|
2664
|
+
title: requireString(raw.title, "title"),
|
|
2665
|
+
version: requireString(raw.version, "version"),
|
|
2666
|
+
summary: requireString(raw.summary, "summary"),
|
|
2667
|
+
category: requireString(raw.category, "category"),
|
|
2668
|
+
runtime: requireString(raw.runtime, "runtime"),
|
|
2669
|
+
deploy: {
|
|
2670
|
+
kind: requireString(deploy.kind, "deploy.kind")
|
|
2671
|
+
},
|
|
2672
|
+
vm: {
|
|
2673
|
+
memory_mb: requireInt(vm.memory_mb, "vm.memory_mb"),
|
|
2674
|
+
vcpu_count: requireInt(vm.vcpu_count, "vm.vcpu_count"),
|
|
2675
|
+
timeout_seconds: Number.isInteger(vm.timeout_seconds) ? Number(vm.timeout_seconds) : void 0,
|
|
2676
|
+
image_variant: typeof vm.image_variant === "string" ? vm.image_variant.trim() || void 0 : void 0
|
|
2677
|
+
},
|
|
2678
|
+
app: {
|
|
2679
|
+
port: requireInt(app.port, "app.port"),
|
|
2680
|
+
healthcheck_path: requireString(app.healthcheck_path, "app.healthcheck_path"),
|
|
2681
|
+
share_public_default: Boolean(app.share_public_default),
|
|
2682
|
+
readiness_timeout_seconds: requireInt(app.readiness_timeout_seconds, "app.readiness_timeout_seconds")
|
|
2683
|
+
},
|
|
2684
|
+
run: {
|
|
2685
|
+
workdir: requireString(run.workdir, "run.workdir"),
|
|
2686
|
+
start_command: requireString(run.start_command, "run.start_command"),
|
|
2687
|
+
logs_hint: typeof run.logs_hint === "string" ? run.logs_hint.trim() || void 0 : void 0
|
|
2688
|
+
},
|
|
2689
|
+
secrets: secrets.map((secret, index) => {
|
|
2690
|
+
const obj = requireObject(secret, `secrets[${index}]`);
|
|
2691
|
+
return {
|
|
2692
|
+
name: requireString(obj.name, `secrets[${index}].name`),
|
|
2693
|
+
required: obj.required !== false,
|
|
2694
|
+
prompt: requireString(obj.prompt, `secrets[${index}].prompt`),
|
|
2695
|
+
env_name: requireString(obj.env_name, `secrets[${index}].env_name`)
|
|
2696
|
+
};
|
|
2697
|
+
}),
|
|
2698
|
+
post_deploy_notes: typeof raw.post_deploy_notes === "string" ? [raw.post_deploy_notes.trim()].filter(Boolean) : Array.isArray(raw.post_deploy_notes) ? raw.post_deploy_notes.map((entry) => requireString(entry, "post_deploy_notes[]")) : [],
|
|
2699
|
+
artifact: raw.artifact ? {
|
|
2700
|
+
oci_image: requireString(raw.artifact.oci_image, "artifact.oci_image"),
|
|
2701
|
+
snapshot_name: requireString(raw.artifact.snapshot_name, "artifact.snapshot_name"),
|
|
2702
|
+
snapshot_visibility: requireString(raw.artifact.snapshot_visibility, "artifact.snapshot_visibility")
|
|
2703
|
+
} : void 0,
|
|
2704
|
+
build: raw.build ? {
|
|
2705
|
+
context: requireString(raw.build.context, "build.context"),
|
|
2706
|
+
dockerfile: requireString(raw.build.dockerfile, "build.dockerfile"),
|
|
2707
|
+
target: typeof raw.build.target === "string" ? raw.build.target.trim() || void 0 : void 0
|
|
2708
|
+
} : void 0,
|
|
2709
|
+
source: raw.source ? {
|
|
2710
|
+
include: Array.isArray(raw.source.include) ? raw.source.include.map((entry) => requireString(entry, "source.include[]")) : [],
|
|
2711
|
+
exclude: Array.isArray(raw.source.exclude) ? raw.source.exclude.map((entry) => requireString(entry, "source.exclude[]")) : [],
|
|
2712
|
+
setup_command: requireString(raw.source.setup_command, "source.setup_command")
|
|
2713
|
+
} : void 0
|
|
2714
|
+
};
|
|
2715
|
+
if (!["published_snapshot", "upload_and_run"].includes(manifest.deploy.kind)) {
|
|
2716
|
+
throw new CookbookError("Manifest field `deploy.kind` must be `published_snapshot` or `upload_and_run`.");
|
|
2619
2717
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
function parseVolumeSpec(spec) {
|
|
2623
|
-
const parts = spec.split(":");
|
|
2624
|
-
if (parts.length < 2) {
|
|
2625
|
-
throw new Error("volume mounts must use <volume_id>:<mount_path>[:rw|ro[:checkpoint_id]]");
|
|
2718
|
+
if (manifest.schema_version !== 1) {
|
|
2719
|
+
throw new CookbookError("Unsupported cookbook schema version. Expected `schema_version: 1`.");
|
|
2626
2720
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
let mode = "rw";
|
|
2630
|
-
let checkpointId = null;
|
|
2631
|
-
if (parts[2]?.trim()) {
|
|
2632
|
-
mode = parts[2].trim().toLowerCase();
|
|
2721
|
+
if (!manifest.app.healthcheck_path.startsWith("/")) {
|
|
2722
|
+
throw new CookbookError("Manifest field `app.healthcheck_path` must start with `/`.");
|
|
2633
2723
|
}
|
|
2634
|
-
if (
|
|
2635
|
-
|
|
2724
|
+
if (manifest.deploy.kind === "published_snapshot" && !manifest.artifact) {
|
|
2725
|
+
throw new CookbookError("Published snapshot cookbooks require `artifact` config.");
|
|
2636
2726
|
}
|
|
2637
|
-
if (
|
|
2638
|
-
throw new
|
|
2727
|
+
if (manifest.deploy.kind === "published_snapshot" && manifest.artifact?.snapshot_visibility !== "public_system") {
|
|
2728
|
+
throw new CookbookError("Manifest field `artifact.snapshot_visibility` must be `public_system`.");
|
|
2639
2729
|
}
|
|
2640
|
-
if (
|
|
2641
|
-
|
|
2730
|
+
if (manifest.deploy.kind === "published_snapshot" && manifest.build) {
|
|
2731
|
+
if (!import_fs3.default.existsSync(import_path12.default.join(import_path12.default.dirname(manifestPath), manifest.build.context))) {
|
|
2732
|
+
throw new CookbookError(`Build context does not exist: ${import_path12.default.join(import_path12.default.dirname(manifestPath), manifest.build.context)}`);
|
|
2733
|
+
}
|
|
2734
|
+
if (!import_fs3.default.existsSync(import_path12.default.join(import_path12.default.dirname(manifestPath), manifest.build.dockerfile))) {
|
|
2735
|
+
throw new CookbookError(`Dockerfile does not exist: ${import_path12.default.join(import_path12.default.dirname(manifestPath), manifest.build.dockerfile)}`);
|
|
2736
|
+
}
|
|
2642
2737
|
}
|
|
2643
|
-
if (
|
|
2644
|
-
throw new
|
|
2738
|
+
if (manifest.deploy.kind === "upload_and_run" && !manifest.source) {
|
|
2739
|
+
throw new CookbookError("Upload-and-run cookbooks require `source` config.");
|
|
2645
2740
|
}
|
|
2646
|
-
if (
|
|
2647
|
-
|
|
2741
|
+
if (manifest.deploy.kind === "upload_and_run" && manifest.source && manifest.source.include.length === 0) {
|
|
2742
|
+
throw new CookbookError("Manifest field `source.include` must contain at least one path or glob.");
|
|
2648
2743
|
}
|
|
2744
|
+
return manifest;
|
|
2745
|
+
}
|
|
2746
|
+
function manifestDisplayRecord(manifest) {
|
|
2649
2747
|
return {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2748
|
+
slug: manifest.slug,
|
|
2749
|
+
title: manifest.title,
|
|
2750
|
+
summary: manifest.summary,
|
|
2751
|
+
runtime: manifest.runtime,
|
|
2752
|
+
category: manifest.category,
|
|
2753
|
+
deploy_kind: manifest.deploy.kind,
|
|
2754
|
+
port: manifest.app.port,
|
|
2755
|
+
requires_secrets: manifest.secrets.length > 0,
|
|
2756
|
+
version: manifest.version
|
|
2654
2757
|
};
|
|
2655
2758
|
}
|
|
2656
|
-
function
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
const profile = getActiveProfile(settings.config);
|
|
2666
|
-
const storedKey = profile.auth?.api_key ? String(profile.auth.api_key) : void 0;
|
|
2667
|
-
return {
|
|
2668
|
-
config_path: settings.configPath,
|
|
2669
|
-
active_profile: settings.config.active_profile,
|
|
2670
|
-
api_key: {
|
|
2671
|
-
configured: Boolean(settings.apiKey),
|
|
2672
|
-
redacted: redactSecret(settings.apiKey),
|
|
2673
|
-
source: settings.apiKeySource,
|
|
2674
|
-
stored: Boolean(storedKey)
|
|
2675
|
-
},
|
|
2676
|
-
base_url: {
|
|
2677
|
-
value: settings.baseURL,
|
|
2678
|
-
source: settings.baseURLSource
|
|
2679
|
-
},
|
|
2680
|
-
ssh_host: {
|
|
2681
|
-
value: settings.sshHost,
|
|
2682
|
-
source: settings.sshHostSource
|
|
2759
|
+
function preflightTools(deps) {
|
|
2760
|
+
const found = {};
|
|
2761
|
+
const missing = [];
|
|
2762
|
+
for (const binary of ["git", "ssh", "scp", "tar"]) {
|
|
2763
|
+
const resolved = findExecutable(deps, binary);
|
|
2764
|
+
if (resolved) {
|
|
2765
|
+
found[binary] = resolved;
|
|
2766
|
+
} else {
|
|
2767
|
+
missing.push(binary);
|
|
2683
2768
|
}
|
|
2684
|
-
}
|
|
2769
|
+
}
|
|
2770
|
+
if (missing.length > 0) {
|
|
2771
|
+
throw new CookbookError(`Missing required local tools: ${missing.join(", ")}`);
|
|
2772
|
+
}
|
|
2773
|
+
return found;
|
|
2685
2774
|
}
|
|
2686
|
-
function
|
|
2687
|
-
const
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
`API key: ${payload.api_key.redacted || "not configured"} (${payload.api_key.source})`,
|
|
2691
|
-
`Base URL: ${payload.base_url.value} (${payload.base_url.source})`,
|
|
2692
|
-
`SSH host: ${payload.ssh_host.value} (${payload.ssh_host.source})`
|
|
2693
|
-
];
|
|
2694
|
-
if (payload.api_key.stored) {
|
|
2695
|
-
lines.push("Stored key: present");
|
|
2775
|
+
function scanPublicKeys(homeDir = import_os2.default.homedir()) {
|
|
2776
|
+
const sshDir = import_path12.default.join(homeDir, ".ssh");
|
|
2777
|
+
if (!import_fs3.default.existsSync(sshDir)) {
|
|
2778
|
+
return [];
|
|
2696
2779
|
}
|
|
2697
|
-
return
|
|
2780
|
+
return import_fs3.default.readdirSync(sshDir).filter((entry) => entry.endsWith(".pub")).map((entry) => import_path12.default.join(sshDir, entry)).filter((entry) => import_fs3.default.statSync(entry).isFile()).sort();
|
|
2698
2781
|
}
|
|
2699
|
-
function
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2782
|
+
function publicKeyFingerprint(publicKeyPath) {
|
|
2783
|
+
let content = "";
|
|
2784
|
+
try {
|
|
2785
|
+
content = import_fs3.default.readFileSync(publicKeyPath, "utf8").trim();
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
return void 0;
|
|
2788
|
+
}
|
|
2789
|
+
const parts = content.split(/\s+/);
|
|
2790
|
+
if (parts.length < 2) {
|
|
2791
|
+
return void 0;
|
|
2792
|
+
}
|
|
2793
|
+
try {
|
|
2794
|
+
const keyBlob = Buffer.from(parts[1], "base64");
|
|
2795
|
+
const digest = import_crypto.default.createHash("sha256").update(keyBlob).digest("base64").replace(/=+$/g, "");
|
|
2796
|
+
return `SHA256:${digest}`;
|
|
2797
|
+
} catch (error) {
|
|
2798
|
+
return void 0;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
function matchingLocalPublicKey(keys, candidates) {
|
|
2802
|
+
const fingerprints = new Set(
|
|
2803
|
+
keys.map((key) => String(key.fingerprint || "").trim()).filter(Boolean)
|
|
2804
|
+
);
|
|
2805
|
+
if (fingerprints.size === 0) {
|
|
2806
|
+
return void 0;
|
|
2807
|
+
}
|
|
2808
|
+
return candidates.find((candidate) => {
|
|
2809
|
+
const fingerprint = publicKeyFingerprint(candidate);
|
|
2810
|
+
return fingerprint ? fingerprints.has(fingerprint) : false;
|
|
2704
2811
|
});
|
|
2705
2812
|
}
|
|
2706
|
-
function
|
|
2707
|
-
const
|
|
2708
|
-
|
|
2709
|
-
|
|
2813
|
+
async function ensureAccountSshKey(client, deps, progress, homeDir = import_os2.default.homedir()) {
|
|
2814
|
+
const keys = await client.listSshKeys();
|
|
2815
|
+
const candidates = scanPublicKeys(homeDir);
|
|
2816
|
+
if (keys.length > 0) {
|
|
2817
|
+
const selectedPath = matchingLocalPublicKey(keys, candidates);
|
|
2818
|
+
if (progress && selectedPath) {
|
|
2819
|
+
progress(`Using local SSH identity from ${selectedPath}`);
|
|
2820
|
+
}
|
|
2821
|
+
return { configured: true, auto_added: false, keys, selected_path: selectedPath };
|
|
2710
2822
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2823
|
+
if (candidates.length === 0) {
|
|
2824
|
+
throw new CookbookError(
|
|
2825
|
+
"No SSH key is registered on your account, and no local public key was found in ~/.ssh. Generate one with `ssh-keygen -t ed25519` and rerun the command."
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (progress) {
|
|
2829
|
+
progress("No account SSH key found. Selecting a local public key to register.");
|
|
2830
|
+
}
|
|
2831
|
+
let selected = candidates[0];
|
|
2832
|
+
if (candidates.length > 1 && process.stdin.isTTY) {
|
|
2833
|
+
deps.stdout.write("Select a public key to add:\n");
|
|
2834
|
+
candidates.forEach((candidate, index) => {
|
|
2835
|
+
deps.stdout.write(` ${index + 1}. ${candidate}
|
|
2836
|
+
`);
|
|
2837
|
+
});
|
|
2838
|
+
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
2839
|
+
try {
|
|
2840
|
+
const choice = (await question(rl, "Choose a key [1]: ")).trim();
|
|
2841
|
+
if (choice) {
|
|
2842
|
+
const index = Number.parseInt(choice, 10) - 1;
|
|
2843
|
+
if (!Number.isInteger(index) || index < 0 || index >= candidates.length) {
|
|
2844
|
+
throw new CookbookError("Invalid SSH key selection.");
|
|
2845
|
+
}
|
|
2846
|
+
selected = candidates[index];
|
|
2847
|
+
}
|
|
2848
|
+
} finally {
|
|
2849
|
+
rl.close();
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
const orderedCandidates = [selected, ...candidates.filter((candidate) => candidate !== selected)];
|
|
2853
|
+
const failures = [];
|
|
2854
|
+
for (const candidate of orderedCandidates) {
|
|
2855
|
+
const publicKey = import_fs3.default.readFileSync(candidate, "utf8").trim();
|
|
2856
|
+
if (!publicKey) {
|
|
2857
|
+
failures.push(`${candidate}: empty file`);
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
try {
|
|
2861
|
+
const result = await client.addSshKey(publicKey);
|
|
2862
|
+
if (progress) {
|
|
2863
|
+
progress(`Registered SSH key from ${candidate}`);
|
|
2864
|
+
}
|
|
2865
|
+
return { configured: true, auto_added: true, selected_path: candidate, result };
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2868
|
+
failures.push(`${candidate}: ${message}`);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
throw new CookbookError(`Unable to register any local public SSH key. Tried: ${failures.join("; ")}`);
|
|
2715
2872
|
}
|
|
2716
|
-
async function
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2873
|
+
async function resolveSecretValues(manifest, deps) {
|
|
2874
|
+
const values = {};
|
|
2875
|
+
const configuredNames = [];
|
|
2876
|
+
for (const secret of manifest.secrets) {
|
|
2877
|
+
let value = (process.env[secret.env_name] || "").trim();
|
|
2878
|
+
if (!value && secret.required) {
|
|
2879
|
+
if (!process.stdin.isTTY) {
|
|
2880
|
+
throw new CookbookError(
|
|
2881
|
+
`Secret \`${secret.env_name}\` is required for \`${manifest.slug}\`. Set it in the environment or rerun interactively.`
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
value = (await deps.promptSecret(`${secret.prompt}: `)).trim();
|
|
2885
|
+
}
|
|
2886
|
+
if (value) {
|
|
2887
|
+
values[secret.env_name] = value;
|
|
2888
|
+
configuredNames.push(secret.env_name);
|
|
2889
|
+
} else if (secret.required) {
|
|
2890
|
+
throw new CookbookError(`Secret \`${secret.env_name}\` is required for \`${manifest.slug}\`.`);
|
|
2891
|
+
}
|
|
2720
2892
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
...status,
|
|
2727
|
-
session_id: sessionId,
|
|
2728
|
-
vm_id: status.vm_id || vm.vm_id
|
|
2729
|
-
};
|
|
2893
|
+
return { values, configuredNames };
|
|
2894
|
+
}
|
|
2895
|
+
async function resolveSnapshotId(client, manifest) {
|
|
2896
|
+
if (!manifest.artifact) {
|
|
2897
|
+
throw new CookbookError("Published snapshot deploys require `artifact` config.");
|
|
2730
2898
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2899
|
+
const snapshots = await client.snapshots.list({ type: "system" });
|
|
2900
|
+
const found = snapshots.find(
|
|
2901
|
+
(snapshot) => snapshot.name === manifest.artifact?.snapshot_name && snapshot.status === "ready" && snapshot.is_public
|
|
2902
|
+
);
|
|
2903
|
+
if (!found?.id) {
|
|
2904
|
+
throw new CookbookError(`Public system snapshot \`${manifest.artifact.snapshot_name}\` was not found or is not ready.`);
|
|
2905
|
+
}
|
|
2906
|
+
return String(found.id);
|
|
2907
|
+
}
|
|
2908
|
+
function buildVmPayload(manifest, snapshotId) {
|
|
2909
|
+
const payload = {
|
|
2910
|
+
memory_mb: manifest.vm.memory_mb,
|
|
2911
|
+
vcpu_count: manifest.vm.vcpu_count,
|
|
2912
|
+
vm_lifetime_seconds: manifest.vm.timeout_seconds
|
|
2736
2913
|
};
|
|
2914
|
+
if (manifest.vm.image_variant) {
|
|
2915
|
+
payload.image_variant = manifest.vm.image_variant;
|
|
2916
|
+
}
|
|
2917
|
+
if (snapshotId) {
|
|
2918
|
+
payload.snapshot_id = snapshotId;
|
|
2919
|
+
}
|
|
2920
|
+
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== void 0 && value !== null));
|
|
2737
2921
|
}
|
|
2738
|
-
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2922
|
+
function isResourceTierError(error) {
|
|
2923
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2924
|
+
const normalized = message.toLowerCase();
|
|
2925
|
+
return normalized.includes("free tier") && (normalized.includes("configurable cpu") || normalized.includes("configurable memory"));
|
|
2926
|
+
}
|
|
2927
|
+
function planLifetimeLimitSeconds(error) {
|
|
2928
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2929
|
+
const match = message.match(/vm lifetime cannot exceed\s+(\d+)s/i);
|
|
2930
|
+
if (!match) {
|
|
2931
|
+
return void 0;
|
|
2932
|
+
}
|
|
2933
|
+
return Number.parseInt(match[1], 10);
|
|
2934
|
+
}
|
|
2935
|
+
async function createVm(client, manifest, snapshotId, progress) {
|
|
2936
|
+
let payload = buildVmPayload(manifest, snapshotId);
|
|
2937
|
+
let adjustedForTier = false;
|
|
2938
|
+
let adjustedForLifetime = false;
|
|
2939
|
+
while (true) {
|
|
2741
2940
|
try {
|
|
2742
|
-
|
|
2941
|
+
return await client.vms.create(payload, true);
|
|
2743
2942
|
} catch (error) {
|
|
2943
|
+
let nextPayload = { ...payload };
|
|
2944
|
+
if (isResourceTierError(error) && ("memory_mb" in nextPayload || "vcpu_count" in nextPayload)) {
|
|
2945
|
+
nextPayload = Object.fromEntries(
|
|
2946
|
+
Object.entries(nextPayload).filter(([key]) => key !== "memory_mb" && key !== "vcpu_count")
|
|
2947
|
+
);
|
|
2948
|
+
if (progress && !adjustedForTier) {
|
|
2949
|
+
progress("Requested CPU or memory exceeds the current account tier.");
|
|
2950
|
+
progress("Retrying VM creation with platform defaults");
|
|
2951
|
+
}
|
|
2952
|
+
adjustedForTier = true;
|
|
2953
|
+
}
|
|
2954
|
+
const lifetimeLimit = planLifetimeLimitSeconds(error);
|
|
2955
|
+
const currentLifetime = typeof nextPayload.vm_lifetime_seconds === "number" ? nextPayload.vm_lifetime_seconds : void 0;
|
|
2956
|
+
if (lifetimeLimit && currentLifetime && currentLifetime > lifetimeLimit) {
|
|
2957
|
+
nextPayload.vm_lifetime_seconds = lifetimeLimit;
|
|
2958
|
+
if (progress && !adjustedForLifetime) {
|
|
2959
|
+
progress("Requested VM lifetime exceeds the current account tier.");
|
|
2960
|
+
progress(`Retrying VM creation with a ${lifetimeLimit}s lifetime`);
|
|
2961
|
+
}
|
|
2962
|
+
adjustedForLifetime = true;
|
|
2963
|
+
}
|
|
2964
|
+
if (JSON.stringify(nextPayload) === JSON.stringify(payload)) {
|
|
2965
|
+
throw error;
|
|
2966
|
+
}
|
|
2967
|
+
payload = nextPayload;
|
|
2744
2968
|
}
|
|
2745
2969
|
}
|
|
2746
|
-
return payload;
|
|
2747
2970
|
}
|
|
2748
|
-
function
|
|
2971
|
+
async function runDeploy(manifest, client, settings, deps, progress, snapshotIdOverride) {
|
|
2972
|
+
preflightTools(deps);
|
|
2973
|
+
const sshKeyInfo = await ensureAccountSshKey(client, deps, progress);
|
|
2974
|
+
const { values: secretValues, configuredNames } = await resolveSecretValues(manifest, deps);
|
|
2975
|
+
let snapshotId = snapshotIdOverride;
|
|
2976
|
+
if (!snapshotId && manifest.deploy.kind === "published_snapshot") {
|
|
2977
|
+
if (progress) {
|
|
2978
|
+
progress(`Resolving public snapshot ${manifest.artifact?.snapshot_name}`);
|
|
2979
|
+
}
|
|
2980
|
+
snapshotId = await resolveSnapshotId(client, manifest);
|
|
2981
|
+
}
|
|
2982
|
+
if (progress) {
|
|
2983
|
+
progress("Creating VM");
|
|
2984
|
+
}
|
|
2985
|
+
const vm = await createVm(client, manifest, snapshotId, progress);
|
|
2986
|
+
const vmId = String(vm.vm_id || "");
|
|
2987
|
+
const sessionId = String(vm.session_id || "");
|
|
2988
|
+
if (!vmId) {
|
|
2989
|
+
throw new CookbookError("VM creation did not return a VM ID.");
|
|
2990
|
+
}
|
|
2991
|
+
const sshTarget = `${vmId}@${settings.sshHost}`;
|
|
2992
|
+
const identityFile = resolveIdentityFile(sshKeyInfo.selected_path);
|
|
2993
|
+
let remoteBase = `/home/appuser/${DEFAULT_REMOTE_ROOT}`;
|
|
2994
|
+
let remoteRoot = `${remoteBase}/${manifest.slug}`;
|
|
2995
|
+
let workdir = remoteWorkdir(manifest, remoteRoot);
|
|
2996
|
+
const connectCommand = buildConnectCommand(vmId, settings.sshHost, identityFile);
|
|
2997
|
+
let serviceMode = "unknown";
|
|
2998
|
+
let serviceName = `instavm-cookbook-${manifest.slug}`;
|
|
2999
|
+
try {
|
|
3000
|
+
if (progress) {
|
|
3001
|
+
progress("Waiting for SSH access");
|
|
3002
|
+
}
|
|
3003
|
+
waitForSshReady(deps, sshTarget, identityFile);
|
|
3004
|
+
remoteBase = resolveRemoteBase(deps, sshTarget, identityFile);
|
|
3005
|
+
remoteRoot = `${remoteBase}/${manifest.slug}`;
|
|
3006
|
+
workdir = remoteWorkdir(manifest, remoteRoot);
|
|
3007
|
+
if (manifest.deploy.kind === "upload_and_run") {
|
|
3008
|
+
const archivePath = packageSource(manifest, deps);
|
|
3009
|
+
try {
|
|
3010
|
+
const remoteTarball = `/tmp/${manifest.slug}-${Date.now()}.tar.gz`;
|
|
3011
|
+
if (progress) {
|
|
3012
|
+
progress("Uploading cookbook source");
|
|
3013
|
+
}
|
|
3014
|
+
runScp(deps, archivePath, `${sshTarget}:${remoteTarball}`, "Failed to upload the cookbook bundle to the VM.", identityFile);
|
|
3015
|
+
runSsh(
|
|
3016
|
+
deps,
|
|
3017
|
+
sshTarget,
|
|
3018
|
+
`mkdir -p ${shellEscape(remoteRoot)} && tar -xzf ${shellEscape(remoteTarball)} -C ${shellEscape(remoteRoot)} && rm -f ${shellEscape(remoteTarball)}`,
|
|
3019
|
+
"Failed to extract the cookbook bundle.",
|
|
3020
|
+
false,
|
|
3021
|
+
identityFile
|
|
3022
|
+
);
|
|
3023
|
+
if (progress) {
|
|
3024
|
+
progress("Running cookbook setup");
|
|
3025
|
+
}
|
|
3026
|
+
runSsh(deps, sshTarget, `cd ${shellEscape(workdir)} && ${manifest.source?.setup_command}`, "Cookbook setup command failed.", false, identityFile);
|
|
3027
|
+
} finally {
|
|
3028
|
+
import_fs3.default.rmSync(archivePath, { force: true });
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
if (progress) {
|
|
3032
|
+
progress("Starting application service");
|
|
3033
|
+
}
|
|
3034
|
+
const started = startRemoteService(deps, sshTarget, manifest, remoteRoot, workdir, secretValues, identityFile);
|
|
3035
|
+
serviceName = started.serviceName;
|
|
3036
|
+
serviceMode = started.serviceMode;
|
|
3037
|
+
if (progress) {
|
|
3038
|
+
progress("Waiting for application healthcheck");
|
|
3039
|
+
}
|
|
3040
|
+
waitForInternalHealth(deps, sshTarget, manifest.app.port, manifest.app.healthcheck_path, manifest.app.readiness_timeout_seconds, identityFile);
|
|
3041
|
+
if (progress) {
|
|
3042
|
+
progress("Creating public share");
|
|
3043
|
+
}
|
|
3044
|
+
const share = await client.shares.create({
|
|
3045
|
+
vm_id: vmId,
|
|
3046
|
+
port: manifest.app.port,
|
|
3047
|
+
is_public: manifest.app.share_public_default
|
|
3048
|
+
});
|
|
3049
|
+
const shareUrl = String(share.url || "");
|
|
3050
|
+
if (!shareUrl) {
|
|
3051
|
+
throw new CookbookError("Share creation did not return a URL.");
|
|
3052
|
+
}
|
|
3053
|
+
if (progress) {
|
|
3054
|
+
progress("Verifying public share URL");
|
|
3055
|
+
}
|
|
3056
|
+
const healthcheckUrl = new URL(manifest.app.healthcheck_path.replace(/^\//, ""), shareUrl.endsWith("/") ? shareUrl : `${shareUrl}/`).toString();
|
|
3057
|
+
await verifyPublicShare(healthcheckUrl, manifest.app.readiness_timeout_seconds);
|
|
3058
|
+
return {
|
|
3059
|
+
slug: manifest.slug,
|
|
3060
|
+
version: manifest.version,
|
|
3061
|
+
deploy_kind: manifest.deploy.kind,
|
|
3062
|
+
vm_id: vmId,
|
|
3063
|
+
session_id: sessionId,
|
|
3064
|
+
service_name: serviceName,
|
|
3065
|
+
service_mode: serviceMode,
|
|
3066
|
+
port: manifest.app.port,
|
|
3067
|
+
share_url: shareUrl,
|
|
3068
|
+
healthcheck_url: healthcheckUrl,
|
|
3069
|
+
connect_command: connectCommand,
|
|
3070
|
+
logs_command: logsCommand(vmId, settings.sshHost, manifest.slug, serviceName, serviceMode, remoteBase, manifest.run.logs_hint, identityFile),
|
|
3071
|
+
destroy_command: `instavm rm ${vmId}`,
|
|
3072
|
+
configured_secrets: configuredNames,
|
|
3073
|
+
notes: manifest.post_deploy_notes
|
|
3074
|
+
};
|
|
3075
|
+
} catch (error) {
|
|
3076
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3077
|
+
throw new CookbookDeployError(message, {
|
|
3078
|
+
slug: manifest.slug,
|
|
3079
|
+
version: manifest.version,
|
|
3080
|
+
deploy_kind: manifest.deploy.kind,
|
|
3081
|
+
vm_id: vmId,
|
|
3082
|
+
session_id: sessionId,
|
|
3083
|
+
service_name: serviceName,
|
|
3084
|
+
service_mode: serviceMode,
|
|
3085
|
+
port: manifest.app.port,
|
|
3086
|
+
connect_command: connectCommand,
|
|
3087
|
+
logs_command: logsCommand(vmId, settings.sshHost, manifest.slug, serviceName, serviceMode, remoteBase, manifest.run.logs_hint, identityFile),
|
|
3088
|
+
destroy_command: `instavm rm ${vmId}`,
|
|
3089
|
+
configured_secrets: configuredNames
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
async function deployCookbook(manifest, client, settings, deps, progress) {
|
|
3094
|
+
return runDeploy(manifest, client, settings, deps, progress);
|
|
3095
|
+
}
|
|
3096
|
+
function packageSource(manifest, deps) {
|
|
3097
|
+
if (!manifest.source) {
|
|
3098
|
+
throw new CookbookError("Upload-and-run cookbooks require `source` config.");
|
|
3099
|
+
}
|
|
3100
|
+
const stagingRoot = import_fs3.default.mkdtempSync(import_path12.default.join(import_os2.default.tmpdir(), `${manifest.slug}-src-`));
|
|
3101
|
+
const archivePath = import_path12.default.join(import_os2.default.tmpdir(), `${manifest.slug}-${Date.now()}.tar.gz`);
|
|
3102
|
+
try {
|
|
3103
|
+
const excludes = /* @__PURE__ */ new Set([...DEFAULT_SOURCE_EXCLUDES, ...manifest.source.exclude]);
|
|
3104
|
+
copySelectedTree(manifest.path, stagingRoot, manifest.source.include, excludes);
|
|
3105
|
+
runCommand(deps, "tar", ["-czf", archivePath, "-C", stagingRoot, "."], "Failed to package the cookbook source.");
|
|
3106
|
+
} finally {
|
|
3107
|
+
import_fs3.default.rmSync(stagingRoot, { recursive: true, force: true });
|
|
3108
|
+
}
|
|
3109
|
+
return archivePath;
|
|
3110
|
+
}
|
|
3111
|
+
function copySelectedTree(sourceRoot, targetRoot, include, exclude) {
|
|
3112
|
+
const visit = (currentSource, currentRelative) => {
|
|
3113
|
+
const entries = import_fs3.default.readdirSync(currentSource, { withFileTypes: true });
|
|
3114
|
+
for (const entry of entries) {
|
|
3115
|
+
const relative = currentRelative ? `${currentRelative}/${entry.name}` : entry.name;
|
|
3116
|
+
if (!shouldInclude(relative, include, exclude)) {
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
const sourcePath = import_path12.default.join(currentSource, entry.name);
|
|
3120
|
+
const targetPath = import_path12.default.join(targetRoot, relative);
|
|
3121
|
+
if (entry.isDirectory()) {
|
|
3122
|
+
import_fs3.default.mkdirSync(targetPath, { recursive: true });
|
|
3123
|
+
visit(sourcePath, relative);
|
|
3124
|
+
} else {
|
|
3125
|
+
import_fs3.default.mkdirSync(import_path12.default.dirname(targetPath), { recursive: true });
|
|
3126
|
+
import_fs3.default.copyFileSync(sourcePath, targetPath);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
visit(sourceRoot, "");
|
|
3131
|
+
}
|
|
3132
|
+
function shouldInclude(relative, include, exclude) {
|
|
3133
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
3134
|
+
for (const pattern of exclude) {
|
|
3135
|
+
if (matchesPattern(normalized, pattern)) {
|
|
3136
|
+
return false;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (include.length === 0) {
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
3142
|
+
return include.some((pattern) => matchesPattern(normalized, pattern) || normalized.startsWith(`${pattern.replace(/\/$/, "")}/`));
|
|
3143
|
+
}
|
|
3144
|
+
function matchesPattern(value, pattern) {
|
|
3145
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3146
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
3147
|
+
}
|
|
3148
|
+
function startRemoteService(deps, sshTarget, manifest, remoteRoot, workdir, secretValues, identityFile) {
|
|
3149
|
+
const serviceName = `instavm-cookbook-${manifest.slug}`;
|
|
3150
|
+
const remoteBase = remoteRoot.slice(0, remoteRoot.lastIndexOf("/"));
|
|
3151
|
+
const logFile = `${remoteBase}/logs/${manifest.slug}.log`;
|
|
3152
|
+
const environment = Object.entries(secretValues).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${shellEscape(value)}`).join(" ");
|
|
3153
|
+
const launchBody = `cd ${shellEscape(workdir)} && ${environment} ${manifest.run.start_command}`.trim();
|
|
3154
|
+
const remoteScript = `
|
|
3155
|
+
set -e
|
|
3156
|
+
mkdir -p ${shellEscape(`${remoteBase}/logs`)}
|
|
3157
|
+
mkdir -p ${shellEscape(remoteRoot)}
|
|
3158
|
+
if command -v systemd-run >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
|
|
3159
|
+
if systemd-run --unit ${shellEscape(serviceName)} --collect --quiet --property=WorkingDirectory=${shellEscape(workdir)} /bin/bash -lc ${shellEscape(launchBody)} >/dev/null 2>&1; then
|
|
3160
|
+
printf '%s' 'systemd'
|
|
3161
|
+
exit 0
|
|
3162
|
+
fi
|
|
3163
|
+
fi
|
|
3164
|
+
nohup /bin/bash -lc ${shellEscape(launchBody)} > ${shellEscape(logFile)} 2>&1 < /dev/null &
|
|
3165
|
+
echo $! > ${shellEscape(`${remoteBase}/logs/${manifest.slug}.pid`)}
|
|
3166
|
+
printf '%s' 'nohup'
|
|
3167
|
+
`.trim();
|
|
3168
|
+
const result = runSsh(deps, sshTarget, remoteScript, "Failed to start the cookbook service.", true, identityFile);
|
|
3169
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf8") || "";
|
|
3170
|
+
return {
|
|
3171
|
+
serviceName,
|
|
3172
|
+
serviceMode: stdout.trim() || "nohup"
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
function waitForInternalHealth(deps, sshTarget, port, healthcheckPath, timeoutSeconds, identityFile) {
|
|
3176
|
+
const url = `http://127.0.0.1:${port}${healthcheckPath}`;
|
|
3177
|
+
const attempts = Math.max(Math.floor(timeoutSeconds / 2), 1);
|
|
3178
|
+
const remoteScript = `
|
|
3179
|
+
set -e
|
|
3180
|
+
URL=${shellEscape(url)}
|
|
3181
|
+
for _ in $(seq 1 ${attempts}); do
|
|
3182
|
+
if command -v curl >/dev/null 2>&1; then
|
|
3183
|
+
curl -fsS "$URL" >/dev/null && exit 0
|
|
3184
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
3185
|
+
wget -q -O /dev/null "$URL" && exit 0
|
|
3186
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
3187
|
+
python3 - <<'PY' && exit 0
|
|
3188
|
+
from urllib.request import urlopen
|
|
3189
|
+
with urlopen(${JSON.stringify(url)}, timeout=3) as response:
|
|
3190
|
+
if response.status >= 400:
|
|
3191
|
+
raise SystemExit(1)
|
|
3192
|
+
PY
|
|
3193
|
+
fi
|
|
3194
|
+
sleep 2
|
|
3195
|
+
done
|
|
3196
|
+
exit 1
|
|
3197
|
+
`.trim();
|
|
3198
|
+
runSsh(deps, sshTarget, remoteScript, "Timed out waiting for the app healthcheck.", false, identityFile);
|
|
3199
|
+
}
|
|
3200
|
+
async function verifyPublicShare(url, timeoutSeconds) {
|
|
3201
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
3202
|
+
while (Date.now() < deadline) {
|
|
3203
|
+
try {
|
|
3204
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
3205
|
+
if (response.ok) {
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
}
|
|
3210
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3211
|
+
}
|
|
3212
|
+
throw new CookbookError(`Timed out verifying the public share URL: ${url}`);
|
|
3213
|
+
}
|
|
3214
|
+
function runSsh(deps, sshTarget, remoteCommand, errorMessage, captureOutput = false, identityFile) {
|
|
3215
|
+
const args = ["-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new"];
|
|
3216
|
+
if (identityFile) {
|
|
3217
|
+
args.push("-o", "IdentitiesOnly=yes", "-i", identityFile);
|
|
3218
|
+
}
|
|
3219
|
+
args.push(sshTarget, remoteCommand);
|
|
3220
|
+
return runCommand(
|
|
3221
|
+
deps,
|
|
3222
|
+
"ssh",
|
|
3223
|
+
args,
|
|
3224
|
+
errorMessage,
|
|
3225
|
+
captureOutput
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
function runScp(deps, localPath, remotePath, errorMessage, identityFile) {
|
|
3229
|
+
const args = ["-q"];
|
|
3230
|
+
if (identityFile) {
|
|
3231
|
+
args.push("-o", "IdentitiesOnly=yes", "-i", identityFile);
|
|
3232
|
+
}
|
|
3233
|
+
args.push(localPath, remotePath);
|
|
3234
|
+
return runCommand(deps, "scp", args, errorMessage);
|
|
3235
|
+
}
|
|
3236
|
+
function waitForSshReady(deps, sshTarget, identityFile, timeoutSeconds = 90) {
|
|
3237
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
3238
|
+
let lastError;
|
|
3239
|
+
while (Date.now() < deadline) {
|
|
3240
|
+
try {
|
|
3241
|
+
runSsh(deps, sshTarget, "true", "Timed out waiting for SSH access to the VM.", true, identityFile);
|
|
3242
|
+
return;
|
|
3243
|
+
} catch (error) {
|
|
3244
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3245
|
+
}
|
|
3246
|
+
sleepMs(2e3);
|
|
3247
|
+
}
|
|
3248
|
+
if (lastError) {
|
|
3249
|
+
throw new CookbookError(`Timed out waiting for SSH access to the VM: ${sshTarget}. Last SSH error: ${lastError}`);
|
|
3250
|
+
}
|
|
3251
|
+
throw new CookbookError(`Timed out waiting for SSH access to the VM: ${sshTarget}`);
|
|
3252
|
+
}
|
|
3253
|
+
function emitCookbookDeployError(deps, options, payload) {
|
|
3254
|
+
if (options.json) {
|
|
3255
|
+
deps.stdout.write(`${JSON.stringify(payload)}
|
|
3256
|
+
`);
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
deps.stderr.write(`Deploy failed: ${payload.error}
|
|
3260
|
+
`);
|
|
3261
|
+
for (const [label, value] of [
|
|
3262
|
+
["VM", payload.vm_id],
|
|
3263
|
+
["Connect", payload.connect_command],
|
|
3264
|
+
["Logs", payload.logs_command],
|
|
3265
|
+
["Destroy", payload.destroy_command]
|
|
3266
|
+
]) {
|
|
3267
|
+
deps.stderr.write(`${label}: ${value || "-"}
|
|
3268
|
+
`);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
function runCommand(deps, command, args, errorMessage, captureOutput = false) {
|
|
3272
|
+
const result = deps.spawnSync(command, args, {
|
|
3273
|
+
encoding: "utf8",
|
|
3274
|
+
stdio: captureOutput ? ["ignore", "pipe", "pipe"] : ["ignore", "ignore", "pipe"]
|
|
3275
|
+
});
|
|
3276
|
+
if (result.error) {
|
|
3277
|
+
throw new CookbookError(errorMessage);
|
|
3278
|
+
}
|
|
3279
|
+
if ((result.status ?? 0) !== 0) {
|
|
3280
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr || "");
|
|
3281
|
+
const detail = stderr.trim() ? ` ${stderr.trim()}` : "";
|
|
3282
|
+
throw new CookbookError(`${errorMessage}${detail}`);
|
|
3283
|
+
}
|
|
3284
|
+
return result;
|
|
3285
|
+
}
|
|
3286
|
+
function logsCommand(vmId, sshHost, slug, serviceName, serviceMode, remoteBase, logsHint, identityFile) {
|
|
3287
|
+
if (logsHint) {
|
|
3288
|
+
return logsHint;
|
|
3289
|
+
}
|
|
3290
|
+
const sshPrefix = buildSshPrefix(vmId, sshHost, identityFile);
|
|
3291
|
+
if (serviceMode === "systemd") {
|
|
3292
|
+
return `${sshPrefix} journalctl --no-pager -u ${serviceName} -n 200`;
|
|
3293
|
+
}
|
|
3294
|
+
return `${sshPrefix} tail -n 200 ${shellEscape(`${remoteBase}/logs/${slug}.log`)}`;
|
|
3295
|
+
}
|
|
3296
|
+
function remoteWorkdir(manifest, remoteRoot) {
|
|
3297
|
+
if (manifest.run.workdir.startsWith("/")) {
|
|
3298
|
+
return manifest.run.workdir;
|
|
3299
|
+
}
|
|
3300
|
+
if (manifest.run.workdir === "." || manifest.run.workdir === "./") {
|
|
3301
|
+
return remoteRoot;
|
|
3302
|
+
}
|
|
3303
|
+
return `${remoteRoot}/${manifest.run.workdir.replace(/^\.\//, "").replace(/\/$/, "")}`;
|
|
3304
|
+
}
|
|
3305
|
+
function resolveRemoteBase(deps, sshTarget, identityFile) {
|
|
3306
|
+
const result = runSsh(
|
|
3307
|
+
deps,
|
|
3308
|
+
sshTarget,
|
|
3309
|
+
'printf %s "$HOME"',
|
|
3310
|
+
"Failed to determine the remote home directory.",
|
|
3311
|
+
true,
|
|
3312
|
+
identityFile
|
|
3313
|
+
);
|
|
3314
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf8") || "";
|
|
3315
|
+
const remoteHome = stdout.trim() || "/tmp";
|
|
3316
|
+
return `${remoteHome}/${DEFAULT_REMOTE_ROOT}`;
|
|
3317
|
+
}
|
|
3318
|
+
function findExecutable(deps, binary) {
|
|
3319
|
+
const result = deps.spawnSync(process.platform === "win32" ? "where" : "which", [binary], {
|
|
3320
|
+
encoding: "utf8",
|
|
3321
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3322
|
+
});
|
|
3323
|
+
if ((result.status ?? 1) !== 0) {
|
|
3324
|
+
return null;
|
|
3325
|
+
}
|
|
3326
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout || "");
|
|
3327
|
+
const output = stdout.split(/\r?\n/).find(Boolean);
|
|
3328
|
+
return output || null;
|
|
3329
|
+
}
|
|
3330
|
+
function shellEscape(value) {
|
|
3331
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
3332
|
+
}
|
|
3333
|
+
function resolveIdentityFile(selectedPath) {
|
|
3334
|
+
if (!selectedPath) {
|
|
3335
|
+
return void 0;
|
|
3336
|
+
}
|
|
3337
|
+
const candidate = selectedPath.endsWith(".pub") ? selectedPath.slice(0, -4) : selectedPath;
|
|
3338
|
+
return import_fs3.default.existsSync(candidate) ? candidate : void 0;
|
|
3339
|
+
}
|
|
3340
|
+
function buildSshPrefix(vmId, sshHost, identityFile) {
|
|
3341
|
+
if (identityFile) {
|
|
3342
|
+
return `ssh -o IdentitiesOnly=yes -i ${shellEscape(identityFile)} ${vmId}@${sshHost}`;
|
|
3343
|
+
}
|
|
3344
|
+
return `ssh ${vmId}@${sshHost}`;
|
|
3345
|
+
}
|
|
3346
|
+
function buildConnectCommand(vmId, sshHost, identityFile) {
|
|
3347
|
+
if (identityFile) {
|
|
3348
|
+
return buildSshPrefix(vmId, sshHost, identityFile);
|
|
3349
|
+
}
|
|
3350
|
+
return `instavm connect ${vmId}`;
|
|
3351
|
+
}
|
|
3352
|
+
function sleepMs(durationMs) {
|
|
3353
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, durationMs);
|
|
3354
|
+
}
|
|
3355
|
+
function question(rl, prompt) {
|
|
3356
|
+
return new Promise((resolve) => {
|
|
3357
|
+
rl.question(prompt, resolve);
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
// src/cli/detect.ts
|
|
3362
|
+
var import_fs4 = __toESM(require("fs"));
|
|
3363
|
+
var import_path13 = __toESM(require("path"));
|
|
3364
|
+
var NON_SECRET_VARS = /* @__PURE__ */ new Set([
|
|
3365
|
+
"PORT",
|
|
3366
|
+
"HOST",
|
|
3367
|
+
"NODE_ENV",
|
|
3368
|
+
"DEBUG",
|
|
3369
|
+
"PYTHONPATH",
|
|
3370
|
+
"PATH",
|
|
3371
|
+
"HOME",
|
|
3372
|
+
"HOSTNAME",
|
|
3373
|
+
"LANG",
|
|
3374
|
+
"TZ",
|
|
3375
|
+
"TERM",
|
|
3376
|
+
"CI",
|
|
3377
|
+
"VIRTUAL_ENV",
|
|
3378
|
+
"PWD",
|
|
3379
|
+
"SHELL",
|
|
3380
|
+
"USER",
|
|
3381
|
+
"LOGNAME",
|
|
3382
|
+
"TMPDIR",
|
|
3383
|
+
"PYTHONDONTWRITEBYTECODE",
|
|
3384
|
+
"PYTHONUNBUFFERED",
|
|
3385
|
+
"NPM_CONFIG_LOGLEVEL",
|
|
3386
|
+
"NEXT_TELEMETRY_DISABLED",
|
|
3387
|
+
// Model/config env vars that typically have defaults in source
|
|
3388
|
+
"OPENAI_MODEL",
|
|
3389
|
+
"GOOGLE_MODEL",
|
|
3390
|
+
"ANTHROPIC_MODEL",
|
|
3391
|
+
"OPENAI_COMPAT_API_BASE",
|
|
3392
|
+
"OPENAI_COMPAT_MODEL"
|
|
3393
|
+
]);
|
|
3394
|
+
var DEP_SECRET_HINTS = {
|
|
3395
|
+
// Python
|
|
3396
|
+
"anthropic": ["ANTHROPIC_API_KEY", "Anthropic API key"],
|
|
3397
|
+
"openai-agents": ["OPENAI_API_KEY", "OpenAI API key"],
|
|
3398
|
+
"openai": ["OPENAI_API_KEY", "OpenAI API key"],
|
|
3399
|
+
"google-adk": ["GOOGLE_API_KEY", "Google API key"],
|
|
3400
|
+
"google-generativeai": ["GOOGLE_API_KEY", "Google API key"],
|
|
3401
|
+
"replicate": ["REPLICATE_API_TOKEN", "Replicate API token"],
|
|
3402
|
+
// Node
|
|
3403
|
+
"@anthropic-ai/claude-agent-sdk": ["ANTHROPIC_API_KEY", "Anthropic API key"],
|
|
3404
|
+
"@anthropic-ai/sdk": ["ANTHROPIC_API_KEY", "Anthropic API key"],
|
|
3405
|
+
"@openai/agents": ["OPENAI_API_KEY", "OpenAI API key"],
|
|
3406
|
+
"@ai-sdk/anthropic": ["ANTHROPIC_API_KEY", "Anthropic API key"],
|
|
3407
|
+
"@ai-sdk/openai": ["OPENAI_API_KEY", "OpenAI API key"],
|
|
3408
|
+
"@ai-sdk/google": ["GOOGLE_API_KEY", "Google API key"]
|
|
3409
|
+
};
|
|
3410
|
+
var PYTHON_APP_CANDIDATES = ["app.py", "main.py", "server.py", "bot.py", "start.py", "src/app.py", "src/main.py"];
|
|
3411
|
+
var NODE_SOURCE_CANDIDATES = [
|
|
3412
|
+
"server.mjs",
|
|
3413
|
+
"server.js",
|
|
3414
|
+
"server.ts",
|
|
3415
|
+
"index.js",
|
|
3416
|
+
"index.ts",
|
|
3417
|
+
"src/index.ts",
|
|
3418
|
+
"src/index.js",
|
|
3419
|
+
"src/server.ts",
|
|
3420
|
+
"src/server.js",
|
|
3421
|
+
"server/server.ts",
|
|
3422
|
+
"server/server.js"
|
|
3423
|
+
];
|
|
3424
|
+
var GO_SOURCE_CANDIDATES = ["main.go", "server.go", "app.go", "cmd/server/main.go", "cmd/main.go"];
|
|
3425
|
+
var DENO_MAIN_CANDIDATES = ["main.ts", "main.js", "main.mjs", "main.mts", "server.ts", "server.js", "app.ts", "app.js"];
|
|
3426
|
+
var STATIC_ROOT_CANDIDATES = ["public", "dist", "build"];
|
|
3427
|
+
var HEALTH_ROUTE_RE = /(?:@app\.(?:get|route)|app\.get|router\.get)\s*\(\s*['"](\/(health|healthz|api\/health))['"]\s*/;
|
|
3428
|
+
var GO_HEALTH_RE = /(?:HandleFunc|Handle)\s*\(\s*['"](\/(health|healthz|api\/health))['"]/;
|
|
3429
|
+
var GO_PORT_RE = /ListenAndServe\s*\(\s*["']:(\d{2,5})["']/;
|
|
3430
|
+
var PORT_PATTERN_RE = /PORT\s*(?:\|\||===?\s*undefined\s*\?\s*|\?\?)\s*['"]?(\d{2,5})['"]?/;
|
|
3431
|
+
var PYTHON_PORT_PATTERNS = [
|
|
3432
|
+
/os\.(?:getenv|environ\.get)\s*\(\s*['"]PORT['"]\s*,\s*['"]?(\d{2,5})['"]?\s*\)/,
|
|
3433
|
+
/runserver\s+0\.0\.0\.0:(\d{2,5})/,
|
|
3434
|
+
/--server\.port\s+(\d{2,5})/,
|
|
3435
|
+
/--port\s+(\d{2,5})/
|
|
3436
|
+
];
|
|
3437
|
+
var PYTHON_ENV_RE = /os\.(?:environ\s*(?:\.get\s*\(|(?:\[)))\s*['"]([\w]+)['"]|os\.getenv\s*\(\s*['"]([\w]+)['"]/g;
|
|
3438
|
+
var NODE_ENV_RE = /process\.env\.([\w]+)/g;
|
|
3439
|
+
var GO_ENV_RE = /os\.(?:Getenv|LookupEnv)\s*\(\s*["'](\w+)["']\)/g;
|
|
3440
|
+
var DENO_ENV_RE = /Deno\.env\.get\s*\(\s*["'](\w+)["']\)/g;
|
|
3441
|
+
var DJANGO_WSGI_RE = /WSGI_APPLICATION\s*=\s*["']([A-Za-z_][\w.]*)\.application["']/;
|
|
3442
|
+
var NVM_SETUP_PREFIX = 'export NVM_DIR="$HOME/.nvm" && if [ ! -s "$NVM_DIR/nvm.sh" ]; then curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash; fi && . "$NVM_DIR/nvm.sh" && nvm install {NODE_VERSION} && nvm use {NODE_VERSION} && ';
|
|
3443
|
+
var NVM_RUN_PREFIX = 'export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh" && nvm use {NODE_VERSION} >/dev/null && ';
|
|
3444
|
+
var COREPACK_ENABLE_PREFIX = "corepack enable >/dev/null 2>&1 && ";
|
|
3445
|
+
function shellQuote(value) {
|
|
3446
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
3447
|
+
}
|
|
3448
|
+
function detectProject(projectPath) {
|
|
3449
|
+
const resolved = import_path13.default.resolve(projectPath);
|
|
3450
|
+
if (!import_fs4.default.existsSync(resolved) || !import_fs4.default.statSync(resolved).isDirectory()) {
|
|
3451
|
+
throw new Error(`Not a directory: ${resolved}`);
|
|
3452
|
+
}
|
|
3453
|
+
if (import_fs4.default.existsSync(import_path13.default.join(resolved, "package.json"))) {
|
|
3454
|
+
return detectNode(resolved);
|
|
3455
|
+
}
|
|
3456
|
+
if (import_fs4.default.existsSync(import_path13.default.join(resolved, "requirements.txt")) || import_fs4.default.existsSync(import_path13.default.join(resolved, "pyproject.toml")) || import_fs4.default.existsSync(import_path13.default.join(resolved, "Pipfile"))) {
|
|
3457
|
+
return detectPython(resolved);
|
|
3458
|
+
}
|
|
3459
|
+
if (import_fs4.default.existsSync(import_path13.default.join(resolved, "go.mod")) || import_fs4.default.existsSync(import_path13.default.join(resolved, "main.go"))) {
|
|
3460
|
+
return detectGo(resolved);
|
|
3461
|
+
}
|
|
3462
|
+
if (import_fs4.default.existsSync(import_path13.default.join(resolved, "deno.json")) || import_fs4.default.existsSync(import_path13.default.join(resolved, "deno.jsonc"))) {
|
|
3463
|
+
return detectDeno(resolved);
|
|
3464
|
+
}
|
|
3465
|
+
if (hasStaticSignals(resolved)) {
|
|
3466
|
+
return detectStatic(resolved);
|
|
3467
|
+
}
|
|
3468
|
+
throw new Error(
|
|
3469
|
+
"Could not detect project type. Expected package.json, requirements.txt, pyproject.toml, Pipfile, go.mod, deno.json, or index.html."
|
|
3470
|
+
);
|
|
3471
|
+
}
|
|
3472
|
+
function planToManifest(plan, projectPath) {
|
|
3473
|
+
let setupCommand = plan.installCommand;
|
|
3474
|
+
if (plan.buildCommand) {
|
|
3475
|
+
setupCommand += ` && ${plan.buildCommand}`;
|
|
3476
|
+
}
|
|
3477
|
+
let startCommand = plan.startCommand;
|
|
3478
|
+
if (plan.runtime === "node") {
|
|
3479
|
+
const nodeVersion = detectNodeVersion(projectPath);
|
|
3480
|
+
setupCommand = NVM_SETUP_PREFIX.replace(/\{NODE_VERSION\}/g, nodeVersion) + setupCommand;
|
|
3481
|
+
startCommand = NVM_RUN_PREFIX.replace(/\{NODE_VERSION\}/g, nodeVersion) + startCommand;
|
|
3482
|
+
}
|
|
3483
|
+
return {
|
|
3484
|
+
path: import_path13.default.resolve(projectPath),
|
|
3485
|
+
schema_version: 1,
|
|
3486
|
+
slug: plan.slug,
|
|
3487
|
+
title: plan.slug,
|
|
3488
|
+
version: "0.0.0",
|
|
3489
|
+
summary: "",
|
|
3490
|
+
category: "deploy",
|
|
3491
|
+
runtime: plan.runtime,
|
|
3492
|
+
deploy: { kind: "upload_and_run" },
|
|
3493
|
+
vm: {
|
|
3494
|
+
memory_mb: 2048,
|
|
3495
|
+
vcpu_count: 2,
|
|
3496
|
+
timeout_seconds: 3600
|
|
3497
|
+
},
|
|
3498
|
+
app: {
|
|
3499
|
+
port: plan.port,
|
|
3500
|
+
healthcheck_path: plan.healthPath,
|
|
3501
|
+
share_public_default: true,
|
|
3502
|
+
readiness_timeout_seconds: 120
|
|
3503
|
+
},
|
|
3504
|
+
run: {
|
|
3505
|
+
workdir: ".",
|
|
3506
|
+
start_command: startCommand
|
|
3507
|
+
},
|
|
3508
|
+
secrets: plan.secrets,
|
|
3509
|
+
post_deploy_notes: [],
|
|
3510
|
+
source: {
|
|
3511
|
+
include: ["*"],
|
|
3512
|
+
exclude: plan.runtime === "static" ? [] : ["dist", "dist/*", "build", "build/*"],
|
|
3513
|
+
setup_command: setupCommand
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
function detectNode(projectPath) {
|
|
3518
|
+
const pkgPath = import_path13.default.join(projectPath, "package.json");
|
|
3519
|
+
let pkg;
|
|
3520
|
+
try {
|
|
3521
|
+
pkg = JSON.parse(import_fs4.default.readFileSync(pkgPath, "utf-8"));
|
|
3522
|
+
} catch (err) {
|
|
3523
|
+
throw new Error(`Cannot read package.json: ${err}`);
|
|
3524
|
+
}
|
|
3525
|
+
const pm = detectNodePm(projectPath, pkg);
|
|
3526
|
+
const scripts = pkg.scripts || {};
|
|
3527
|
+
const deps = readNodeDeps(pkg);
|
|
3528
|
+
let installCommand;
|
|
3529
|
+
if (pm === "npm" && import_fs4.default.existsSync(import_path13.default.join(projectPath, "package-lock.json"))) {
|
|
3530
|
+
installCommand = "npm ci";
|
|
3531
|
+
} else if (pm === "yarn") {
|
|
3532
|
+
installCommand = wrapNodePmCommand(pm, isYarnBerry(projectPath, pkg) ? "yarn install --check-cache" : "yarn install --frozen-lockfile");
|
|
3533
|
+
} else if (pm === "pnpm") {
|
|
3534
|
+
installCommand = wrapNodePmCommand(pm, "pnpm install");
|
|
3535
|
+
} else {
|
|
3536
|
+
installCommand = `${pm} install`;
|
|
3537
|
+
}
|
|
3538
|
+
let buildCommand = null;
|
|
3539
|
+
if (scripts.build) {
|
|
3540
|
+
buildCommand = wrapNodePmCommand(pm, `${pm} run build`);
|
|
3541
|
+
}
|
|
3542
|
+
let startCommand = null;
|
|
3543
|
+
if (scripts.start) {
|
|
3544
|
+
startCommand = wrapNodePmCommand(pm, `${pm} start`);
|
|
3545
|
+
} else if (scripts.serve) {
|
|
3546
|
+
startCommand = wrapNodePmCommand(pm, `${pm} run serve`);
|
|
3547
|
+
} else if (pkg.main) {
|
|
3548
|
+
startCommand = `node ${shellQuote(String(pkg.main))}`;
|
|
3549
|
+
}
|
|
3550
|
+
if (!startCommand) {
|
|
3551
|
+
throw new Error(
|
|
3552
|
+
"Could not detect start command. Add a 'start' script to package.json or use --start-command."
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
let port = detectPortFromStartScript(scripts.start || "");
|
|
3556
|
+
if (port == null) {
|
|
3557
|
+
port = detectPortFromSource(projectPath, NODE_SOURCE_CANDIDATES);
|
|
3558
|
+
}
|
|
3559
|
+
if (port == null) {
|
|
3560
|
+
port = 3e3;
|
|
3561
|
+
}
|
|
3562
|
+
return {
|
|
3563
|
+
slug: slugify(pkg.name || import_path13.default.basename(projectPath)),
|
|
3564
|
+
runtime: "node",
|
|
3565
|
+
packageManager: pm,
|
|
3566
|
+
installCommand,
|
|
3567
|
+
buildCommand,
|
|
3568
|
+
startCommand,
|
|
3569
|
+
port,
|
|
3570
|
+
healthPath: detectHealthPath(projectPath),
|
|
3571
|
+
secrets: collectSecrets(projectPath, deps, "node")
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
function detectNodePm(projectPath, pkg) {
|
|
3575
|
+
const pmField = String(pkg?.packageManager || "").trim();
|
|
3576
|
+
if (pmField) {
|
|
3577
|
+
const pmName = pmField.split("@")[0].trim().toLowerCase();
|
|
3578
|
+
if (["pnpm", "yarn", "bun", "npm"].includes(pmName)) return pmName;
|
|
3579
|
+
}
|
|
3580
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
3581
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "yarn.lock"))) return "yarn";
|
|
3582
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "bun.lockb")) || import_fs4.default.existsSync(import_path13.default.join(projectPath, "bun.lock"))) return "bun";
|
|
3583
|
+
return "npm";
|
|
3584
|
+
}
|
|
3585
|
+
function isYarnBerry(projectPath, pkg) {
|
|
3586
|
+
const pmField = String(pkg?.packageManager || "").trim();
|
|
3587
|
+
if (pmField.toLowerCase().startsWith("yarn@")) {
|
|
3588
|
+
const versionPart = pmField.split("@")[1].split("+")[0];
|
|
3589
|
+
const major = versionPart.split(".")[0];
|
|
3590
|
+
if (/^\d+$/.test(major) && parseInt(major, 10) >= 2) return true;
|
|
3591
|
+
}
|
|
3592
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, ".yarnrc.yml")) || import_fs4.default.existsSync(import_path13.default.join(projectPath, ".yarnrc.yaml"))) return true;
|
|
3593
|
+
return false;
|
|
3594
|
+
}
|
|
3595
|
+
function wrapNodePmCommand(pm, command) {
|
|
3596
|
+
if (pm === "yarn" || pm === "pnpm") {
|
|
3597
|
+
return `${COREPACK_ENABLE_PREFIX}${command}`;
|
|
3598
|
+
}
|
|
3599
|
+
return command;
|
|
3600
|
+
}
|
|
3601
|
+
function readNodeDeps(pkg) {
|
|
3602
|
+
const deps = [];
|
|
3603
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
3604
|
+
if (pkg[section]) {
|
|
3605
|
+
deps.push(...Object.keys(pkg[section]));
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
return deps;
|
|
3609
|
+
}
|
|
3610
|
+
function detectNodeVersion(projectPath) {
|
|
3611
|
+
const pkgPath = import_path13.default.join(projectPath, "package.json");
|
|
3612
|
+
try {
|
|
3613
|
+
const pkg = JSON.parse(import_fs4.default.readFileSync(pkgPath, "utf-8"));
|
|
3614
|
+
const nodeConstraint = pkg?.engines?.node || "";
|
|
3615
|
+
const m = nodeConstraint.match(/(\d+)/);
|
|
3616
|
+
if (m) return m[1];
|
|
3617
|
+
} catch {
|
|
3618
|
+
}
|
|
3619
|
+
return "22";
|
|
3620
|
+
}
|
|
3621
|
+
function detectPython(projectPath) {
|
|
3622
|
+
const deps = readPythonDeps(projectPath);
|
|
3623
|
+
const pm = detectPythonPm(projectPath);
|
|
3624
|
+
let installCommand;
|
|
3625
|
+
if (pm === "poetry") {
|
|
3626
|
+
installCommand = "poetry install";
|
|
3627
|
+
} else if (pm === "uv") {
|
|
3628
|
+
installCommand = "uv sync";
|
|
3629
|
+
} else if (pm === "pdm") {
|
|
3630
|
+
installCommand = "pdm install";
|
|
3631
|
+
} else if (pm === "pipenv") {
|
|
3632
|
+
installCommand = import_fs4.default.existsSync(import_path13.default.join(projectPath, "Pipfile.lock")) ? "pipenv install --deploy" : "pipenv install";
|
|
3633
|
+
} else if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "requirements.txt"))) {
|
|
3634
|
+
installCommand = "pip install -r requirements.txt";
|
|
3635
|
+
} else {
|
|
3636
|
+
installCommand = "pip install .";
|
|
3637
|
+
}
|
|
3638
|
+
const depNamesLower = new Set(
|
|
3639
|
+
deps.map((d) => d.toLowerCase().split("==")[0].split(">=")[0].split("[")[0].trim())
|
|
3640
|
+
);
|
|
3641
|
+
const appModule = findPythonAppModule(projectPath);
|
|
3642
|
+
let startCommand = null;
|
|
3643
|
+
const port = detectPythonPort(projectPath);
|
|
3644
|
+
if (depNamesLower.has("django") && import_fs4.default.existsSync(import_path13.default.join(projectPath, "manage.py"))) {
|
|
3645
|
+
const wsgiModule = findDjangoWsgi(projectPath);
|
|
3646
|
+
if (depNamesLower.has("gunicorn")) {
|
|
3647
|
+
startCommand = `python manage.py migrate && gunicorn ${wsgiModule}:application -b 0.0.0.0:${port}`;
|
|
3648
|
+
} else {
|
|
3649
|
+
startCommand = `python manage.py migrate && python manage.py runserver 0.0.0.0:${port}`;
|
|
3650
|
+
}
|
|
3651
|
+
} else if (depNamesLower.has("uvicorn") || depNamesLower.has("python-fasthtml") || depNamesLower.has("fasthtml")) {
|
|
3652
|
+
startCommand = `python -m uvicorn ${appModule}:app --host 0.0.0.0 --port ${port}`;
|
|
3653
|
+
} else if (depNamesLower.has("gunicorn")) {
|
|
3654
|
+
startCommand = `gunicorn ${appModule}:app -b 0.0.0.0:${port}`;
|
|
3655
|
+
} else if (depNamesLower.has("flask")) {
|
|
3656
|
+
startCommand = `python -m flask run --host 0.0.0.0 --port ${port}`;
|
|
3657
|
+
} else if (depNamesLower.has("streamlit")) {
|
|
3658
|
+
const entry = findStreamlitEntry(projectPath);
|
|
3659
|
+
startCommand = `python -m streamlit run ${entry} --server.port ${port} --server.address 0.0.0.0`;
|
|
3660
|
+
}
|
|
3661
|
+
if (!startCommand) {
|
|
3662
|
+
throw new Error(
|
|
3663
|
+
"Could not detect start command. No django, uvicorn, gunicorn, flask, fasthtml, or streamlit found in dependencies. Use --start-command to specify how to start the app."
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
return {
|
|
3667
|
+
slug: slugify(import_path13.default.basename(projectPath)),
|
|
3668
|
+
runtime: "python",
|
|
3669
|
+
packageManager: pm,
|
|
3670
|
+
installCommand,
|
|
3671
|
+
buildCommand: null,
|
|
3672
|
+
startCommand,
|
|
3673
|
+
port,
|
|
3674
|
+
healthPath: detectHealthPath(projectPath),
|
|
3675
|
+
secrets: collectSecrets(projectPath, [...depNamesLower], "python")
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
function detectPythonPm(projectPath) {
|
|
3679
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "uv.lock"))) return "uv";
|
|
3680
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "pdm.lock"))) return "pdm";
|
|
3681
|
+
const pyprojectPath = import_path13.default.join(projectPath, "pyproject.toml");
|
|
3682
|
+
if (import_fs4.default.existsSync(pyprojectPath)) {
|
|
3683
|
+
try {
|
|
3684
|
+
const text = import_fs4.default.readFileSync(pyprojectPath, "utf-8");
|
|
3685
|
+
if (text.includes("[tool.uv]")) return "uv";
|
|
3686
|
+
if (text.includes("[tool.poetry]")) return "poetry";
|
|
3687
|
+
} catch {
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "Pipfile")) || import_fs4.default.existsSync(import_path13.default.join(projectPath, "Pipfile.lock"))) return "pipenv";
|
|
3691
|
+
return "pip";
|
|
3692
|
+
}
|
|
3693
|
+
function readPythonDeps(projectPath) {
|
|
3694
|
+
const reqPath = import_path13.default.join(projectPath, "requirements.txt");
|
|
3695
|
+
if (import_fs4.default.existsSync(reqPath)) {
|
|
3696
|
+
try {
|
|
3697
|
+
const lines = import_fs4.default.readFileSync(reqPath, "utf-8").split("\n");
|
|
3698
|
+
const deps = lines.map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
3699
|
+
if (deps.length > 0) return deps;
|
|
3700
|
+
} catch {
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
const pyprojectPath = import_path13.default.join(projectPath, "pyproject.toml");
|
|
3704
|
+
if (import_fs4.default.existsSync(pyprojectPath)) {
|
|
3705
|
+
try {
|
|
3706
|
+
const text = import_fs4.default.readFileSync(pyprojectPath, "utf-8");
|
|
3707
|
+
return parsePyprojectDeps(text);
|
|
3708
|
+
} catch {
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
const pipfilePath = import_path13.default.join(projectPath, "Pipfile");
|
|
3712
|
+
if (import_fs4.default.existsSync(pipfilePath)) {
|
|
3713
|
+
return parsePipfileDeps(pipfilePath);
|
|
3714
|
+
}
|
|
3715
|
+
return [];
|
|
3716
|
+
}
|
|
3717
|
+
function parsePyprojectDeps(text) {
|
|
3718
|
+
const deps = [];
|
|
3719
|
+
const inlineMatch = text.match(/(?<![a-z-])dependencies\s*=\s*\[([^\]]*)\]/);
|
|
3720
|
+
if (inlineMatch) {
|
|
3721
|
+
for (const item of inlineMatch[1].split(",")) {
|
|
3722
|
+
const cleaned = item.trim().replace(/^["']|["']$/g, "").trim();
|
|
3723
|
+
if (cleaned) deps.push(cleaned);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
const lines = text.split("\n");
|
|
3727
|
+
let inPoetryDeps = false;
|
|
3728
|
+
for (const line of lines) {
|
|
3729
|
+
const stripped = line.trim();
|
|
3730
|
+
if (stripped === "[tool.poetry.dependencies]") {
|
|
3731
|
+
inPoetryDeps = true;
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
if (inPoetryDeps) {
|
|
3735
|
+
if (stripped.startsWith("[")) break;
|
|
3736
|
+
if (stripped.includes("=") && !stripped.startsWith("#")) {
|
|
3737
|
+
const key = stripped.split("=")[0].trim();
|
|
3738
|
+
if (key && key !== "python") deps.push(key);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
return deps;
|
|
3743
|
+
}
|
|
3744
|
+
function parsePipfileDeps(pipfilePath) {
|
|
3745
|
+
const deps = [];
|
|
3746
|
+
let inPackages = false;
|
|
3747
|
+
try {
|
|
3748
|
+
const text = import_fs4.default.readFileSync(pipfilePath, "utf-8");
|
|
3749
|
+
for (const line of text.split("\n")) {
|
|
3750
|
+
const stripped = line.trim();
|
|
3751
|
+
if (stripped === "[packages]") {
|
|
3752
|
+
inPackages = true;
|
|
3753
|
+
continue;
|
|
3754
|
+
}
|
|
3755
|
+
if (inPackages) {
|
|
3756
|
+
if (stripped.startsWith("[")) break;
|
|
3757
|
+
if (stripped.includes("=") && !stripped.startsWith("#")) {
|
|
3758
|
+
const key = stripped.split("=")[0].trim();
|
|
3759
|
+
if (key) deps.push(key);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
} catch {
|
|
3764
|
+
}
|
|
3765
|
+
return deps;
|
|
3766
|
+
}
|
|
3767
|
+
function findPythonAppModule(projectPath) {
|
|
3768
|
+
for (const candidate of PYTHON_APP_CANDIDATES) {
|
|
3769
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, candidate))) {
|
|
3770
|
+
return candidate.replace(/\//g, ".").replace(/\.py$/, "");
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
return "app";
|
|
3774
|
+
}
|
|
3775
|
+
function findStreamlitEntry(projectPath) {
|
|
3776
|
+
for (const name of ["app.py", "main.py", "streamlit_app.py"]) {
|
|
3777
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, name))) return name;
|
|
3778
|
+
}
|
|
3779
|
+
return "app.py";
|
|
3780
|
+
}
|
|
3781
|
+
function findDjangoWsgi(projectPath) {
|
|
3782
|
+
const entries = import_fs4.default.readdirSync(projectPath, { withFileTypes: true });
|
|
3783
|
+
for (const entry of entries) {
|
|
3784
|
+
if (entry.isDirectory()) {
|
|
3785
|
+
const settingsPath = import_path13.default.join(projectPath, entry.name, "settings.py");
|
|
3786
|
+
if (import_fs4.default.existsSync(settingsPath)) {
|
|
3787
|
+
try {
|
|
3788
|
+
const text = import_fs4.default.readFileSync(settingsPath, "utf-8");
|
|
3789
|
+
const m = text.match(DJANGO_WSGI_RE);
|
|
3790
|
+
if (m) return m[1];
|
|
3791
|
+
} catch {
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
return `${import_path13.default.basename(projectPath)}.wsgi`;
|
|
3798
|
+
}
|
|
3799
|
+
function detectGo(projectPath) {
|
|
3800
|
+
let buildTarget = ".";
|
|
3801
|
+
const rootGoFiles = import_fs4.default.readdirSync(projectPath).filter((f) => f.endsWith(".go"));
|
|
3802
|
+
if (rootGoFiles.length > 0) {
|
|
3803
|
+
buildTarget = ".";
|
|
3804
|
+
} else {
|
|
3805
|
+
const cmdDir = import_path13.default.join(projectPath, "cmd");
|
|
3806
|
+
if (import_fs4.default.existsSync(cmdDir) && import_fs4.default.statSync(cmdDir).isDirectory()) {
|
|
3807
|
+
const subdirs = import_fs4.default.readdirSync(cmdDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
3808
|
+
if (subdirs.length > 0) {
|
|
3809
|
+
buildTarget = `./cmd/${subdirs[0]}`;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
const goVersion = detectGoVersion(projectPath);
|
|
3814
|
+
const buildCommand = `export GOROOT="$HOME/.local/go" && export PATH="$GOROOT/bin:$PATH" && CGO_ENABLED=0 go build -ldflags='-w -s' -o app ${buildTarget}`;
|
|
3815
|
+
const installCommand = `GO_VERSION="${goVersion}" && case "$GO_VERSION" in *.*.*) ;; *) GO_VERSION="$GO_VERSION.0" ;; esac && export GOROOT="$HOME/.local/go" && export PATH="$GOROOT/bin:$PATH" && if [ ! -x "$GOROOT/bin/go" ] || ! "$GOROOT/bin/go" version | grep -q "go$GO_VERSION"; then rm -rf "$GOROOT" && mkdir -p "$HOME/.local" && curl -fsSL "https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz" -o /tmp/instavm-go.tgz && tar -C "$HOME/.local" -xzf /tmp/instavm-go.tgz; fi && go mod download`;
|
|
3816
|
+
const port = detectGoPort(projectPath);
|
|
3817
|
+
const healthPath = detectGoHealthPath(projectPath);
|
|
3818
|
+
const secrets = collectSecrets(projectPath, [], "go");
|
|
3819
|
+
return {
|
|
3820
|
+
slug: slugify(import_path13.default.basename(projectPath)),
|
|
3821
|
+
runtime: "go",
|
|
3822
|
+
packageManager: "go",
|
|
3823
|
+
installCommand,
|
|
3824
|
+
buildCommand,
|
|
3825
|
+
startCommand: "./app",
|
|
3826
|
+
port,
|
|
3827
|
+
healthPath,
|
|
3828
|
+
secrets
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
function detectGoPort(projectPath) {
|
|
3832
|
+
for (const candidate of GO_SOURCE_CANDIDATES) {
|
|
3833
|
+
const full = import_path13.default.join(projectPath, candidate);
|
|
3834
|
+
if (!import_fs4.default.existsSync(full)) continue;
|
|
3835
|
+
try {
|
|
3836
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
3837
|
+
const m = text.match(GO_PORT_RE);
|
|
3838
|
+
if (m) return parseInt(m[1], 10);
|
|
3839
|
+
const m2 = text.match(PORT_PATTERN_RE);
|
|
3840
|
+
if (m2) return parseInt(m2[1], 10);
|
|
3841
|
+
} catch {
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
try {
|
|
3846
|
+
for (const f of import_fs4.default.readdirSync(projectPath)) {
|
|
3847
|
+
if (!f.endsWith(".go")) continue;
|
|
3848
|
+
const text = import_fs4.default.readFileSync(import_path13.default.join(projectPath, f), "utf-8");
|
|
3849
|
+
const m = text.match(GO_PORT_RE);
|
|
3850
|
+
if (m) return parseInt(m[1], 10);
|
|
3851
|
+
}
|
|
3852
|
+
} catch {
|
|
3853
|
+
}
|
|
3854
|
+
return 8080;
|
|
3855
|
+
}
|
|
3856
|
+
function detectGoVersion(projectPath) {
|
|
3857
|
+
const goModPath = import_path13.default.join(projectPath, "go.mod");
|
|
3858
|
+
if (!import_fs4.default.existsSync(goModPath)) return "1.23.0";
|
|
3859
|
+
try {
|
|
3860
|
+
const text = import_fs4.default.readFileSync(goModPath, "utf-8");
|
|
3861
|
+
const match = text.match(/^go\s+([0-9]+(?:\.[0-9]+){1,2})\s*$/m);
|
|
3862
|
+
return match?.[1] || "1.23.0";
|
|
3863
|
+
} catch {
|
|
3864
|
+
return "1.23.0";
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
function detectGoHealthPath(projectPath) {
|
|
3868
|
+
for (const candidate of GO_SOURCE_CANDIDATES) {
|
|
3869
|
+
const full = import_path13.default.join(projectPath, candidate);
|
|
3870
|
+
if (!import_fs4.default.existsSync(full)) continue;
|
|
3871
|
+
try {
|
|
3872
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
3873
|
+
const m = text.match(GO_HEALTH_RE);
|
|
3874
|
+
if (m) return m[1];
|
|
3875
|
+
} catch {
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
try {
|
|
3880
|
+
for (const f of import_fs4.default.readdirSync(projectPath)) {
|
|
3881
|
+
if (!f.endsWith(".go")) continue;
|
|
3882
|
+
const text = import_fs4.default.readFileSync(import_path13.default.join(projectPath, f), "utf-8");
|
|
3883
|
+
const m = text.match(GO_HEALTH_RE);
|
|
3884
|
+
if (m) return m[1];
|
|
3885
|
+
}
|
|
3886
|
+
} catch {
|
|
3887
|
+
}
|
|
3888
|
+
return "/";
|
|
3889
|
+
}
|
|
3890
|
+
function detectDeno(projectPath) {
|
|
3891
|
+
const mainFile = findDenoMain(projectPath);
|
|
3892
|
+
if (!mainFile) {
|
|
3893
|
+
throw new Error(
|
|
3894
|
+
"Could not detect start command. No main.ts, main.js, server.ts, server.js, app.ts, or app.js found. Use --start-command to specify how to start the app."
|
|
3895
|
+
);
|
|
3896
|
+
}
|
|
3897
|
+
let port = 8e3;
|
|
3898
|
+
const full = import_path13.default.join(projectPath, mainFile);
|
|
3899
|
+
if (import_fs4.default.existsSync(full)) {
|
|
3900
|
+
try {
|
|
3901
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
3902
|
+
const m = text.match(/port:\s*(\d{2,5})/);
|
|
3903
|
+
if (m) {
|
|
3904
|
+
port = parseInt(m[1], 10);
|
|
3905
|
+
} else {
|
|
3906
|
+
const m2 = text.match(PORT_PATTERN_RE);
|
|
3907
|
+
if (m2) port = parseInt(m2[1], 10);
|
|
3908
|
+
}
|
|
3909
|
+
} catch {
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
const secrets = collectSecrets(projectPath, [], "deno");
|
|
3913
|
+
return {
|
|
3914
|
+
slug: slugify(import_path13.default.basename(projectPath)),
|
|
3915
|
+
runtime: "deno",
|
|
3916
|
+
packageManager: "deno",
|
|
3917
|
+
installCommand: "curl -fsSL https://deno.land/install.sh | sh",
|
|
3918
|
+
buildCommand: null,
|
|
3919
|
+
startCommand: `$HOME/.deno/bin/deno run --allow-all ${mainFile}`,
|
|
3920
|
+
port,
|
|
3921
|
+
healthPath: "/",
|
|
3922
|
+
secrets
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
function findDenoMain(projectPath) {
|
|
3926
|
+
for (const candidate of DENO_MAIN_CANDIDATES) {
|
|
3927
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, candidate))) return candidate;
|
|
3928
|
+
}
|
|
3929
|
+
return null;
|
|
3930
|
+
}
|
|
3931
|
+
function hasStaticSignals(projectPath) {
|
|
3932
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "index.html"))) return true;
|
|
3933
|
+
return STATIC_ROOT_CANDIDATES.some(
|
|
3934
|
+
(d) => import_fs4.default.existsSync(import_path13.default.join(projectPath, d, "index.html"))
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
function findStaticRoot(projectPath) {
|
|
3938
|
+
for (const candidate of STATIC_ROOT_CANDIDATES) {
|
|
3939
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, candidate, "index.html"))) return candidate;
|
|
3940
|
+
}
|
|
3941
|
+
if (import_fs4.default.existsSync(import_path13.default.join(projectPath, "index.html"))) return ".";
|
|
3942
|
+
throw new Error(
|
|
3943
|
+
"Could not detect project type. Expected package.json, requirements.txt, pyproject.toml, Pipfile, go.mod, deno.json, or index.html."
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
function detectStatic(projectPath) {
|
|
3947
|
+
const rootDir = findStaticRoot(projectPath);
|
|
3948
|
+
const port = 3e3;
|
|
3949
|
+
return {
|
|
3950
|
+
slug: slugify(import_path13.default.basename(projectPath)),
|
|
3951
|
+
runtime: "static",
|
|
3952
|
+
packageManager: "none",
|
|
3953
|
+
installCommand: "echo 'No dependencies'",
|
|
3954
|
+
buildCommand: null,
|
|
3955
|
+
startCommand: `python3 -m http.server ${port} --directory ${rootDir} --bind 0.0.0.0`,
|
|
3956
|
+
port,
|
|
3957
|
+
healthPath: "/",
|
|
3958
|
+
secrets: []
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
function detectPortFromStartScript(script) {
|
|
3962
|
+
const m1 = script.match(/--port\s+(\d+)/);
|
|
3963
|
+
if (m1) return parseInt(m1[1], 10);
|
|
3964
|
+
const m2 = script.match(/-p\s+(\d+)/);
|
|
3965
|
+
if (m2) return parseInt(m2[1], 10);
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
function detectPortFromSource(projectPath, candidates) {
|
|
3969
|
+
for (const candidate of candidates) {
|
|
3970
|
+
const full = import_path13.default.join(projectPath, candidate);
|
|
3971
|
+
if (!import_fs4.default.existsSync(full)) continue;
|
|
3972
|
+
try {
|
|
3973
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
3974
|
+
const m = text.match(PORT_PATTERN_RE);
|
|
3975
|
+
if (m) return parseInt(m[1], 10);
|
|
3976
|
+
} catch {
|
|
3977
|
+
continue;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
return null;
|
|
3981
|
+
}
|
|
3982
|
+
function detectPythonPort(projectPath) {
|
|
3983
|
+
const visit = (dir) => {
|
|
3984
|
+
let entries;
|
|
3985
|
+
try {
|
|
3986
|
+
entries = import_fs4.default.readdirSync(dir, { withFileTypes: true });
|
|
3987
|
+
} catch {
|
|
3988
|
+
return null;
|
|
3989
|
+
}
|
|
3990
|
+
for (const entry of entries) {
|
|
3991
|
+
const full = import_path13.default.join(dir, entry.name);
|
|
3992
|
+
if (entry.isDirectory()) {
|
|
3993
|
+
if (shouldSkipFile(full, projectPath)) continue;
|
|
3994
|
+
const nested = visit(full);
|
|
3995
|
+
if (nested != null) return nested;
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
if (!entry.isFile() || !entry.name.endsWith(".py")) continue;
|
|
3999
|
+
if (shouldSkipFile(full, projectPath)) continue;
|
|
4000
|
+
try {
|
|
4001
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
4002
|
+
for (const pattern of PYTHON_PORT_PATTERNS) {
|
|
4003
|
+
const match = text.match(pattern);
|
|
4004
|
+
if (match) return parseInt(match[1], 10);
|
|
4005
|
+
}
|
|
4006
|
+
} catch {
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
return null;
|
|
4011
|
+
};
|
|
4012
|
+
return visit(projectPath) ?? 8e3;
|
|
4013
|
+
}
|
|
4014
|
+
function detectHealthPath(projectPath) {
|
|
4015
|
+
const allCandidates = [...PYTHON_APP_CANDIDATES, ...NODE_SOURCE_CANDIDATES];
|
|
4016
|
+
for (const candidate of allCandidates) {
|
|
4017
|
+
const full = import_path13.default.join(projectPath, candidate);
|
|
4018
|
+
if (!import_fs4.default.existsSync(full)) continue;
|
|
4019
|
+
try {
|
|
4020
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
4021
|
+
const m = text.match(HEALTH_ROUTE_RE);
|
|
4022
|
+
if (m) return m[1];
|
|
4023
|
+
} catch {
|
|
4024
|
+
continue;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
return "/health";
|
|
4028
|
+
}
|
|
4029
|
+
function collectSecrets(projectPath, deps, runtime) {
|
|
4030
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4031
|
+
const secrets = [];
|
|
4032
|
+
for (const dep of deps) {
|
|
4033
|
+
const depKey = dep.toLowerCase().split("==")[0].split(">=")[0].split("[")[0].trim();
|
|
4034
|
+
const hint = DEP_SECRET_HINTS[depKey];
|
|
4035
|
+
if (hint && !seen.has(hint[0])) {
|
|
4036
|
+
seen.add(hint[0]);
|
|
4037
|
+
secrets.push({ name: hint[0], prompt: hint[1], env_name: hint[0], required: true });
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
const scanned = scanEnvVars(projectPath, runtime);
|
|
4041
|
+
for (const varName of [...scanned].sort()) {
|
|
4042
|
+
if (!seen.has(varName) && !NON_SECRET_VARS.has(varName)) {
|
|
4043
|
+
seen.add(varName);
|
|
4044
|
+
secrets.push({ name: varName, prompt: varName, env_name: varName, required: false });
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return secrets;
|
|
4048
|
+
}
|
|
4049
|
+
function scanEnvVars(projectPath, runtime) {
|
|
4050
|
+
const found = /* @__PURE__ */ new Set();
|
|
4051
|
+
let extensions;
|
|
4052
|
+
let pattern;
|
|
4053
|
+
if (runtime === "python") {
|
|
4054
|
+
extensions = [".py"];
|
|
4055
|
+
pattern = PYTHON_ENV_RE;
|
|
4056
|
+
} else if (runtime === "go") {
|
|
4057
|
+
extensions = [".go"];
|
|
4058
|
+
pattern = GO_ENV_RE;
|
|
4059
|
+
} else if (runtime === "deno") {
|
|
4060
|
+
extensions = [".ts", ".js", ".mjs", ".mts"];
|
|
4061
|
+
pattern = DENO_ENV_RE;
|
|
4062
|
+
} else {
|
|
4063
|
+
extensions = [".js", ".ts", ".mjs", ".tsx", ".jsx"];
|
|
4064
|
+
pattern = NODE_ENV_RE;
|
|
4065
|
+
}
|
|
4066
|
+
const scanDir = (dir) => {
|
|
4067
|
+
let entries;
|
|
4068
|
+
try {
|
|
4069
|
+
entries = import_fs4.default.readdirSync(dir, { withFileTypes: true });
|
|
4070
|
+
} catch {
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
for (const entry of entries) {
|
|
4074
|
+
const full = import_path13.default.join(dir, entry.name);
|
|
4075
|
+
if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
4076
|
+
if (shouldSkipFile(full, projectPath)) continue;
|
|
4077
|
+
try {
|
|
4078
|
+
const text = import_fs4.default.readFileSync(full, "utf-8");
|
|
4079
|
+
let m;
|
|
4080
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
4081
|
+
while ((m = re.exec(text)) !== null) {
|
|
4082
|
+
const varName = m[1] || m[2];
|
|
4083
|
+
if (varName) found.add(varName);
|
|
4084
|
+
}
|
|
4085
|
+
} catch {
|
|
4086
|
+
continue;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
scanDir(projectPath);
|
|
4092
|
+
for (const subdir of ["src", "server", "api", "lib", "cmd"]) {
|
|
4093
|
+
const sub = import_path13.default.join(projectPath, subdir);
|
|
4094
|
+
if (import_fs4.default.existsSync(sub) && import_fs4.default.statSync(sub).isDirectory()) {
|
|
4095
|
+
scanDir(sub);
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
return found;
|
|
4099
|
+
}
|
|
4100
|
+
function shouldSkipFile(filePath, projectRoot) {
|
|
4101
|
+
const rel = import_path13.default.relative(projectRoot, filePath);
|
|
4102
|
+
const parts = rel.split(import_path13.default.sep);
|
|
4103
|
+
const skipDirs = /* @__PURE__ */ new Set(["node_modules", ".venv", "venv", "__pycache__", ".git", "dist", "build", ".next"]);
|
|
4104
|
+
return parts.some((p) => skipDirs.has(p));
|
|
4105
|
+
}
|
|
4106
|
+
function slugify(name) {
|
|
4107
|
+
const slug = name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4108
|
+
return slug || "app";
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
// src/cli.ts
|
|
4112
|
+
var defaultDeps = {
|
|
4113
|
+
stdout: process.stdout,
|
|
4114
|
+
stderr: process.stderr,
|
|
4115
|
+
spawnSync: import_child_process.spawnSync,
|
|
4116
|
+
resolveRuntimeConfig,
|
|
4117
|
+
updateProfileSettings,
|
|
4118
|
+
clientFactory: (apiKey, options) => new InstaVM(apiKey, options),
|
|
4119
|
+
promptSecret,
|
|
4120
|
+
readStdin,
|
|
4121
|
+
writeFile: (outputPath, content) => {
|
|
4122
|
+
import_fs5.default.writeFileSync(outputPath, content);
|
|
4123
|
+
}
|
|
4124
|
+
};
|
|
4125
|
+
var ACTIVE_VM_STATUSES = /* @__PURE__ */ new Set(["active", "running", "creating"]);
|
|
4126
|
+
var INTERACTIVE_PROGRESS_PREFIXES = ["Resolving public snapshot ", "Reusing saved snapshot ", "Retrying VM creation "];
|
|
4127
|
+
var INTERACTIVE_PROGRESS_STEPS = /* @__PURE__ */ new Set([
|
|
4128
|
+
"Creating VM",
|
|
4129
|
+
"Waiting for SSH access",
|
|
4130
|
+
"Uploading cookbook source",
|
|
4131
|
+
"Uploading source",
|
|
4132
|
+
"Running cookbook setup",
|
|
4133
|
+
"Running setup",
|
|
4134
|
+
"Starting application service",
|
|
4135
|
+
"Starting service",
|
|
4136
|
+
"Waiting for application healthcheck",
|
|
4137
|
+
"Waiting for healthcheck",
|
|
4138
|
+
"Creating public share",
|
|
4139
|
+
"Verifying public share URL",
|
|
4140
|
+
"Detecting project",
|
|
4141
|
+
"Creating snapshot"
|
|
4142
|
+
]);
|
|
4143
|
+
function printJson(deps, payload) {
|
|
4144
|
+
deps.stdout.write(`${JSON.stringify(payload)}
|
|
4145
|
+
`);
|
|
4146
|
+
}
|
|
4147
|
+
function printLines(deps, lines) {
|
|
4148
|
+
for (const line of lines) {
|
|
4149
|
+
deps.stdout.write(`${line}
|
|
4150
|
+
`);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
function printErrorLines(deps, lines) {
|
|
4154
|
+
for (const line of lines) {
|
|
4155
|
+
deps.stderr.write(`${line}
|
|
4156
|
+
`);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
function supportsAnsi(stream) {
|
|
4160
|
+
return Boolean(stream.isTTY) && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
4161
|
+
}
|
|
4162
|
+
function styleText(text, codes, enabled) {
|
|
4163
|
+
if (!enabled || codes.length === 0) {
|
|
4164
|
+
return text;
|
|
4165
|
+
}
|
|
4166
|
+
return `\x1B[${codes.join(";")}m${text}\x1B[0m`;
|
|
4167
|
+
}
|
|
4168
|
+
function titleCaseLabel(value) {
|
|
4169
|
+
return value.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
4170
|
+
}
|
|
4171
|
+
function displayValue(value, fallback = "-") {
|
|
4172
|
+
if (value === null || value === void 0) {
|
|
4173
|
+
return fallback;
|
|
4174
|
+
}
|
|
4175
|
+
const text = String(value).trim();
|
|
4176
|
+
return text || fallback;
|
|
4177
|
+
}
|
|
4178
|
+
function formatTable(headers, rows) {
|
|
4179
|
+
const widths = headers.map((header) => header.length);
|
|
4180
|
+
const normalizedRows = rows.map((row) => row.map((cell) => displayValue(cell)));
|
|
4181
|
+
for (const row of normalizedRows) {
|
|
4182
|
+
row.forEach((cell, index) => {
|
|
4183
|
+
widths[index] = Math.max(widths[index], cell.length);
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
const formatRow = (row) => ` ${row.map((cell, index) => cell.padEnd(widths[index], " ")).join(" ")}`;
|
|
4187
|
+
const separator = ` ${widths.map((width) => "-".repeat(width)).join(" ")}`;
|
|
4188
|
+
return [formatRow(headers), separator, ...normalizedRows.map((row) => formatRow(row))];
|
|
4189
|
+
}
|
|
4190
|
+
function formatCookbookList(cookbooks) {
|
|
4191
|
+
if (cookbooks.length === 0) {
|
|
4192
|
+
return ["No cookbooks found."];
|
|
4193
|
+
}
|
|
4194
|
+
const categoryOrder = /* @__PURE__ */ new Map([
|
|
4195
|
+
["web", 0],
|
|
4196
|
+
["graphics", 1],
|
|
4197
|
+
["agents", 2]
|
|
4198
|
+
]);
|
|
4199
|
+
const sorted = [...cookbooks].sort((left, right) => {
|
|
4200
|
+
const leftCategory = String(left.category || "other");
|
|
4201
|
+
const rightCategory = String(right.category || "other");
|
|
4202
|
+
const categoryRankCompare = (categoryOrder.get(leftCategory) ?? 99) - (categoryOrder.get(rightCategory) ?? 99);
|
|
4203
|
+
if (categoryRankCompare !== 0) {
|
|
4204
|
+
return categoryRankCompare;
|
|
4205
|
+
}
|
|
4206
|
+
const categoryCompare = leftCategory.localeCompare(rightCategory);
|
|
4207
|
+
if (categoryCompare !== 0) {
|
|
4208
|
+
return categoryCompare;
|
|
4209
|
+
}
|
|
4210
|
+
const secretCompare = Number(Boolean(left.requires_secrets)) - Number(Boolean(right.requires_secrets));
|
|
4211
|
+
if (secretCompare !== 0) {
|
|
4212
|
+
return secretCompare;
|
|
4213
|
+
}
|
|
4214
|
+
return String(left.title || left.slug || "").localeCompare(String(right.title || right.slug || ""));
|
|
4215
|
+
});
|
|
4216
|
+
const featured = sorted.filter((entry) => !entry.requires_secrets);
|
|
4217
|
+
const titleWidth = Math.min(
|
|
4218
|
+
20,
|
|
4219
|
+
Math.max(...sorted.map((entry) => String(entry.title || "").length), "Title".length)
|
|
4220
|
+
);
|
|
4221
|
+
const slugWidth = Math.max(...sorted.map((entry) => String(entry.slug || "").length), "Slug".length);
|
|
4222
|
+
const renderEntry = (entry) => {
|
|
4223
|
+
const title = String(entry.title || "").padEnd(titleWidth, " ");
|
|
4224
|
+
const slug = String(entry.slug || "").padEnd(slugWidth, " ");
|
|
4225
|
+
const secretState = entry.requires_secrets ? "key" : "no key";
|
|
4226
|
+
return ` ${title} ${slug} :${entry.port} ${secretState}`;
|
|
4227
|
+
};
|
|
4228
|
+
const lines = [`Cookbooks (${cookbooks.length})`];
|
|
4229
|
+
if (featured.length > 0) {
|
|
4230
|
+
lines.push("", "Try first");
|
|
4231
|
+
for (const entry of featured) {
|
|
4232
|
+
lines.push(renderEntry(entry));
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
let currentCategory = null;
|
|
4236
|
+
for (const entry of sorted) {
|
|
4237
|
+
if (!entry.requires_secrets) {
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
4240
|
+
const category = String(entry.category || "other");
|
|
4241
|
+
if (category !== currentCategory) {
|
|
4242
|
+
lines.push("", titleCaseLabel(category));
|
|
4243
|
+
currentCategory = category;
|
|
4244
|
+
}
|
|
4245
|
+
lines.push(renderEntry(entry));
|
|
4246
|
+
}
|
|
4247
|
+
lines.push("", "Use `instavm cookbook info <slug>` for details.", "Use `instavm cookbook deploy <slug>` to launch one.");
|
|
4248
|
+
return lines;
|
|
4249
|
+
}
|
|
4250
|
+
function formatWhoamiOutput(payload) {
|
|
4251
|
+
const sshKeys = Array.isArray(payload.ssh_keys) ? payload.ssh_keys : [];
|
|
4252
|
+
const lines = [
|
|
4253
|
+
"Account",
|
|
4254
|
+
` Email: ${displayValue(payload.email)}`,
|
|
4255
|
+
` User ID: ${displayValue(payload.id)}`,
|
|
4256
|
+
` Name: ${displayValue(payload.name)}`,
|
|
4257
|
+
` Verified: ${payload.is_verified ? "yes" : "no"}`,
|
|
4258
|
+
"",
|
|
4259
|
+
`SSH Keys (${sshKeys.length})`
|
|
4260
|
+
];
|
|
4261
|
+
if (sshKeys.length > 0) {
|
|
4262
|
+
lines.push(...formatTable(
|
|
4263
|
+
["ID", "Fingerprint", "Comment"],
|
|
4264
|
+
sshKeys.map((key) => [
|
|
4265
|
+
displayValue(key.id),
|
|
4266
|
+
displayValue(key.fingerprint),
|
|
4267
|
+
displayValue(key.comment)
|
|
4268
|
+
])
|
|
4269
|
+
));
|
|
4270
|
+
} else {
|
|
4271
|
+
lines.push(" none");
|
|
4272
|
+
}
|
|
4273
|
+
return lines;
|
|
4274
|
+
}
|
|
4275
|
+
function formatVmList(vms, includeAll) {
|
|
4276
|
+
return [
|
|
4277
|
+
includeAll ? `VMs (${vms.length})` : `Active VMs (${vms.length})`,
|
|
4278
|
+
...includeAll ? [] : ["Use `instavm ls --all` to include terminated VMs."],
|
|
4279
|
+
"",
|
|
4280
|
+
...formatTable(
|
|
4281
|
+
["ID", "Status", "Created", "SSH"],
|
|
4282
|
+
vms.map((vm) => [
|
|
4283
|
+
displayValue(vm.vm_id),
|
|
4284
|
+
displayValue(vm.status),
|
|
4285
|
+
displayValue(vm.created_at),
|
|
4286
|
+
displayValue(vm.ssh_host)
|
|
4287
|
+
])
|
|
4288
|
+
)
|
|
4289
|
+
];
|
|
4290
|
+
}
|
|
4291
|
+
function formatSnapshotList(snapshots) {
|
|
4292
|
+
return [
|
|
4293
|
+
`Snapshots (${snapshots.length})`,
|
|
4294
|
+
"",
|
|
4295
|
+
...formatTable(
|
|
4296
|
+
["ID", "Name", "Status", "Type"],
|
|
4297
|
+
snapshots.map((snapshot) => [
|
|
4298
|
+
displayValue(snapshot.id),
|
|
4299
|
+
displayValue(snapshot.name),
|
|
4300
|
+
displayValue(snapshot.status),
|
|
4301
|
+
displayValue(snapshot.type)
|
|
4302
|
+
])
|
|
4303
|
+
)
|
|
4304
|
+
];
|
|
4305
|
+
}
|
|
4306
|
+
function formatSshKeyList(keys) {
|
|
4307
|
+
return [
|
|
4308
|
+
`SSH Keys (${keys.length})`,
|
|
4309
|
+
"",
|
|
4310
|
+
...formatTable(
|
|
4311
|
+
["ID", "Fingerprint", "Comment"],
|
|
4312
|
+
keys.map((key) => [
|
|
4313
|
+
displayValue(key.id),
|
|
4314
|
+
displayValue(key.fingerprint),
|
|
4315
|
+
displayValue(key.comment)
|
|
4316
|
+
])
|
|
4317
|
+
)
|
|
4318
|
+
];
|
|
4319
|
+
}
|
|
4320
|
+
function formatVolumeList(volumes) {
|
|
4321
|
+
return [
|
|
4322
|
+
`Volumes (${volumes.length})`,
|
|
4323
|
+
"",
|
|
4324
|
+
...formatTable(
|
|
4325
|
+
["ID", "Name", "Usage", "Status"],
|
|
4326
|
+
volumes.map((entry) => [
|
|
4327
|
+
displayValue(entry.id),
|
|
4328
|
+
displayValue(entry.name),
|
|
4329
|
+
`${humanBytes(entry.used_bytes)} / ${humanBytes(entry.quota_bytes)}`,
|
|
4330
|
+
displayValue(entry.status)
|
|
4331
|
+
])
|
|
4332
|
+
)
|
|
4333
|
+
];
|
|
4334
|
+
}
|
|
4335
|
+
function formatCheckpointList(checkpoints) {
|
|
4336
|
+
return [
|
|
4337
|
+
`Checkpoints (${checkpoints.length})`,
|
|
4338
|
+
"",
|
|
4339
|
+
...formatTable(
|
|
4340
|
+
["ID", "Name", "Status"],
|
|
4341
|
+
checkpoints.map((entry) => [
|
|
4342
|
+
displayValue(entry.id),
|
|
4343
|
+
displayValue(entry.name),
|
|
4344
|
+
displayValue(entry.status)
|
|
4345
|
+
])
|
|
4346
|
+
)
|
|
4347
|
+
];
|
|
4348
|
+
}
|
|
4349
|
+
function formatFileList(files) {
|
|
4350
|
+
return [
|
|
4351
|
+
`Files (${files.length})`,
|
|
4352
|
+
"",
|
|
4353
|
+
...formatTable(
|
|
4354
|
+
["Path", "Size"],
|
|
4355
|
+
files.map((entry) => [
|
|
4356
|
+
displayValue(entry.path),
|
|
4357
|
+
humanBytes(entry.size)
|
|
4358
|
+
])
|
|
4359
|
+
)
|
|
4360
|
+
];
|
|
4361
|
+
}
|
|
4362
|
+
var ProgressRenderer = class {
|
|
4363
|
+
constructor(deps, options) {
|
|
4364
|
+
this.deps = deps;
|
|
4365
|
+
this.options = options;
|
|
4366
|
+
this.currentMessage = null;
|
|
4367
|
+
this.startedAt = 0;
|
|
4368
|
+
this.timer = null;
|
|
4369
|
+
this.frame = 0;
|
|
4370
|
+
this.enabled = !options.json && supportsAnsi(deps.stderr);
|
|
4371
|
+
}
|
|
4372
|
+
step(message) {
|
|
4373
|
+
if (this.options.json) {
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
if (!this.enabled) {
|
|
4377
|
+
this.note(`${message}...`);
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
if (message !== this.currentMessage) {
|
|
4381
|
+
this.startedAt = Date.now();
|
|
4382
|
+
this.frame = 0;
|
|
4383
|
+
}
|
|
4384
|
+
this.currentMessage = message;
|
|
4385
|
+
if (!this.timer) {
|
|
4386
|
+
this.timer = setInterval(() => this.render(), 180);
|
|
4387
|
+
this.render();
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
note(message) {
|
|
4391
|
+
if (this.options.json) {
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
if (!this.enabled) {
|
|
4395
|
+
this.deps.stderr.write(`${message}
|
|
4396
|
+
`);
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
this.currentMessage = null;
|
|
4400
|
+
this.startedAt = 0;
|
|
4401
|
+
this.clearLine();
|
|
4402
|
+
this.deps.stderr.write(`${message}
|
|
4403
|
+
`);
|
|
4404
|
+
}
|
|
4405
|
+
stop() {
|
|
4406
|
+
if (this.timer) {
|
|
4407
|
+
clearInterval(this.timer);
|
|
4408
|
+
this.timer = null;
|
|
4409
|
+
}
|
|
4410
|
+
if (this.enabled) {
|
|
4411
|
+
this.currentMessage = null;
|
|
4412
|
+
this.clearLine();
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
render() {
|
|
4416
|
+
if (!this.currentMessage) {
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4419
|
+
const suffixes = ["", ".", "..", "..."];
|
|
4420
|
+
const elapsed = this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0;
|
|
4421
|
+
this.deps.stderr.write(`\r\x1B[2K${this.currentMessage}${suffixes[this.frame % suffixes.length]} ${elapsed}s`);
|
|
4422
|
+
this.frame += 1;
|
|
4423
|
+
}
|
|
4424
|
+
clearLine() {
|
|
4425
|
+
this.deps.stderr.write("\r\x1B[2K");
|
|
4426
|
+
}
|
|
4427
|
+
};
|
|
4428
|
+
function filterActiveVms(vms) {
|
|
4429
|
+
return vms.filter((vm) => ACTIVE_VM_STATUSES.has(String(vm.status || "").toLowerCase()));
|
|
4430
|
+
}
|
|
4431
|
+
function normalizeArgv(argv) {
|
|
4432
|
+
return argv.map((token) => token === "-all" ? "--all" : token);
|
|
4433
|
+
}
|
|
4434
|
+
function emitOutput(deps, options, payload, textLines) {
|
|
4435
|
+
if (options.json) {
|
|
4436
|
+
printJson(deps, payload);
|
|
4437
|
+
} else {
|
|
4438
|
+
printLines(deps, textLines);
|
|
4439
|
+
}
|
|
4440
|
+
return 0;
|
|
4441
|
+
}
|
|
4442
|
+
function shouldSpin(message) {
|
|
4443
|
+
return INTERACTIVE_PROGRESS_STEPS.has(message) || INTERACTIVE_PROGRESS_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
4444
|
+
}
|
|
4445
|
+
function cookbookProgress(renderer, message) {
|
|
4446
|
+
if (shouldSpin(message)) {
|
|
4447
|
+
renderer.step(message);
|
|
4448
|
+
} else {
|
|
4449
|
+
renderer.note(message);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
function formatCookbookDeploySuccess(options, deps, payload) {
|
|
4453
|
+
const colorEnabled = !options.json && supportsAnsi(deps.stdout);
|
|
4454
|
+
const vmId = String(payload.vm_id || "");
|
|
4455
|
+
const slug = String(payload.slug || "");
|
|
4456
|
+
const displayUrl = String(payload.share_url || "").replace(/\/$/, "");
|
|
4457
|
+
const connectHint = vmId ? `instavm connect ${vmId}` : "-";
|
|
4458
|
+
const logsHint = slug ? `After connecting: tail -n 200 ~/.instavm-cookbooks/logs/${slug}.log` : "-";
|
|
4459
|
+
return [
|
|
4460
|
+
styleText("Cookbook ready", ["1", "32"], colorEnabled),
|
|
4461
|
+
"",
|
|
4462
|
+
` Cookbook ${slug}`,
|
|
4463
|
+
` VM ${vmId}`,
|
|
4464
|
+
` URL ${styleText(displayUrl, ["32", "4"], colorEnabled)}`,
|
|
4465
|
+
` Connect ${connectHint}`,
|
|
4466
|
+
` Logs ${logsHint}`,
|
|
4467
|
+
` Destroy ${payload.destroy_command}`,
|
|
4468
|
+
...payload.notes?.length ? ["", " Notes", ...payload.notes.map((note) => ` - ${note}`)] : []
|
|
4469
|
+
];
|
|
4470
|
+
}
|
|
4471
|
+
function formatCookbookDeployFailure(options, deps, message, payload) {
|
|
4472
|
+
const colorEnabled = !options.json && supportsAnsi(deps.stderr);
|
|
4473
|
+
const vmId = String(payload.vm_id || "");
|
|
4474
|
+
const slug = String(payload.slug || "");
|
|
4475
|
+
const connectHint = vmId ? `instavm connect ${vmId}` : "-";
|
|
4476
|
+
const logsHint = vmId && slug ? `After connecting: tail -n 200 ~/.instavm-cookbooks/logs/${slug}.log` : "-";
|
|
4477
|
+
return [
|
|
4478
|
+
styleText("Deploy failed", ["1", "31"], colorEnabled),
|
|
4479
|
+
` Error ${message}`,
|
|
4480
|
+
` VM ${vmId || "-"}`,
|
|
4481
|
+
` Connect ${connectHint}`,
|
|
4482
|
+
` Logs ${logsHint}`,
|
|
4483
|
+
` Destroy ${payload.destroy_command || "-"}`
|
|
4484
|
+
];
|
|
4485
|
+
}
|
|
4486
|
+
function emitCookbookStyleFailure(deps, options, error, commanderCode, fallbackPayload = {}) {
|
|
4487
|
+
if (!(error instanceof CookbookError)) {
|
|
4488
|
+
throw error;
|
|
4489
|
+
}
|
|
4490
|
+
const payload = {
|
|
4491
|
+
status: "failed",
|
|
4492
|
+
error: error.message,
|
|
4493
|
+
...fallbackPayload,
|
|
4494
|
+
...error instanceof CookbookDeployError ? error.payload : {}
|
|
4495
|
+
};
|
|
4496
|
+
if (options.json) {
|
|
4497
|
+
emitCookbookDeployError(deps, options, payload);
|
|
4498
|
+
} else {
|
|
4499
|
+
printErrorLines(deps, formatCookbookDeployFailure(options, deps, error.message, payload));
|
|
4500
|
+
}
|
|
4501
|
+
throw new import_commander.CommanderError(1, commanderCode, "");
|
|
4502
|
+
}
|
|
4503
|
+
function printDeployPlan(deps, plan) {
|
|
4504
|
+
const runtimeLabel = { node: "Node.js", python: "Python", static: "Static Site", go: "Go", deno: "Deno" };
|
|
4505
|
+
const label = runtimeLabel[plan.runtime] || plan.runtime;
|
|
4506
|
+
const lines = [
|
|
4507
|
+
"",
|
|
4508
|
+
` Detected: ${label} (${plan.packageManager})`,
|
|
4509
|
+
"",
|
|
4510
|
+
` Install: ${plan.installCommand}`
|
|
4511
|
+
];
|
|
4512
|
+
if (plan.buildCommand) lines.push(` Build: ${plan.buildCommand}`);
|
|
4513
|
+
lines.push(
|
|
4514
|
+
` Start: ${plan.startCommand}`,
|
|
4515
|
+
` Port: ${plan.port}`,
|
|
4516
|
+
` Health: ${plan.healthPath}`
|
|
4517
|
+
);
|
|
4518
|
+
if (plan.secrets.length > 0) {
|
|
4519
|
+
lines.push("", " Secrets:");
|
|
4520
|
+
for (const s of plan.secrets) {
|
|
4521
|
+
const envVal = process.env[s.env_name] || "";
|
|
4522
|
+
if (envVal) {
|
|
4523
|
+
lines.push(` ${s.env_name}: set (from environment)`);
|
|
4524
|
+
} else {
|
|
4525
|
+
lines.push(` ${s.env_name}: (not set \u2014 will prompt)`);
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
lines.push("");
|
|
4530
|
+
printLines(deps, lines);
|
|
4531
|
+
}
|
|
4532
|
+
function getDeployCacheDir() {
|
|
4533
|
+
const cacheDir = import_path14.default.join(getConfigDir(), "deploys");
|
|
4534
|
+
import_fs5.default.mkdirSync(cacheDir, { recursive: true, mode: 448 });
|
|
4535
|
+
try {
|
|
4536
|
+
import_fs5.default.chmodSync(cacheDir, 448);
|
|
4537
|
+
} catch {
|
|
4538
|
+
}
|
|
4539
|
+
return cacheDir;
|
|
4540
|
+
}
|
|
4541
|
+
function getDeployCachePath(projectPath) {
|
|
4542
|
+
const digest = import_crypto2.default.createHash("sha256").update(import_path14.default.resolve(projectPath)).digest("hex");
|
|
4543
|
+
return import_path14.default.join(getDeployCacheDir(), `${digest}.json`);
|
|
4544
|
+
}
|
|
4545
|
+
function deployFingerprint(plan) {
|
|
4546
|
+
return import_crypto2.default.createHash("sha256").update(JSON.stringify({
|
|
4547
|
+
slug: plan.slug,
|
|
4548
|
+
runtime: plan.runtime,
|
|
4549
|
+
package_manager: plan.packageManager,
|
|
4550
|
+
install_command: plan.installCommand,
|
|
4551
|
+
build_command: plan.buildCommand,
|
|
4552
|
+
start_command: plan.startCommand,
|
|
4553
|
+
port: plan.port,
|
|
4554
|
+
health_path: plan.healthPath
|
|
4555
|
+
})).digest("hex");
|
|
4556
|
+
}
|
|
4557
|
+
function loadDeployRecord(projectPath) {
|
|
4558
|
+
const recordPath = getDeployCachePath(projectPath);
|
|
4559
|
+
if (!import_fs5.default.existsSync(recordPath)) return null;
|
|
4560
|
+
try {
|
|
4561
|
+
return JSON.parse(import_fs5.default.readFileSync(recordPath, "utf8"));
|
|
4562
|
+
} catch {
|
|
4563
|
+
return null;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
function saveDeployRecord(projectPath, record) {
|
|
4567
|
+
const recordPath = getDeployCachePath(projectPath);
|
|
4568
|
+
import_fs5.default.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
|
|
4569
|
+
`, { encoding: "utf8", mode: 384 });
|
|
4570
|
+
try {
|
|
4571
|
+
import_fs5.default.chmodSync(recordPath, 384);
|
|
4572
|
+
} catch {
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
async function resolveSavedSnapshot(client, projectPath, plan, progress) {
|
|
4576
|
+
const record = loadDeployRecord(projectPath);
|
|
4577
|
+
if (!record) return void 0;
|
|
4578
|
+
const snapshotId = String(record.snapshot_id || "").trim();
|
|
4579
|
+
if (!snapshotId || record.fingerprint !== deployFingerprint(plan)) return void 0;
|
|
4580
|
+
try {
|
|
4581
|
+
const snapshot = await client.snapshots.get(snapshotId);
|
|
4582
|
+
const status = String(snapshot.status || "").toLowerCase();
|
|
4583
|
+
if (status && status !== "ready") return void 0;
|
|
4584
|
+
} catch {
|
|
4585
|
+
return void 0;
|
|
4586
|
+
}
|
|
4587
|
+
progress?.(`Reusing saved snapshot ${snapshotId}`);
|
|
4588
|
+
return snapshotId;
|
|
4589
|
+
}
|
|
4590
|
+
async function createDeploySnapshot(client, projectPath, plan, payload, progress) {
|
|
4591
|
+
const vmId = String(payload.vm_id || "").trim();
|
|
4592
|
+
if (!vmId) return void 0;
|
|
4593
|
+
progress?.("Creating snapshot");
|
|
4594
|
+
let result;
|
|
4595
|
+
try {
|
|
4596
|
+
result = await client.vms.snapshot(vmId, { name: `deploy/${plan.slug}` }, true);
|
|
4597
|
+
} catch (error) {
|
|
4598
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4599
|
+
throw new CookbookError(`Failed to create deploy snapshot: ${message}`);
|
|
4600
|
+
}
|
|
4601
|
+
const snapshotId = String(result.snapshot_id || result.id || "").trim();
|
|
4602
|
+
if (!snapshotId) return void 0;
|
|
4603
|
+
saveDeployRecord(projectPath, {
|
|
4604
|
+
slug: plan.slug,
|
|
4605
|
+
project_path: import_path14.default.resolve(projectPath),
|
|
4606
|
+
snapshot_id: snapshotId,
|
|
4607
|
+
vm_id: vmId,
|
|
4608
|
+
share_url: payload.share_url || null,
|
|
4609
|
+
last_deployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4610
|
+
fingerprint: deployFingerprint(plan),
|
|
4611
|
+
plan: {
|
|
4612
|
+
runtime: plan.runtime,
|
|
4613
|
+
package_manager: plan.packageManager,
|
|
4614
|
+
install_command: plan.installCommand,
|
|
4615
|
+
build_command: plan.buildCommand,
|
|
4616
|
+
start_command: plan.startCommand,
|
|
4617
|
+
port: plan.port,
|
|
4618
|
+
health_path: plan.healthPath
|
|
4619
|
+
}
|
|
4620
|
+
});
|
|
4621
|
+
return snapshotId;
|
|
4622
|
+
}
|
|
4623
|
+
function formatDeploySuccess(options, deps, payload) {
|
|
4624
|
+
const colorEnabled = !options.json && supportsAnsi(deps.stdout);
|
|
4625
|
+
const vmId = String(payload.vm_id || "");
|
|
4626
|
+
const slug = String(payload.slug || "");
|
|
4627
|
+
const displayUrl = String(payload.share_url || "").replace(/\/$/, "");
|
|
4628
|
+
const connectHint = vmId ? `instavm connect ${vmId}` : "-";
|
|
4629
|
+
return [
|
|
4630
|
+
styleText("Deployed", ["1", "32"], colorEnabled),
|
|
4631
|
+
"",
|
|
4632
|
+
` App ${slug}`,
|
|
4633
|
+
` VM ${vmId}`,
|
|
4634
|
+
` URL ${styleText(displayUrl, ["32", "4"], colorEnabled)}`,
|
|
4635
|
+
` Connect ${connectHint}`,
|
|
4636
|
+
` Destroy ${payload.destroy_command || ""}`,
|
|
4637
|
+
...payload.reused_snapshot_id ? [` Snapshot reused ${payload.reused_snapshot_id}`] : [],
|
|
4638
|
+
...payload.saved_snapshot_id ? [` Snapshot saved ${payload.saved_snapshot_id}`] : []
|
|
4639
|
+
];
|
|
4640
|
+
}
|
|
4641
|
+
function addRuntimeOptions(command, options = {}) {
|
|
4642
|
+
command.option("--api-key <apiKey>", "Override the InstaVM API key for this command");
|
|
4643
|
+
command.option("--base-url <baseUrl>", "Override the InstaVM API base URL for this command");
|
|
4644
|
+
if (options.includeSshHost) {
|
|
4645
|
+
command.option("--ssh-host <sshHost>", "Override the SSH gateway host for this command");
|
|
4646
|
+
}
|
|
4647
|
+
if (options.includeJson !== false) {
|
|
4648
|
+
command.option("-j, --json", "Emit JSON output");
|
|
4649
|
+
}
|
|
4650
|
+
return command;
|
|
4651
|
+
}
|
|
4652
|
+
function parseSize(value) {
|
|
4653
|
+
const raw = value.trim().toLowerCase();
|
|
4654
|
+
if (!raw) {
|
|
4655
|
+
throw new Error("size is required");
|
|
4656
|
+
}
|
|
4657
|
+
const units = [
|
|
4658
|
+
["tb", 1024 ** 4],
|
|
4659
|
+
["gb", 1024 ** 3],
|
|
4660
|
+
["mb", 1024 ** 2],
|
|
4661
|
+
["kb", 1024],
|
|
4662
|
+
["b", 1]
|
|
4663
|
+
];
|
|
4664
|
+
for (const [suffix, multiplier] of units) {
|
|
4665
|
+
if (raw.endsWith(suffix) && raw !== suffix) {
|
|
4666
|
+
const number = raw.slice(0, -suffix.length).trim();
|
|
4667
|
+
return Math.floor(Number.parseFloat(number) * multiplier);
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
return Number.parseInt(raw, 10);
|
|
4671
|
+
}
|
|
4672
|
+
function humanBytes(value) {
|
|
4673
|
+
let amount = Number(value || 0);
|
|
4674
|
+
for (const unit of ["B", "KB", "MB", "GB", "TB"]) {
|
|
4675
|
+
if (amount < 1024 || unit === "TB") {
|
|
4676
|
+
if (unit === "B") {
|
|
4677
|
+
return `${Math.trunc(amount)}${unit}`;
|
|
4678
|
+
}
|
|
4679
|
+
return `${amount.toFixed(1)}${unit}`;
|
|
4680
|
+
}
|
|
4681
|
+
amount /= 1024;
|
|
4682
|
+
}
|
|
4683
|
+
return `${Math.trunc(Number(value || 0))}B`;
|
|
4684
|
+
}
|
|
4685
|
+
function parseEnvPairs(values) {
|
|
4686
|
+
const pairs = {};
|
|
4687
|
+
for (const entry of values || []) {
|
|
4688
|
+
const separatorIndex = entry.indexOf("=");
|
|
4689
|
+
if (separatorIndex < 1) {
|
|
4690
|
+
throw new Error(`Expected KEY=VALUE, got: ${entry}`);
|
|
4691
|
+
}
|
|
4692
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
4693
|
+
const value = entry.slice(separatorIndex + 1);
|
|
4694
|
+
if (!key) {
|
|
4695
|
+
throw new Error(`Invalid env key: ${entry}`);
|
|
4696
|
+
}
|
|
4697
|
+
pairs[key] = value;
|
|
4698
|
+
}
|
|
4699
|
+
return pairs;
|
|
4700
|
+
}
|
|
4701
|
+
function parseVolumeSpec(spec) {
|
|
4702
|
+
const parts = spec.split(":");
|
|
4703
|
+
if (parts.length < 2) {
|
|
4704
|
+
throw new Error("volume mounts must use <volume_id>:<mount_path>[:rw|ro[:checkpoint_id]]");
|
|
4705
|
+
}
|
|
4706
|
+
const volumeId = parts[0].trim();
|
|
4707
|
+
const mountPath = parts[1].trim();
|
|
4708
|
+
let mode = "rw";
|
|
4709
|
+
let checkpointId = null;
|
|
4710
|
+
if (parts[2]?.trim()) {
|
|
4711
|
+
mode = parts[2].trim().toLowerCase();
|
|
4712
|
+
}
|
|
4713
|
+
if (parts[3]?.trim()) {
|
|
4714
|
+
checkpointId = parts[3].trim();
|
|
4715
|
+
}
|
|
4716
|
+
if (parts.length > 4) {
|
|
4717
|
+
throw new Error("volume mounts must use <volume_id>:<mount_path>[:rw|ro[:checkpoint_id]]");
|
|
4718
|
+
}
|
|
4719
|
+
if (!["rw", "ro"].includes(mode)) {
|
|
4720
|
+
throw new Error("volume mount mode must be rw or ro");
|
|
4721
|
+
}
|
|
4722
|
+
if (mode === "rw" && checkpointId) {
|
|
4723
|
+
throw new Error("checkpoint_id is only allowed for ro mounts");
|
|
4724
|
+
}
|
|
4725
|
+
if (mode === "ro" && !checkpointId) {
|
|
4726
|
+
checkpointId = "latest";
|
|
4727
|
+
}
|
|
4728
|
+
return {
|
|
4729
|
+
volume_id: volumeId,
|
|
4730
|
+
mount_path: mountPath,
|
|
4731
|
+
mode,
|
|
4732
|
+
checkpoint_id: checkpointId
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
function looksLikeUuid(value) {
|
|
4736
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
4737
|
+
}
|
|
4738
|
+
function statusPayload(deps, options) {
|
|
4739
|
+
const settings = deps.resolveRuntimeConfig({
|
|
4740
|
+
apiKey: options.apiKey,
|
|
4741
|
+
baseURL: options.baseUrl,
|
|
4742
|
+
sshHost: options.sshHost
|
|
4743
|
+
});
|
|
4744
|
+
const profile = getActiveProfile(settings.config);
|
|
4745
|
+
const storedKey = profile.auth?.api_key ? String(profile.auth.api_key) : void 0;
|
|
4746
|
+
return {
|
|
4747
|
+
config_path: settings.configPath,
|
|
4748
|
+
active_profile: settings.config.active_profile,
|
|
4749
|
+
api_key: {
|
|
4750
|
+
configured: Boolean(settings.apiKey),
|
|
4751
|
+
redacted: redactSecret(settings.apiKey),
|
|
4752
|
+
source: settings.apiKeySource,
|
|
4753
|
+
stored: Boolean(storedKey)
|
|
4754
|
+
},
|
|
4755
|
+
base_url: {
|
|
4756
|
+
value: settings.baseURL,
|
|
4757
|
+
source: settings.baseURLSource
|
|
4758
|
+
},
|
|
4759
|
+
ssh_host: {
|
|
4760
|
+
value: settings.sshHost,
|
|
4761
|
+
source: settings.sshHostSource
|
|
4762
|
+
}
|
|
4763
|
+
};
|
|
4764
|
+
}
|
|
4765
|
+
function statusText(payload) {
|
|
4766
|
+
const lines = [
|
|
4767
|
+
`Config path: ${payload.config_path}`,
|
|
4768
|
+
`Active profile: ${payload.active_profile}`,
|
|
4769
|
+
`API key: ${payload.api_key.redacted || "not configured"} (${payload.api_key.source})`,
|
|
4770
|
+
`Base URL: ${payload.base_url.value} (${payload.base_url.source})`,
|
|
4771
|
+
`SSH host: ${payload.ssh_host.value} (${payload.ssh_host.source})`
|
|
4772
|
+
];
|
|
4773
|
+
if (payload.api_key.stored) {
|
|
4774
|
+
lines.push("Stored key: present");
|
|
4775
|
+
}
|
|
4776
|
+
return lines;
|
|
4777
|
+
}
|
|
4778
|
+
function resolveSettings(deps, options) {
|
|
4779
|
+
return deps.resolveRuntimeConfig({
|
|
4780
|
+
apiKey: options.apiKey,
|
|
4781
|
+
baseURL: options.baseUrl,
|
|
4782
|
+
sshHost: options.sshHost
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4785
|
+
function requireClient(deps, options) {
|
|
4786
|
+
const settings = resolveSettings(deps, options);
|
|
4787
|
+
if (!settings.apiKey) {
|
|
4788
|
+
throw new Error("No InstaVM API key configured. Run `instavm auth set-key` or export INSTAVM_API_KEY.");
|
|
4789
|
+
}
|
|
4790
|
+
return {
|
|
4791
|
+
client: deps.clientFactory(settings.apiKey, { baseURL: settings.baseURL }),
|
|
4792
|
+
settings
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
async function resolveDesktopTarget(client, target) {
|
|
4796
|
+
if (looksLikeUuid(target)) {
|
|
4797
|
+
const status = await client.getSessionStatus(target);
|
|
4798
|
+
return { ...status, session_id: target };
|
|
4799
|
+
}
|
|
4800
|
+
const vm = await client.vms.get(target);
|
|
4801
|
+
const sessionId = vm.session_id ? String(vm.session_id) : null;
|
|
4802
|
+
if (sessionId) {
|
|
4803
|
+
const status = await client.getSessionStatus(sessionId);
|
|
4804
|
+
return {
|
|
4805
|
+
...status,
|
|
4806
|
+
session_id: sessionId,
|
|
4807
|
+
vm_id: status.vm_id || vm.vm_id
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
return {
|
|
4811
|
+
session_id: null,
|
|
4812
|
+
vm_id: vm.vm_id,
|
|
4813
|
+
vm_status: vm.status,
|
|
4814
|
+
vm_alive: vm.status === "active"
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
async function desktopPayload(client, target) {
|
|
4818
|
+
const payload = await resolveDesktopTarget(client, target);
|
|
4819
|
+
if (payload.vm_alive && payload.session_id) {
|
|
4820
|
+
try {
|
|
4821
|
+
Object.assign(payload, await client.computerUse.viewerUrl(String(payload.session_id)));
|
|
4822
|
+
} catch (error) {
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
return payload;
|
|
4826
|
+
}
|
|
4827
|
+
function createProgram(deps = defaultDeps) {
|
|
2749
4828
|
const program = new import_commander.Command();
|
|
2750
4829
|
program.name("instavm").description("InstaVM CLI for VM, snapshot, volume, desktop, identity, and sharing workflows.").showHelpAfterError().showSuggestionAfterError().configureOutput({
|
|
2751
4830
|
writeOut: (str) => deps.stdout.write(str),
|
|
@@ -2823,19 +4902,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
2823
4902
|
is_verified: user.is_verified,
|
|
2824
4903
|
ssh_keys: sshKeys
|
|
2825
4904
|
};
|
|
2826
|
-
emitOutput(
|
|
2827
|
-
deps,
|
|
2828
|
-
options,
|
|
2829
|
-
payload,
|
|
2830
|
-
[
|
|
2831
|
-
`Email: ${payload.email || "-"}`,
|
|
2832
|
-
`User ID: ${payload.id}`,
|
|
2833
|
-
`Name: ${payload.name || "-"}`,
|
|
2834
|
-
`Verified: ${payload.is_verified ? "yes" : "no"}`,
|
|
2835
|
-
"SSH Keys:",
|
|
2836
|
-
...sshKeys.length ? sshKeys.map((key) => ` ${key.id} ${key.fingerprint} ${key.comment || ""}`.trimEnd()) : [" none"]
|
|
2837
|
-
]
|
|
2838
|
-
);
|
|
4905
|
+
emitOutput(deps, options, payload, formatWhoamiOutput(payload));
|
|
2839
4906
|
})
|
|
2840
4907
|
);
|
|
2841
4908
|
addRuntimeOptions(
|
|
@@ -2881,14 +4948,15 @@ function createProgram(deps = defaultDeps) {
|
|
|
2881
4948
|
})
|
|
2882
4949
|
);
|
|
2883
4950
|
addRuntimeOptions(
|
|
2884
|
-
program.command("ls").alias("list").description("List
|
|
4951
|
+
program.command("ls").alias("list").description("List active VMs by default").option("-a, --all", "Include all VM records, including terminated VMs").action(async (options) => {
|
|
2885
4952
|
const { client } = requireClient(deps, options);
|
|
2886
|
-
const
|
|
4953
|
+
const listedVms = options.all ? await client.vms.listAllRecords() : await client.vms.list();
|
|
4954
|
+
const vms = options.all ? listedVms : filterActiveVms(listedVms);
|
|
2887
4955
|
emitOutput(
|
|
2888
4956
|
deps,
|
|
2889
4957
|
options,
|
|
2890
4958
|
{ vms },
|
|
2891
|
-
vms.length > 0 ? vms.
|
|
4959
|
+
vms.length > 0 ? formatVmList(vms, Boolean(options.all)) : [options.all ? "No VMs." : "No active VMs. Use `instavm ls -a` or `instavm ls --all` to include terminated VMs."]
|
|
2892
4960
|
);
|
|
2893
4961
|
})
|
|
2894
4962
|
);
|
|
@@ -2927,7 +4995,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
2927
4995
|
addRuntimeOptions(
|
|
2928
4996
|
program.command("connect").description("SSH into a VM with the local ssh client").argument("<vmId>").argument("[sshArgs...]").action(async (vmId, sshArgs, options) => {
|
|
2929
4997
|
const settings = resolveSettings(deps, options);
|
|
2930
|
-
const sshBinary = process.platform === "win32" ?
|
|
4998
|
+
const sshBinary = process.platform === "win32" ? findExecutable2(deps, "ssh.exe") || findExecutable2(deps, "ssh") : findExecutable2(deps, "ssh") || findExecutable2(deps, "ssh.exe");
|
|
2931
4999
|
if (!sshBinary) {
|
|
2932
5000
|
throw new Error("ssh client not found on PATH");
|
|
2933
5001
|
}
|
|
@@ -2959,7 +5027,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
2959
5027
|
deps,
|
|
2960
5028
|
options,
|
|
2961
5029
|
{ snapshots },
|
|
2962
|
-
snapshots.length > 0 ? snapshots
|
|
5030
|
+
snapshots.length > 0 ? formatSnapshotList(snapshots) : ["No snapshots."]
|
|
2963
5031
|
);
|
|
2964
5032
|
})
|
|
2965
5033
|
);
|
|
@@ -3081,7 +5149,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
3081
5149
|
deps,
|
|
3082
5150
|
options,
|
|
3083
5151
|
{ keys },
|
|
3084
|
-
keys.length > 0 ? keys
|
|
5152
|
+
keys.length > 0 ? formatSshKeyList(keys) : ["No SSH keys."]
|
|
3085
5153
|
);
|
|
3086
5154
|
})
|
|
3087
5155
|
);
|
|
@@ -3198,7 +5266,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
3198
5266
|
deps,
|
|
3199
5267
|
options,
|
|
3200
5268
|
{ volumes },
|
|
3201
|
-
volumes.length > 0 ? volumes
|
|
5269
|
+
volumes.length > 0 ? formatVolumeList(volumes) : ["No volumes."]
|
|
3202
5270
|
);
|
|
3203
5271
|
})
|
|
3204
5272
|
);
|
|
@@ -3261,7 +5329,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
3261
5329
|
deps,
|
|
3262
5330
|
options,
|
|
3263
5331
|
{ checkpoints },
|
|
3264
|
-
checkpoints.length > 0 ? checkpoints
|
|
5332
|
+
checkpoints.length > 0 ? formatCheckpointList(checkpoints) : ["No checkpoints."]
|
|
3265
5333
|
);
|
|
3266
5334
|
})
|
|
3267
5335
|
);
|
|
@@ -3292,14 +5360,14 @@ function createProgram(deps = defaultDeps) {
|
|
|
3292
5360
|
deps,
|
|
3293
5361
|
options,
|
|
3294
5362
|
{ files: result },
|
|
3295
|
-
result.length > 0 ? result
|
|
5363
|
+
result.length > 0 ? formatFileList(result) : ["No files."]
|
|
3296
5364
|
);
|
|
3297
5365
|
})
|
|
3298
5366
|
);
|
|
3299
5367
|
addRuntimeOptions(
|
|
3300
5368
|
files.command("upload").description("Upload a file into a volume").argument("<volumeId>").argument("<localPath>").option("--path <remotePath>", "Remote path inside the volume").option("--overwrite", "Overwrite if the path already exists").action(async (volumeId, localPath, options) => {
|
|
3301
5369
|
const { client } = requireClient(deps, options);
|
|
3302
|
-
const remotePath = options.path ||
|
|
5370
|
+
const remotePath = options.path || import_path14.default.basename(localPath);
|
|
3303
5371
|
const result = await client.volumes.uploadFile(volumeId, {
|
|
3304
5372
|
filePath: localPath,
|
|
3305
5373
|
path: remotePath,
|
|
@@ -3312,7 +5380,7 @@ function createProgram(deps = defaultDeps) {
|
|
|
3312
5380
|
files.command("download").description("Download a file from a volume").argument("<volumeId>").argument("<remotePath>").option("--out <outputPath>", "Local output path").action(async (volumeId, remotePath, options) => {
|
|
3313
5381
|
const { client } = requireClient(deps, options);
|
|
3314
5382
|
const result = await client.volumes.downloadFile(volumeId, remotePath);
|
|
3315
|
-
const outputPath = options.out ||
|
|
5383
|
+
const outputPath = options.out || import_path14.default.basename(remotePath);
|
|
3316
5384
|
deps.writeFile(outputPath, result.content);
|
|
3317
5385
|
emitOutput(
|
|
3318
5386
|
deps,
|
|
@@ -3340,20 +5408,173 @@ function createProgram(deps = defaultDeps) {
|
|
|
3340
5408
|
program.command("billing").description("Show the billing portal URL").option("-j, --json", "Emit JSON output").action((options) => {
|
|
3341
5409
|
emitOutput(deps, options, { url: DEFAULT_BILLING_URL }, [DEFAULT_BILLING_URL]);
|
|
3342
5410
|
});
|
|
5411
|
+
addRuntimeOptions(
|
|
5412
|
+
program.command("deploy").description("Detect and deploy an app from a local directory").argument("[path]", "Project directory", ".").option("-y, --yes", "Skip confirmation prompt").option("--env <key=value...>", "Set environment variable (repeatable)", (val, prev) => [...prev, val], []).option("--port <port>", "Override detected port", parseInt).option("--health-path <path>", "Override detected health check path").option("--start-command <cmd>", "Override detected start command").option("--install-command <cmd>", "Override detected install command").option("--build-command <cmd>", "Override detected build command").option("--plan", "Show detected plan and exit (dry-run)").option("--private", "Don't create public share").option("--save-snapshot", "Create VM snapshot after successful deploy").action(async (projectPath, options) => {
|
|
5413
|
+
const resolved = import_path14.default.resolve(projectPath);
|
|
5414
|
+
let plan;
|
|
5415
|
+
try {
|
|
5416
|
+
plan = detectProject(resolved);
|
|
5417
|
+
} catch (err) {
|
|
5418
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5419
|
+
deps.stderr.write(`Error: ${msg}
|
|
5420
|
+
`);
|
|
5421
|
+
throw new import_commander.CommanderError(1, "deploy.detect.failed", "");
|
|
5422
|
+
}
|
|
5423
|
+
if (options.port) plan.port = options.port;
|
|
5424
|
+
if (options.healthPath) plan.healthPath = options.healthPath;
|
|
5425
|
+
if (options.startCommand) plan.startCommand = options.startCommand;
|
|
5426
|
+
if (options.installCommand) plan.installCommand = options.installCommand;
|
|
5427
|
+
if (options.buildCommand !== void 0) plan.buildCommand = options.buildCommand;
|
|
5428
|
+
for (const kv of options.env || []) {
|
|
5429
|
+
const eqIdx = kv.indexOf("=");
|
|
5430
|
+
if (eqIdx > 0) process.env[kv.slice(0, eqIdx)] = kv.slice(eqIdx + 1);
|
|
5431
|
+
}
|
|
5432
|
+
if (options.plan) {
|
|
5433
|
+
if (options.json) {
|
|
5434
|
+
printJson(deps, {
|
|
5435
|
+
slug: plan.slug,
|
|
5436
|
+
runtime: plan.runtime,
|
|
5437
|
+
package_manager: plan.packageManager,
|
|
5438
|
+
install_command: plan.installCommand,
|
|
5439
|
+
build_command: plan.buildCommand,
|
|
5440
|
+
start_command: plan.startCommand,
|
|
5441
|
+
port: plan.port,
|
|
5442
|
+
health_path: plan.healthPath,
|
|
5443
|
+
secrets: plan.secrets.map((s) => s.env_name)
|
|
5444
|
+
});
|
|
5445
|
+
} else {
|
|
5446
|
+
printDeployPlan(deps, plan);
|
|
5447
|
+
}
|
|
5448
|
+
return;
|
|
5449
|
+
}
|
|
5450
|
+
if (!options.yes && deps.stdout.isTTY) {
|
|
5451
|
+
printDeployPlan(deps, plan);
|
|
5452
|
+
const rl = (await import("readline")).createInterface({ input: process.stdin, output: process.stdout });
|
|
5453
|
+
const answer = await new Promise((resolve) => rl.question(" Deploy? [Y/n] ", resolve));
|
|
5454
|
+
rl.close();
|
|
5455
|
+
if (answer.trim().toLowerCase() === "n") return;
|
|
5456
|
+
}
|
|
5457
|
+
const manifest = planToManifest(plan, resolved);
|
|
5458
|
+
if (options.private) manifest.app.share_public_default = false;
|
|
5459
|
+
const { client, settings } = requireClient(deps, options);
|
|
5460
|
+
const progressRenderer = new ProgressRenderer(deps, options);
|
|
5461
|
+
const reusedSnapshotId = await resolveSavedSnapshot(
|
|
5462
|
+
client,
|
|
5463
|
+
resolved,
|
|
5464
|
+
plan,
|
|
5465
|
+
(message) => cookbookProgress(progressRenderer, message)
|
|
5466
|
+
);
|
|
5467
|
+
try {
|
|
5468
|
+
const payload = await runDeploy(
|
|
5469
|
+
manifest,
|
|
5470
|
+
client,
|
|
5471
|
+
settings,
|
|
5472
|
+
deps,
|
|
5473
|
+
(message) => cookbookProgress(progressRenderer, message),
|
|
5474
|
+
reusedSnapshotId
|
|
5475
|
+
);
|
|
5476
|
+
if (reusedSnapshotId) {
|
|
5477
|
+
payload.reused_snapshot_id = reusedSnapshotId;
|
|
5478
|
+
}
|
|
5479
|
+
if (options.saveSnapshot) {
|
|
5480
|
+
const savedSnapshotId = await createDeploySnapshot(
|
|
5481
|
+
client,
|
|
5482
|
+
resolved,
|
|
5483
|
+
plan,
|
|
5484
|
+
payload,
|
|
5485
|
+
(message) => cookbookProgress(progressRenderer, message)
|
|
5486
|
+
);
|
|
5487
|
+
if (savedSnapshotId) {
|
|
5488
|
+
payload.saved_snapshot_id = savedSnapshotId;
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
progressRenderer.stop();
|
|
5492
|
+
emitOutput(deps, options, payload, formatDeploySuccess(options, deps, payload));
|
|
5493
|
+
} catch (error) {
|
|
5494
|
+
progressRenderer.stop();
|
|
5495
|
+
emitCookbookStyleFailure(deps, options, error, "deploy.failed");
|
|
5496
|
+
}
|
|
5497
|
+
}),
|
|
5498
|
+
{ includeSshHost: true }
|
|
5499
|
+
);
|
|
5500
|
+
const cookbook = program.command("cookbook").description("Discover and deploy curated cookbook applications");
|
|
5501
|
+
cookbook.command("list").description("List available cookbooks").option("-j, --json", "Emit JSON output").action(async (options) => {
|
|
5502
|
+
const root = resolveCookbookRoot(deps);
|
|
5503
|
+
const cookbooks = discoverCookbooks(root).map(manifestDisplayRecord);
|
|
5504
|
+
emitOutput(
|
|
5505
|
+
deps,
|
|
5506
|
+
options,
|
|
5507
|
+
{ cookbooks },
|
|
5508
|
+
formatCookbookList(cookbooks)
|
|
5509
|
+
);
|
|
5510
|
+
});
|
|
5511
|
+
cookbook.command("info").description("Show cookbook details and required secrets").argument("<slug>").option("-j, --json", "Emit JSON output").action(async (slug, options) => {
|
|
5512
|
+
const root = resolveCookbookRoot(deps);
|
|
5513
|
+
const manifest = loadManifest(import_path14.default.join(root, slug, "instavm.yaml"));
|
|
5514
|
+
emitOutput(
|
|
5515
|
+
deps,
|
|
5516
|
+
options,
|
|
5517
|
+
{
|
|
5518
|
+
...manifestDisplayRecord(manifest),
|
|
5519
|
+
vm: manifest.vm,
|
|
5520
|
+
app: manifest.app,
|
|
5521
|
+
run: manifest.run,
|
|
5522
|
+
secrets: manifest.secrets,
|
|
5523
|
+
notes: manifest.post_deploy_notes
|
|
5524
|
+
},
|
|
5525
|
+
[
|
|
5526
|
+
`Slug: ${manifest.slug}`,
|
|
5527
|
+
`Title: ${manifest.title}`,
|
|
5528
|
+
`Summary: ${manifest.summary}`,
|
|
5529
|
+
`Runtime: ${manifest.runtime}`,
|
|
5530
|
+
`Deploy: ${manifest.deploy.kind}`,
|
|
5531
|
+
`VM: ${manifest.vm.vcpu_count} vCPU, ${manifest.vm.memory_mb}MB`,
|
|
5532
|
+
`Port: ${manifest.app.port}`,
|
|
5533
|
+
`Healthcheck: ${manifest.app.healthcheck_path}`,
|
|
5534
|
+
`Secrets: ${manifest.secrets.map((secret) => secret.env_name).join(", ") || "none"}`,
|
|
5535
|
+
...manifest.post_deploy_notes.length > 0 ? ["Notes:", ...manifest.post_deploy_notes.map((note) => ` - ${note}`)] : []
|
|
5536
|
+
]
|
|
5537
|
+
);
|
|
5538
|
+
});
|
|
5539
|
+
addRuntimeOptions(
|
|
5540
|
+
cookbook.command("deploy").description("Create a VM, start the cookbook app, and return the public share URL").argument("<slug>").action(async (slug, options) => {
|
|
5541
|
+
const progressRenderer = new ProgressRenderer(deps, options);
|
|
5542
|
+
try {
|
|
5543
|
+
const root = resolveCookbookRoot(deps, (message) => cookbookProgress(progressRenderer, message));
|
|
5544
|
+
const manifest = loadManifest(import_path14.default.join(root, slug, "instavm.yaml"));
|
|
5545
|
+
const { client, settings } = requireClient(deps, options);
|
|
5546
|
+
const payload = await deployCookbook(
|
|
5547
|
+
manifest,
|
|
5548
|
+
client,
|
|
5549
|
+
settings,
|
|
5550
|
+
deps,
|
|
5551
|
+
(message) => cookbookProgress(progressRenderer, message)
|
|
5552
|
+
);
|
|
5553
|
+
progressRenderer.stop();
|
|
5554
|
+
emitOutput(deps, options, payload, formatCookbookDeploySuccess(options, deps, payload));
|
|
5555
|
+
} catch (error) {
|
|
5556
|
+
progressRenderer.stop();
|
|
5557
|
+
emitCookbookStyleFailure(deps, options, error, "cookbook.deploy.failed", { slug });
|
|
5558
|
+
}
|
|
5559
|
+
}),
|
|
5560
|
+
{ includeSshHost: true }
|
|
5561
|
+
);
|
|
3343
5562
|
return program;
|
|
3344
5563
|
}
|
|
3345
5564
|
async function runCli(argv, deps = defaultDeps) {
|
|
3346
5565
|
const program = createProgram(deps);
|
|
3347
5566
|
try {
|
|
3348
|
-
await program.parseAsync(["node", "instavm", ...argv]);
|
|
5567
|
+
await program.parseAsync(["node", "instavm", ...normalizeArgv(argv)]);
|
|
3349
5568
|
return 0;
|
|
3350
5569
|
} catch (error) {
|
|
3351
5570
|
if (error instanceof import_commander.CommanderError) {
|
|
3352
5571
|
if (error.code === "commander.helpDisplayed") {
|
|
3353
5572
|
return error.exitCode;
|
|
3354
5573
|
}
|
|
3355
|
-
|
|
5574
|
+
if (error.message) {
|
|
5575
|
+
deps.stderr.write(`${error.message}
|
|
3356
5576
|
`);
|
|
5577
|
+
}
|
|
3357
5578
|
return error.exitCode || 1;
|
|
3358
5579
|
}
|
|
3359
5580
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -3362,7 +5583,7 @@ async function runCli(argv, deps = defaultDeps) {
|
|
|
3362
5583
|
return 1;
|
|
3363
5584
|
}
|
|
3364
5585
|
}
|
|
3365
|
-
function
|
|
5586
|
+
function findExecutable2(deps, binary) {
|
|
3366
5587
|
const result = deps.spawnSync(process.platform === "win32" ? "where" : "which", [binary], {
|
|
3367
5588
|
encoding: "utf8",
|
|
3368
5589
|
stdio: ["ignore", "pipe", "ignore"]
|