hostedge 0.1.3 → 0.2.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 +1 -5
- package/dist/hostedge.js +594 -148
- package/package.json +13 -17
package/README.md
CHANGED
|
@@ -99,8 +99,4 @@ The CLI follows the same rules as the dashboard. `.git`, `node_modules` and simi
|
|
|
99
99
|
- **Too many requests:** the CLI waits and retries by itself; if it keeps failing, run it again in a minute.
|
|
100
100
|
- **Plan limit (exit 6):** upgrade at https://hostedge.app/pricing or remove an unused project.
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`pnpm --filter hostedge build` bundles `dist/hostedge.js` (no runtime dependencies). `pnpm --filter hostedge test` runs the commands against a fake API.
|
|
105
|
-
|
|
106
|
-
Source: https://github.com/ybenjaa-dev/appdock/tree/main/apps/cli
|
|
102
|
+
More at https://hostedge.app/docs/cli
|
package/dist/hostedge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
// ../../packages/core/src/plans.ts
|
|
7
7
|
var MB = 1024 * 1024;
|
|
@@ -25,6 +25,7 @@ var PLANS = {
|
|
|
25
25
|
runtimeLogsPerDay: 2e4,
|
|
26
26
|
analyticsEventsPerMonth: 2e4,
|
|
27
27
|
analyticsRetentionDays: 30,
|
|
28
|
+
devMinutesPerMonth: 0,
|
|
28
29
|
storageResourcesPerProject: 3,
|
|
29
30
|
storageBytes: 1 * GB,
|
|
30
31
|
kvOperationsPerMonth: 1e6,
|
|
@@ -33,7 +34,8 @@ var PLANS = {
|
|
|
33
34
|
formSubmissionsPerMonth: 100,
|
|
34
35
|
formRetentionDays: 90,
|
|
35
36
|
monitorsPerProject: 2,
|
|
36
|
-
monitorMinIntervalSeconds: 300
|
|
37
|
+
monitorMinIntervalSeconds: 300,
|
|
38
|
+
concurrentBuilds: 1
|
|
37
39
|
}
|
|
38
40
|
},
|
|
39
41
|
starter: {
|
|
@@ -54,6 +56,7 @@ var PLANS = {
|
|
|
54
56
|
runtimeLogsPerDay: 2e5,
|
|
55
57
|
analyticsEventsPerMonth: 2e5,
|
|
56
58
|
analyticsRetentionDays: 365,
|
|
59
|
+
devMinutesPerMonth: 120,
|
|
57
60
|
storageResourcesPerProject: 10,
|
|
58
61
|
storageBytes: 10 * GB,
|
|
59
62
|
kvOperationsPerMonth: 1e7,
|
|
@@ -62,7 +65,8 @@ var PLANS = {
|
|
|
62
65
|
formSubmissionsPerMonth: 2e3,
|
|
63
66
|
formRetentionDays: 365,
|
|
64
67
|
monitorsPerProject: 10,
|
|
65
|
-
monitorMinIntervalSeconds: 60
|
|
68
|
+
monitorMinIntervalSeconds: 60,
|
|
69
|
+
concurrentBuilds: 2
|
|
66
70
|
}
|
|
67
71
|
},
|
|
68
72
|
pro: {
|
|
@@ -83,6 +87,7 @@ var PLANS = {
|
|
|
83
87
|
runtimeLogsPerDay: 2e6,
|
|
84
88
|
analyticsEventsPerMonth: 2e6,
|
|
85
89
|
analyticsRetentionDays: 730,
|
|
90
|
+
devMinutesPerMonth: 600,
|
|
86
91
|
storageResourcesPerProject: 25,
|
|
87
92
|
storageBytes: 100 * GB,
|
|
88
93
|
kvOperationsPerMonth: 5e7,
|
|
@@ -91,7 +96,8 @@ var PLANS = {
|
|
|
91
96
|
formSubmissionsPerMonth: 2e4,
|
|
92
97
|
formRetentionDays: 730,
|
|
93
98
|
monitorsPerProject: 50,
|
|
94
|
-
monitorMinIntervalSeconds: 60
|
|
99
|
+
monitorMinIntervalSeconds: 60,
|
|
100
|
+
concurrentBuilds: 4
|
|
95
101
|
}
|
|
96
102
|
},
|
|
97
103
|
agency: {
|
|
@@ -112,6 +118,7 @@ var PLANS = {
|
|
|
112
118
|
runtimeLogsPerDay: 1e7,
|
|
113
119
|
analyticsEventsPerMonth: 1e7,
|
|
114
120
|
analyticsRetentionDays: 1095,
|
|
121
|
+
devMinutesPerMonth: 2400,
|
|
115
122
|
storageResourcesPerProject: 50,
|
|
116
123
|
storageBytes: 500 * GB,
|
|
117
124
|
kvOperationsPerMonth: 2e8,
|
|
@@ -120,7 +127,8 @@ var PLANS = {
|
|
|
120
127
|
formSubmissionsPerMonth: 1e5,
|
|
121
128
|
formRetentionDays: 1095,
|
|
122
129
|
monitorsPerProject: 100,
|
|
123
|
-
monitorMinIntervalSeconds: 60
|
|
130
|
+
monitorMinIntervalSeconds: 60,
|
|
131
|
+
concurrentBuilds: 8
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
134
|
};
|
|
@@ -139,6 +147,10 @@ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
139
147
|
"ts",
|
|
140
148
|
"tsx",
|
|
141
149
|
"json",
|
|
150
|
+
"jsonc",
|
|
151
|
+
"sql",
|
|
152
|
+
"mts",
|
|
153
|
+
"cts",
|
|
142
154
|
"map",
|
|
143
155
|
"webmanifest",
|
|
144
156
|
"txt",
|
|
@@ -245,10 +257,11 @@ function normalisePath(raw) {
|
|
|
245
257
|
function isIgnoredPath(path) {
|
|
246
258
|
return path.split("/").some((segment) => BLOCKED_SEGMENTS.has(segment));
|
|
247
259
|
}
|
|
248
|
-
function fileTypeProblem(path) {
|
|
260
|
+
function fileTypeProblem(path, anyExtension = false) {
|
|
249
261
|
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
250
262
|
if (SECRET_NAMES.test(name))
|
|
251
263
|
return "Looks like a secret (env file or key). Remove it \u2014 set secrets as environment variables instead.";
|
|
264
|
+
if (anyExtension) return null;
|
|
252
265
|
const dot = name.lastIndexOf(".");
|
|
253
266
|
const ext = dot > 0 ? name.slice(dot + 1).toLowerCase() : name.startsWith(".") ? name.slice(1).toLowerCase() : "";
|
|
254
267
|
if (!ext || dot <= 0) {
|
|
@@ -257,7 +270,7 @@ function fileTypeProblem(path) {
|
|
|
257
270
|
}
|
|
258
271
|
return ALLOWED_EXTENSIONS.has(ext) ? null : `.${ext} files can\u2019t be deployed.`;
|
|
259
272
|
}
|
|
260
|
-
function checkManifest(entries, limits) {
|
|
273
|
+
function checkManifest(entries, limits, options = {}) {
|
|
261
274
|
const problems = [];
|
|
262
275
|
const ignored = [];
|
|
263
276
|
const files = [];
|
|
@@ -278,7 +291,7 @@ function checkManifest(entries, limits) {
|
|
|
278
291
|
continue;
|
|
279
292
|
}
|
|
280
293
|
const { path } = normal;
|
|
281
|
-
if (isIgnoredPath(path)) {
|
|
294
|
+
if (!options.prebuilt && isIgnoredPath(path)) {
|
|
282
295
|
ignored.push(path);
|
|
283
296
|
continue;
|
|
284
297
|
}
|
|
@@ -305,7 +318,7 @@ function checkManifest(entries, limits) {
|
|
|
305
318
|
problems.push({ path, reason: "Invalid content hash." });
|
|
306
319
|
continue;
|
|
307
320
|
}
|
|
308
|
-
const typeProblem = fileTypeProblem(path);
|
|
321
|
+
const typeProblem = options.skipTypeChecks ? null : fileTypeProblem(path, options.prebuilt === true);
|
|
309
322
|
if (typeProblem) {
|
|
310
323
|
problems.push({ path, reason: typeProblem });
|
|
311
324
|
continue;
|
|
@@ -857,6 +870,67 @@ function detectFramework(allPaths, packageJsonText, rootDirectory = "") {
|
|
|
857
870
|
};
|
|
858
871
|
}
|
|
859
872
|
|
|
873
|
+
// ../../packages/core/src/env/dotenv.ts
|
|
874
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
875
|
+
var UNESCAPE = { n: "\n", t: " ", '"': '"', "\\": "\\" };
|
|
876
|
+
function parseDotEnv(text) {
|
|
877
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
878
|
+
const entries = [];
|
|
879
|
+
const errors = [];
|
|
880
|
+
for (let i = 0; i < lines.length; i++) {
|
|
881
|
+
const lineNumber = i + 1;
|
|
882
|
+
const raw = lines[i] ?? "";
|
|
883
|
+
const trimmed = raw.trim();
|
|
884
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
885
|
+
const body2 = trimmed.replace(/^export\s+/, "");
|
|
886
|
+
const eq = body2.indexOf("=");
|
|
887
|
+
if (eq <= 0) {
|
|
888
|
+
errors.push({ line: lineNumber, message: "Expected KEY=value." });
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const key = body2.slice(0, eq).trim();
|
|
892
|
+
if (!ENV_KEY_PATTERN.test(key)) {
|
|
893
|
+
errors.push({
|
|
894
|
+
line: lineNumber,
|
|
895
|
+
message: `\u201C${key}\u201D is not a valid name. Use letters, digits and underscores, not starting with a digit.`
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
let rest = body2.slice(eq + 1).trim();
|
|
900
|
+
let value;
|
|
901
|
+
if (rest.startsWith('"')) {
|
|
902
|
+
let collected = rest.slice(1);
|
|
903
|
+
let closed = /(^|[^\\])(\\\\)*"/.exec(collected);
|
|
904
|
+
while (!closed && i + 1 < lines.length) {
|
|
905
|
+
i += 1;
|
|
906
|
+
collected += `
|
|
907
|
+
${lines[i] ?? ""}`;
|
|
908
|
+
closed = /(^|[^\\])(\\\\)*"/.exec(collected);
|
|
909
|
+
}
|
|
910
|
+
if (!closed) {
|
|
911
|
+
errors.push({ line: lineNumber, message: "Missing closing double quote." });
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const end = closed.index + closed[0].length - 1;
|
|
915
|
+
value = collected.slice(0, end).replace(/\\(["\\nt])/g, (_, c) => UNESCAPE[c] ?? c);
|
|
916
|
+
} else if (rest.startsWith("'")) {
|
|
917
|
+
const end = rest.indexOf("'", 1);
|
|
918
|
+
if (end === -1) {
|
|
919
|
+
errors.push({ line: lineNumber, message: "Missing closing single quote." });
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
value = rest.slice(1, end);
|
|
923
|
+
} else {
|
|
924
|
+
const hash = rest.search(/\s#/);
|
|
925
|
+
if (hash !== -1) rest = rest.slice(0, hash);
|
|
926
|
+
value = rest.trim();
|
|
927
|
+
}
|
|
928
|
+
entries.push({ key, value, line: lineNumber });
|
|
929
|
+
}
|
|
930
|
+
const byKey = new Map(entries.map((e) => [e.key, e]));
|
|
931
|
+
return { entries: [...byKey.values()].sort((a, b) => a.line - b.line), errors };
|
|
932
|
+
}
|
|
933
|
+
|
|
860
934
|
// ../../packages/core/src/ai/starter.ts
|
|
861
935
|
var HOSTEDGE_MCP_URL = "https://hostedge.app/api/mcp";
|
|
862
936
|
var CONNECT_WITHOUT_TOKEN = [
|
|
@@ -1334,7 +1408,7 @@ function createApi(io2, apiUrl, token) {
|
|
|
1334
1408
|
async function raw(path, init2 = {}) {
|
|
1335
1409
|
const headers = new Headers(init2.headers);
|
|
1336
1410
|
headers.set("authorization", `Bearer ${token}`);
|
|
1337
|
-
headers.set("user-agent", "hostedge-cli/0.
|
|
1411
|
+
headers.set("user-agent", "hostedge-cli/0.2.0");
|
|
1338
1412
|
let response;
|
|
1339
1413
|
try {
|
|
1340
1414
|
response = await io2.fetch(`${apiUrl}${path}`, { ...init2, headers });
|
|
@@ -1387,8 +1461,8 @@ async function browserLogin(r, apiUrl) {
|
|
|
1387
1461
|
const { io: io2 } = r;
|
|
1388
1462
|
const state = randomBytes(24).toString("base64url");
|
|
1389
1463
|
let resolveToken = () => void 0;
|
|
1390
|
-
const received = new Promise((
|
|
1391
|
-
resolveToken =
|
|
1464
|
+
const received = new Promise((resolve7) => {
|
|
1465
|
+
resolveToken = resolve7;
|
|
1392
1466
|
});
|
|
1393
1467
|
const server = createServer((req, res) => {
|
|
1394
1468
|
const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -1400,7 +1474,7 @@ async function browserLogin(r, apiUrl) {
|
|
|
1400
1474
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(DONE_PAGE);
|
|
1401
1475
|
resolveToken(token);
|
|
1402
1476
|
});
|
|
1403
|
-
await new Promise((
|
|
1477
|
+
await new Promise((resolve7) => server.listen(0, "127.0.0.1", resolve7));
|
|
1404
1478
|
const address = server.address();
|
|
1405
1479
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
1406
1480
|
const url = `${apiUrl}/cli/authorize?${new URLSearchParams({ port: String(port), state, host: hostname() })}`;
|
|
@@ -1510,8 +1584,8 @@ Scopes: ${me.scopes.join(", ")}
|
|
|
1510
1584
|
}
|
|
1511
1585
|
|
|
1512
1586
|
// src/commands/deploy.ts
|
|
1513
|
-
import { readFile as
|
|
1514
|
-
import { basename, join as
|
|
1587
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
1588
|
+
import { basename, join as join6, resolve as resolve3 } from "node:path";
|
|
1515
1589
|
|
|
1516
1590
|
// src/files.ts
|
|
1517
1591
|
import { lstat as lstat2, readdir, readFile as readFile3 } from "node:fs/promises";
|
|
@@ -1559,14 +1633,19 @@ async function collectFiles(root, limits = { maxFiles: 1e5 }) {
|
|
|
1559
1633
|
{ maxDeploymentBytes: Number.MAX_SAFE_INTEGER, maxFiles: Number.MAX_SAFE_INTEGER }
|
|
1560
1634
|
);
|
|
1561
1635
|
const secrets = /* @__PURE__ */ new Set();
|
|
1636
|
+
const problems = [];
|
|
1562
1637
|
for (const problem of check.problems) {
|
|
1563
1638
|
if (problem.reason.startsWith("Looks like a secret")) secrets.add(problem.path);
|
|
1639
|
+
else if (limits.strict === false) problems.push(problem);
|
|
1564
1640
|
else throw new CliError(`${problem.path}: ${problem.reason}`);
|
|
1565
1641
|
}
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
return { files: upload, skipped, secrets: [...secrets] };
|
|
1642
|
+
const refused = new Set(problems.map((p) => p.path));
|
|
1643
|
+
const upload = files.filter((f) => !secrets.has(f.path) && !refused.has(f.path));
|
|
1644
|
+
if (limits.hash !== false) await hashFiles(upload);
|
|
1645
|
+
return { files: upload, skipped, secrets: [...secrets], problems };
|
|
1646
|
+
}
|
|
1647
|
+
async function hashFiles(files) {
|
|
1648
|
+
for (const file of files) file.hash = await contentHash(await readFile3(file.absolute));
|
|
1570
1649
|
}
|
|
1571
1650
|
async function withRetries(work, retryable, attempts = 4, delayMs = 500) {
|
|
1572
1651
|
for (let attempt = 1; ; attempt++) {
|
|
@@ -1575,7 +1654,7 @@ async function withRetries(work, retryable, attempts = 4, delayMs = 500) {
|
|
|
1575
1654
|
} catch (error) {
|
|
1576
1655
|
if (attempt >= attempts || !retryable(error)) throw error;
|
|
1577
1656
|
const wait = typeof delayMs === "function" ? delayMs(error, attempt) : delayMs * 2 ** (attempt - 1);
|
|
1578
|
-
await new Promise((
|
|
1657
|
+
await new Promise((resolve7) => setTimeout(resolve7, wait));
|
|
1579
1658
|
}
|
|
1580
1659
|
}
|
|
1581
1660
|
}
|
|
@@ -1590,6 +1669,337 @@ async function pool(items, limit, work) {
|
|
|
1590
1669
|
await Promise.all(runners);
|
|
1591
1670
|
}
|
|
1592
1671
|
|
|
1672
|
+
// ../../workers/builder/src/pipeline.ts
|
|
1673
|
+
import { existsSync } from "node:fs";
|
|
1674
|
+
import { copyFile, cp, lstat as lstat3, mkdir as mkdir3, readdir as readdir2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1675
|
+
import { dirname as dirname3, join as join4, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1676
|
+
var BuildError = class extends Error {
|
|
1677
|
+
retryable;
|
|
1678
|
+
constructor(message, retryable = false) {
|
|
1679
|
+
super(message);
|
|
1680
|
+
this.retryable = retryable;
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
var WRANGLER_CONFIGS = [
|
|
1684
|
+
"wrangler.json",
|
|
1685
|
+
"wrangler.jsonc",
|
|
1686
|
+
"wrangler.toml",
|
|
1687
|
+
".wrangler/deploy/config.json"
|
|
1688
|
+
];
|
|
1689
|
+
var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1690
|
+
function safeJoin(root, path) {
|
|
1691
|
+
const target = resolve2(root, path);
|
|
1692
|
+
if (target !== root && !target.startsWith(root + sep2))
|
|
1693
|
+
throw new BuildError(`Refusing to write outside the build folder: ${path}`);
|
|
1694
|
+
return target;
|
|
1695
|
+
}
|
|
1696
|
+
function defaultInstallCommand(files) {
|
|
1697
|
+
if (!files.has("package.json")) return null;
|
|
1698
|
+
if (files.has("pnpm-lock.yaml")) return "corepack pnpm install --frozen-lockfile";
|
|
1699
|
+
if (files.has("yarn.lock")) return "corepack yarn install --frozen-lockfile";
|
|
1700
|
+
if (files.has("package-lock.json") || files.has("npm-shrinkwrap.json")) return "npm ci";
|
|
1701
|
+
return "npm install";
|
|
1702
|
+
}
|
|
1703
|
+
function addPackagesCommand(files, packages) {
|
|
1704
|
+
const list = packages.map((p) => `'${p.replace(/'/g, "")}'`).join(" ");
|
|
1705
|
+
if (files.has("pnpm-lock.yaml")) return `corepack pnpm add -D ${list}`;
|
|
1706
|
+
if (files.has("yarn.lock")) return `corepack yarn add -D ${list}`;
|
|
1707
|
+
return `npm install --no-save --no-audit --no-fund ${list}`;
|
|
1708
|
+
}
|
|
1709
|
+
var packageName = (spec) => spec.replace(/^(@?[^@]+)(@.*)?$/, "$1");
|
|
1710
|
+
function maskSecrets(line, env) {
|
|
1711
|
+
let out = line;
|
|
1712
|
+
for (const value of Object.values(env)) {
|
|
1713
|
+
if (value.length >= 6) out = out.split(value).join("\u2022\u2022\u2022\u2022\u2022\u2022");
|
|
1714
|
+
}
|
|
1715
|
+
return out;
|
|
1716
|
+
}
|
|
1717
|
+
async function collect(dir, base = dir) {
|
|
1718
|
+
const out = [];
|
|
1719
|
+
for (const name of await readdir2(dir)) {
|
|
1720
|
+
const full = join4(dir, name);
|
|
1721
|
+
const info = await lstat3(full);
|
|
1722
|
+
if (info.isSymbolicLink()) continue;
|
|
1723
|
+
if (info.isDirectory()) out.push(...await collect(full, base));
|
|
1724
|
+
else if (info.isFile()) out.push(relative2(base, full).split(sep2).join("/"));
|
|
1725
|
+
}
|
|
1726
|
+
return out;
|
|
1727
|
+
}
|
|
1728
|
+
async function runPipeline(options) {
|
|
1729
|
+
const { cwd, work, spec, exec, log } = options;
|
|
1730
|
+
const written = options.written ?? [];
|
|
1731
|
+
const bundleServer = async (server, out2) => {
|
|
1732
|
+
const hasConfig = WRANGLER_CONFIGS.some((file) => existsSync(join4(cwd, file)));
|
|
1733
|
+
const bundle = join4(work, "server-bundle");
|
|
1734
|
+
const args = ["deploy", "--dry-run", "--outdir", bundle];
|
|
1735
|
+
if (!hasConfig) {
|
|
1736
|
+
if (!server.entry)
|
|
1737
|
+
throw new BuildError(
|
|
1738
|
+
"The build did not write a wrangler config. Check that the framework\u2019s edge adapter is installed and enabled."
|
|
1739
|
+
);
|
|
1740
|
+
if (!existsSync(safeJoin(cwd, server.entry)))
|
|
1741
|
+
throw new BuildError(
|
|
1742
|
+
`The build did not write ${server.entry}. Check that the framework\u2019s edge adapter is installed and enabled.`
|
|
1743
|
+
);
|
|
1744
|
+
args.push(
|
|
1745
|
+
server.entry,
|
|
1746
|
+
"--name",
|
|
1747
|
+
"hostedge-build",
|
|
1748
|
+
"--compatibility-date",
|
|
1749
|
+
server.compatibilityDate,
|
|
1750
|
+
"--compatibility-flags",
|
|
1751
|
+
"nodejs_compat"
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
log("info", "Bundling the server code into a Worker");
|
|
1755
|
+
await exec(`${options.wrangler} ${args.map(shellQuote).join(" ")}`);
|
|
1756
|
+
const target = safeJoin(out2, "_worker.js");
|
|
1757
|
+
await rm2(target, { recursive: true, force: true });
|
|
1758
|
+
let modules = 0;
|
|
1759
|
+
for (const file of await collect(bundle)) {
|
|
1760
|
+
if (file.endsWith(".map") || file === "README.md") continue;
|
|
1761
|
+
const dest = safeJoin(target, file);
|
|
1762
|
+
await mkdir3(dirname3(dest), { recursive: true });
|
|
1763
|
+
await copyFile(join4(bundle, file), dest);
|
|
1764
|
+
modules += 1;
|
|
1765
|
+
}
|
|
1766
|
+
if (modules === 0) throw new BuildError("Bundling the server code produced no modules.");
|
|
1767
|
+
log("success", `Server bundle: ${modules} module${modules === 1 ? "" : "s"}`);
|
|
1768
|
+
};
|
|
1769
|
+
if (spec.server?.files)
|
|
1770
|
+
for (const file of spec.server.files) {
|
|
1771
|
+
if (file.unless.some((name) => existsSync(safeJoin(cwd, name)))) continue;
|
|
1772
|
+
const target = safeJoin(cwd, file.path);
|
|
1773
|
+
await writeFile3(
|
|
1774
|
+
target,
|
|
1775
|
+
file.content.replaceAll("{{compatibilityDate}}", spec.server.compatibilityDate)
|
|
1776
|
+
);
|
|
1777
|
+
written.push(target);
|
|
1778
|
+
log("info", `No ${file.path} in the project: using HostEdge's default.`);
|
|
1779
|
+
}
|
|
1780
|
+
const install = spec.installCommand ?? defaultInstallCommand(new Set(await readdir2(cwd)));
|
|
1781
|
+
if (install && !options.skipInstall) {
|
|
1782
|
+
await exec(install);
|
|
1783
|
+
await options.afterInstall?.();
|
|
1784
|
+
}
|
|
1785
|
+
if (spec.server?.packages?.length) {
|
|
1786
|
+
const missing = spec.server.packages.filter(
|
|
1787
|
+
(pkg) => !existsSync(join4(cwd, "node_modules", packageName(pkg), "package.json"))
|
|
1788
|
+
);
|
|
1789
|
+
if (missing.length > 0 && options.installMissingPackages === false)
|
|
1790
|
+
throw new BuildError(
|
|
1791
|
+
`${missing.map(packageName).join(" and ")} ${missing.length === 1 ? "is" : "are"} not installed in this project.`
|
|
1792
|
+
);
|
|
1793
|
+
if (missing.length > 0) {
|
|
1794
|
+
log("info", `Adding ${missing.map(packageName).join(" and ")} for the build`);
|
|
1795
|
+
await exec(addPackagesCommand(new Set(await readdir2(cwd)), missing));
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (spec.buildCommand) await exec(spec.buildCommand);
|
|
1799
|
+
const built = safeJoin(cwd, spec.outputDirectory || ".");
|
|
1800
|
+
if (!existsSync(built))
|
|
1801
|
+
throw new BuildError(
|
|
1802
|
+
`The build finished but \u201C${spec.outputDirectory ?? "."}\u201D does not exist. Check the output directory.`
|
|
1803
|
+
);
|
|
1804
|
+
let out = built;
|
|
1805
|
+
if (options.stageOutput) {
|
|
1806
|
+
out = join4(work, "output");
|
|
1807
|
+
await rm2(out, { recursive: true, force: true });
|
|
1808
|
+
await cp(built, out, { recursive: true, dereference: false });
|
|
1809
|
+
}
|
|
1810
|
+
if (spec.server) await bundleServer(spec.server, out);
|
|
1811
|
+
for (const name of spec.server?.hide ?? [])
|
|
1812
|
+
await rm2(safeJoin(out, name), { recursive: true, force: true });
|
|
1813
|
+
const extra = spec.server?.extraOutput;
|
|
1814
|
+
if (extra && existsSync(safeJoin(cwd, extra.from))) {
|
|
1815
|
+
const from = safeJoin(cwd, extra.from);
|
|
1816
|
+
for (const file of await collect(from)) {
|
|
1817
|
+
const dest = safeJoin(out, `${extra.to}/${file}`);
|
|
1818
|
+
await mkdir3(dirname3(dest), { recursive: true });
|
|
1819
|
+
await copyFile(join4(from, file), dest);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const files = await collect(out);
|
|
1823
|
+
let bytes = 0;
|
|
1824
|
+
for (const file of files) bytes += (await lstat3(join4(out, file))).size;
|
|
1825
|
+
if (bytes > options.maxOutputBytes)
|
|
1826
|
+
throw new BuildError(
|
|
1827
|
+
`The output is larger than ${Math.round(options.maxOutputBytes / 1024 / 1024)} MB.`
|
|
1828
|
+
);
|
|
1829
|
+
return { out, files, bytes, written };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// src/local-build.ts
|
|
1833
|
+
import { spawn } from "node:child_process";
|
|
1834
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1835
|
+
import { mkdtemp, readFile as readFile4, rm as rm3 } from "node:fs/promises";
|
|
1836
|
+
import { tmpdir } from "node:os";
|
|
1837
|
+
import { join as join5 } from "node:path";
|
|
1838
|
+
var BUILD_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1839
|
+
var MAX_OUTPUT_BYTES = 1024 * 1024 * 1024;
|
|
1840
|
+
var HASH_CONCURRENCY = 8;
|
|
1841
|
+
var ENV_FILES = [".env", ".env.local", ".env.production", ".env.production.local"];
|
|
1842
|
+
var shellQuote2 = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1843
|
+
async function undeclaredEnvKeys(folders, known) {
|
|
1844
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1845
|
+
for (const folder of new Set(folders))
|
|
1846
|
+
for (const name of ENV_FILES) {
|
|
1847
|
+
let text;
|
|
1848
|
+
try {
|
|
1849
|
+
text = await readFile4(join5(folder, name), "utf8");
|
|
1850
|
+
} catch {
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
for (const entry of parseDotEnv(text).entries) if (!known.has(entry.key)) keys.add(entry.key);
|
|
1854
|
+
}
|
|
1855
|
+
return [...keys].sort();
|
|
1856
|
+
}
|
|
1857
|
+
async function localBuild(r, api, input) {
|
|
1858
|
+
const s = r.s;
|
|
1859
|
+
const skip = (reason) => {
|
|
1860
|
+
r.say(`${s.dim("\u2013")} ${reason} Building on HostEdge instead.
|
|
1861
|
+
`);
|
|
1862
|
+
return null;
|
|
1863
|
+
};
|
|
1864
|
+
if (r.io.env.HOSTEDGE_REMOTE_BUILD) return null;
|
|
1865
|
+
if (process.platform === "win32") return null;
|
|
1866
|
+
let plan;
|
|
1867
|
+
try {
|
|
1868
|
+
plan = await api.request("/api/v1/build-plan", {
|
|
1869
|
+
json: {
|
|
1870
|
+
projectId: input.projectId ?? void 0,
|
|
1871
|
+
settings: input.projectId ? void 0 : input.detected,
|
|
1872
|
+
paths: input.sourcePaths.filter((p) => /(^|\/)api\//.test(p))
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
if (error instanceof ApiError && (error.status === 404 || error.status === 405)) return null;
|
|
1877
|
+
throw error;
|
|
1878
|
+
}
|
|
1879
|
+
const { settings } = plan;
|
|
1880
|
+
if (!settings.buildCommand && !plan.server) return null;
|
|
1881
|
+
let projectEnv = {};
|
|
1882
|
+
if (input.projectId) {
|
|
1883
|
+
try {
|
|
1884
|
+
const response = await api.request(
|
|
1885
|
+
`/api/v1/projects/${input.projectId}/env?${new URLSearchParams({ environment: input.environment })}`
|
|
1886
|
+
);
|
|
1887
|
+
projectEnv = Object.fromEntries(response.variables.map((v) => [v.key, v.value]));
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403))
|
|
1890
|
+
return skip("This token can\u2019t read the project\u2019s environment variables.");
|
|
1891
|
+
throw error;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const cwd = join5(input.dir, settings.rootDirectory);
|
|
1895
|
+
if (!existsSync2(cwd))
|
|
1896
|
+
return skip(`The root directory \u201C${settings.rootDirectory}\u201D does not exist.`);
|
|
1897
|
+
const shadowed = await undeclaredEnvKeys([cwd, input.dir], new Set(Object.keys(projectEnv)));
|
|
1898
|
+
if (shadowed.length > 0)
|
|
1899
|
+
return skip(
|
|
1900
|
+
`Your .env files define ${shadowed.slice(0, 4).join(", ")}${shadowed.length > 4 ? ` and ${shadowed.length - 4} more` : ""}, which are not set on HostEdge, so a build here would differ from the one HostEdge runs (set them with the dashboard if the build needs them).`
|
|
1901
|
+
);
|
|
1902
|
+
const framework = FRAMEWORK_INFO[settings.framework].name;
|
|
1903
|
+
r.say(`Building ${framework} on this machine (use --remote-build to build on HostEdge)
|
|
1904
|
+
`);
|
|
1905
|
+
const env = {};
|
|
1906
|
+
for (const [key, value] of Object.entries(r.io.env)) if (value !== void 0) env[key] = value;
|
|
1907
|
+
Object.assign(env, { CI: "1", HOSTEDGE: "1", WRANGLER_SEND_METRICS: "false" }, projectEnv);
|
|
1908
|
+
const emit = (level, msg) => {
|
|
1909
|
+
const text = maskSecrets(msg, projectEnv);
|
|
1910
|
+
const line = level === "error" ? s.red(text) : level === "warn" ? s.yellow(text) : level === "success" ? s.green(text) : text;
|
|
1911
|
+
r.say(`${s.dim("\u2502")} ${line}
|
|
1912
|
+
`);
|
|
1913
|
+
};
|
|
1914
|
+
const exec = (command) => new Promise((resolve7, reject) => {
|
|
1915
|
+
emit("info", `$ ${command}`);
|
|
1916
|
+
const child = spawn("sh", ["-c", command], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
1917
|
+
const timer = setTimeout(() => {
|
|
1918
|
+
child.kill("SIGKILL");
|
|
1919
|
+
reject(new BuildError("The build ran longer than 15 minutes and was stopped."));
|
|
1920
|
+
}, BUILD_TIMEOUT_MS);
|
|
1921
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
1922
|
+
let buffer = "";
|
|
1923
|
+
stream.setEncoding("utf8");
|
|
1924
|
+
stream.on("data", (chunk) => {
|
|
1925
|
+
const parts = (buffer + chunk).split(/\r?\n/);
|
|
1926
|
+
buffer = parts.pop() ?? "";
|
|
1927
|
+
for (const part of parts) emit("info", part);
|
|
1928
|
+
});
|
|
1929
|
+
stream.on("end", () => {
|
|
1930
|
+
if (buffer) emit("info", buffer);
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
child.on("error", (error) => {
|
|
1934
|
+
clearTimeout(timer);
|
|
1935
|
+
reject(new BuildError(`Could not start the command: ${error.message}`));
|
|
1936
|
+
});
|
|
1937
|
+
child.on("close", (code) => {
|
|
1938
|
+
clearTimeout(timer);
|
|
1939
|
+
if (code === 0) resolve7();
|
|
1940
|
+
else reject(new BuildError(`\u201C${command}\u201D exited with code ${code ?? "unknown"}.`));
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
const localWrangler = [cwd, input.dir].map((folder) => join5(folder, "node_modules", ".bin", "wrangler")).find((path) => existsSync2(path));
|
|
1944
|
+
const work = await mkdtemp(join5(tmpdir(), "hostedge-build-"));
|
|
1945
|
+
const written = [];
|
|
1946
|
+
const cleanup = async () => {
|
|
1947
|
+
await Promise.all(written.map((file) => rm3(file, { force: true })));
|
|
1948
|
+
await rm3(work, { recursive: true, force: true });
|
|
1949
|
+
};
|
|
1950
|
+
try {
|
|
1951
|
+
const spec = {
|
|
1952
|
+
buildId: `local-${Date.now()}`,
|
|
1953
|
+
rootDirectory: settings.rootDirectory,
|
|
1954
|
+
installCommand: settings.installCommand,
|
|
1955
|
+
buildCommand: settings.buildCommand,
|
|
1956
|
+
outputDirectory: settings.outputDirectory,
|
|
1957
|
+
server: plan.server,
|
|
1958
|
+
env: projectEnv
|
|
1959
|
+
};
|
|
1960
|
+
const hasModules = existsSync2(join5(cwd, "node_modules")) || existsSync2(join5(input.dir, "node_modules"));
|
|
1961
|
+
const result = await runPipeline({
|
|
1962
|
+
cwd,
|
|
1963
|
+
work,
|
|
1964
|
+
spec,
|
|
1965
|
+
exec,
|
|
1966
|
+
log: emit,
|
|
1967
|
+
wrangler: localWrangler ? shellQuote2(localWrangler) : `npx --yes wrangler@${plan.wrangler}`,
|
|
1968
|
+
maxOutputBytes: MAX_OUTPUT_BYTES,
|
|
1969
|
+
skipInstall: hasModules,
|
|
1970
|
+
installMissingPackages: false,
|
|
1971
|
+
written,
|
|
1972
|
+
stageOutput: true
|
|
1973
|
+
});
|
|
1974
|
+
if (hasModules) emit("info", "Using the dependencies already installed in this folder.");
|
|
1975
|
+
const files = result.files.map((path) => ({
|
|
1976
|
+
path,
|
|
1977
|
+
size: 0,
|
|
1978
|
+
hash: "",
|
|
1979
|
+
absolute: join5(result.out, path)
|
|
1980
|
+
}));
|
|
1981
|
+
await pool(files, HASH_CONCURRENCY, async (file) => {
|
|
1982
|
+
const bytes = await readFile4(file.absolute);
|
|
1983
|
+
file.size = bytes.byteLength;
|
|
1984
|
+
file.hash = await contentHash(bytes);
|
|
1985
|
+
});
|
|
1986
|
+
const check = checkManifest(
|
|
1987
|
+
files.map((f) => ({ path: f.path, size: f.size, hash: f.hash })),
|
|
1988
|
+
{ maxDeploymentBytes: Number.MAX_SAFE_INTEGER, maxFiles: Number.MAX_SAFE_INTEGER },
|
|
1989
|
+
{ prebuilt: true }
|
|
1990
|
+
);
|
|
1991
|
+
const [problem] = check.problems;
|
|
1992
|
+
if (problem) throw new BuildError(`${problem.path ? `${problem.path}: ` : ""}${problem.reason}`);
|
|
1993
|
+
const kept = new Set(check.files.map((f) => f.path));
|
|
1994
|
+
return { files: files.filter((f) => kept.has(f.path)), cleanup };
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
await cleanup();
|
|
1997
|
+
return skip(
|
|
1998
|
+
`The local build did not work${error instanceof Error ? `: ${error.message}` : "."}`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1593
2003
|
// src/output.ts
|
|
1594
2004
|
function styles(io2, stream = "stdout", noColor = false) {
|
|
1595
2005
|
const tty = stream === "stdout" ? io2.isTty : io2.isErrTty;
|
|
@@ -1717,11 +2127,15 @@ async function lookup(api, deploymentId) {
|
|
|
1717
2127
|
async function deploy(r, options) {
|
|
1718
2128
|
const { io: io2 } = r;
|
|
1719
2129
|
const started = Date.now();
|
|
1720
|
-
const dir =
|
|
2130
|
+
const dir = resolve3(io2.cwd, options.dir ?? ".");
|
|
1721
2131
|
const s = r.s;
|
|
1722
2132
|
const api = await connect(io2);
|
|
1723
2133
|
const link = await readLink(dir);
|
|
1724
|
-
const { files, skipped, secrets } = await collectFiles(dir
|
|
2134
|
+
const { files, skipped, secrets, problems } = await collectFiles(dir, {
|
|
2135
|
+
maxFiles: 1e5,
|
|
2136
|
+
hash: false,
|
|
2137
|
+
strict: false
|
|
2138
|
+
});
|
|
1725
2139
|
if (files.length === 0) throw new CliError(`No files to deploy in ${dir}.`);
|
|
1726
2140
|
for (const secret of secrets)
|
|
1727
2141
|
r.say(
|
|
@@ -1735,100 +2149,127 @@ async function deploy(r, options) {
|
|
|
1735
2149
|
`
|
|
1736
2150
|
)
|
|
1737
2151
|
);
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1750
|
-
|
|
2152
|
+
let pkg = null;
|
|
2153
|
+
try {
|
|
2154
|
+
pkg = await readFile5(join6(dir, "package.json"), "utf8");
|
|
2155
|
+
} catch {
|
|
2156
|
+
}
|
|
2157
|
+
const detected = detectFramework(
|
|
2158
|
+
files.map((f) => f.path),
|
|
2159
|
+
pkg
|
|
2160
|
+
).settings;
|
|
2161
|
+
const environment = options.preview ? "preview" : "production";
|
|
2162
|
+
const submit = async (toSend, prebuilt) => {
|
|
2163
|
+
const total = toSend.reduce((sum, f) => sum + f.size, 0);
|
|
2164
|
+
const { missing } = await api.request("/api/v1/uploads", {
|
|
2165
|
+
json: { hashes: [...new Set(toSend.map((f) => f.hash))] }
|
|
2166
|
+
});
|
|
2167
|
+
const needed = new Set(missing);
|
|
2168
|
+
const toUpload = toSend.filter((f) => {
|
|
2169
|
+
if (!needed.has(f.hash)) return false;
|
|
2170
|
+
needed.delete(f.hash);
|
|
2171
|
+
return true;
|
|
2172
|
+
});
|
|
2173
|
+
const uploadBytes = toUpload.reduce((sum, f) => sum + f.size, 0);
|
|
2174
|
+
r.say(
|
|
2175
|
+
`Uploading ${toUpload.length} of ${toSend.length} files (${formatBytes(uploadBytes)} of ${formatBytes(total)}; the rest are already on HostEdge)
|
|
1751
2176
|
`
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2177
|
+
);
|
|
2178
|
+
let uploaded = 0;
|
|
2179
|
+
let reported = 0;
|
|
2180
|
+
const step = Math.max(1, Math.ceil(toUpload.length / 10));
|
|
2181
|
+
const progress = (count2) => {
|
|
2182
|
+
uploaded += count2;
|
|
2183
|
+
if (toUpload.length > 20 && uploaded - reported >= step && uploaded < toUpload.length) {
|
|
2184
|
+
reported = uploaded;
|
|
2185
|
+
r.say(`${s.dim(` ${uploaded}/${toUpload.length} uploaded`)}
|
|
1761
2186
|
`);
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
const send = (path, init2, bytes) => withRetries(
|
|
2190
|
+
() => api.raw(path, {
|
|
2191
|
+
...init2,
|
|
2192
|
+
// 30 s plus a few more for every MB, so a big file on a slow line can finish.
|
|
2193
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS + Math.ceil(bytes / 1048576) * 4e3)
|
|
2194
|
+
}),
|
|
2195
|
+
temporary,
|
|
2196
|
+
6,
|
|
2197
|
+
retryDelay
|
|
2198
|
+
);
|
|
2199
|
+
let batching = true;
|
|
2200
|
+
await pool(batchesOf(toUpload), UPLOAD_CONCURRENCY, async (batch) => {
|
|
2201
|
+
if (batching && (batch.length > 1 || (batch[0]?.size ?? 0) <= BATCH_BYTES)) {
|
|
2202
|
+
const form = new FormData();
|
|
2203
|
+
for (const file of batch)
|
|
2204
|
+
form.append(file.hash, new Blob([await readFile5(file.absolute)]), file.hash);
|
|
2205
|
+
try {
|
|
2206
|
+
await send(
|
|
2207
|
+
"/api/v1/uploads/batch",
|
|
2208
|
+
{ method: "POST", body: form },
|
|
2209
|
+
batch.reduce((sum, file) => sum + file.size, 0)
|
|
2210
|
+
);
|
|
2211
|
+
progress(batch.length);
|
|
2212
|
+
return;
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
if (!(error instanceof ApiError) || error.status !== 404 && error.status !== 405)
|
|
2215
|
+
throw error;
|
|
2216
|
+
batching = false;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
for (const file of batch) {
|
|
1781
2220
|
await send(
|
|
1782
|
-
|
|
1783
|
-
{
|
|
1784
|
-
|
|
2221
|
+
`/api/v1/uploads/${file.hash}`,
|
|
2222
|
+
{
|
|
2223
|
+
method: "PUT",
|
|
2224
|
+
headers: { "content-type": "application/octet-stream" },
|
|
2225
|
+
body: await readFile5(file.absolute)
|
|
2226
|
+
},
|
|
2227
|
+
file.size
|
|
1785
2228
|
);
|
|
1786
|
-
progress(
|
|
1787
|
-
return;
|
|
1788
|
-
} catch (error) {
|
|
1789
|
-
if (!(error instanceof ApiError) || error.status !== 404 && error.status !== 405)
|
|
1790
|
-
throw error;
|
|
1791
|
-
batching = false;
|
|
2229
|
+
progress(1);
|
|
1792
2230
|
}
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
{
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2231
|
+
});
|
|
2232
|
+
return api.request("/api/v1/deployments", {
|
|
2233
|
+
json: {
|
|
2234
|
+
projectId: link?.projectId,
|
|
2235
|
+
project: link ? void 0 : { name: basename(dir), settings: detected },
|
|
2236
|
+
files: toSend.map((f) => ({ path: f.path, hash: f.hash, size: f.size })),
|
|
2237
|
+
environment,
|
|
2238
|
+
commitMessage: options.message ?? null,
|
|
2239
|
+
...prebuilt ? { prebuilt: true } : {}
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
};
|
|
2243
|
+
let built = null;
|
|
2244
|
+
if (!options.remoteBuild)
|
|
2245
|
+
built = await localBuild(r, api, {
|
|
2246
|
+
dir,
|
|
2247
|
+
projectId: link?.projectId ?? null,
|
|
2248
|
+
environment,
|
|
2249
|
+
detected,
|
|
2250
|
+
sourcePaths: files.map((f) => f.path)
|
|
2251
|
+
});
|
|
2252
|
+
let created = null;
|
|
2253
|
+
if (built) {
|
|
1810
2254
|
try {
|
|
1811
|
-
|
|
1812
|
-
} catch {
|
|
2255
|
+
created = await submit(built.files, true);
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
if (!(error instanceof ApiError) || error.status !== 400 && error.status !== 413)
|
|
2258
|
+
throw error;
|
|
2259
|
+
r.say(
|
|
2260
|
+
`${s.dim("\u2013")} HostEdge refused the built output (${error.message}) Building on HostEdge instead.
|
|
2261
|
+
`
|
|
2262
|
+
);
|
|
2263
|
+
} finally {
|
|
2264
|
+
await built.cleanup();
|
|
1813
2265
|
}
|
|
1814
|
-
project = {
|
|
1815
|
-
name: basename(dir),
|
|
1816
|
-
settings: detectFramework(
|
|
1817
|
-
files.map((f) => f.path),
|
|
1818
|
-
pkg
|
|
1819
|
-
).settings
|
|
1820
|
-
};
|
|
1821
2266
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
environment,
|
|
1829
|
-
commitMessage: options.message ?? null
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
2267
|
+
if (!created) {
|
|
2268
|
+
const [problem] = problems;
|
|
2269
|
+
if (problem) throw new CliError(`${problem.path}: ${problem.reason}`);
|
|
2270
|
+
await hashFiles(files);
|
|
2271
|
+
created = await submit(files, false);
|
|
2272
|
+
}
|
|
1832
2273
|
if (!link) {
|
|
1833
2274
|
const me = await api.request("/api/v1/me");
|
|
1834
2275
|
await writeLink(dir, {
|
|
@@ -1885,11 +2326,11 @@ async function deploy(r, options) {
|
|
|
1885
2326
|
}
|
|
1886
2327
|
|
|
1887
2328
|
// src/commands/env.ts
|
|
1888
|
-
import { writeFile as
|
|
1889
|
-
import { resolve as
|
|
2329
|
+
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
2330
|
+
import { resolve as resolve5 } from "node:path";
|
|
1890
2331
|
|
|
1891
2332
|
// src/commands/logs.ts
|
|
1892
|
-
import { resolve as
|
|
2333
|
+
import { resolve as resolve4 } from "node:path";
|
|
1893
2334
|
var POLL_MS = 2e3;
|
|
1894
2335
|
var LEVELS = ["debug", "info", "warn", "error"];
|
|
1895
2336
|
var UNIT_MS = { m: 6e4, h: 36e5, d: 864e5 };
|
|
@@ -1899,7 +2340,7 @@ var RANGES = [
|
|
|
1899
2340
|
{ name: "7d", ms: 7 * UNIT_MS.d }
|
|
1900
2341
|
];
|
|
1901
2342
|
async function requireLink(io2, dir = ".") {
|
|
1902
|
-
const link = await readLink(
|
|
2343
|
+
const link = await readLink(resolve4(io2.cwd, dir));
|
|
1903
2344
|
if (!link)
|
|
1904
2345
|
throw new CliError(
|
|
1905
2346
|
"This folder isn\u2019t linked to a project. Run `hostedge init` or `hostedge deploy`.",
|
|
@@ -1975,8 +2416,8 @@ async function envPull(r, options) {
|
|
|
1975
2416
|
`/api/v1/projects/${link.projectId}/env?${new URLSearchParams({ environment, format: "dotenv" })}`
|
|
1976
2417
|
);
|
|
1977
2418
|
const text = await response.text();
|
|
1978
|
-
const path =
|
|
1979
|
-
await
|
|
2419
|
+
const path = resolve5(io2.cwd, file);
|
|
2420
|
+
await writeFile4(path, text, { mode: 384 });
|
|
1980
2421
|
const count2 = text.split("\n").filter(Boolean).length;
|
|
1981
2422
|
const ignored = await ensureGitignored(io2.cwd, file);
|
|
1982
2423
|
const s = r.s;
|
|
@@ -1989,11 +2430,11 @@ async function envPull(r, options) {
|
|
|
1989
2430
|
}
|
|
1990
2431
|
|
|
1991
2432
|
// src/commands/init.ts
|
|
1992
|
-
import { readFile as
|
|
1993
|
-
import { basename as basename2, join as
|
|
2433
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2434
|
+
import { basename as basename2, join as join7, resolve as resolve6 } from "node:path";
|
|
1994
2435
|
async function init(r, options) {
|
|
1995
2436
|
const { io: io2 } = r;
|
|
1996
|
-
const dir =
|
|
2437
|
+
const dir = resolve6(io2.cwd, options.dir ?? ".");
|
|
1997
2438
|
const s = r.s;
|
|
1998
2439
|
const existing = await readLink(dir);
|
|
1999
2440
|
let kit = null;
|
|
@@ -2044,7 +2485,7 @@ async function init(r, options) {
|
|
|
2044
2485
|
const { files: found } = await collectFiles(dir, { maxFiles: 1e5, hash: false });
|
|
2045
2486
|
let pkg = null;
|
|
2046
2487
|
try {
|
|
2047
|
-
pkg = await
|
|
2488
|
+
pkg = await readFile6(join7(dir, "package.json"), "utf8");
|
|
2048
2489
|
} catch {
|
|
2049
2490
|
}
|
|
2050
2491
|
const detection = detectFramework(
|
|
@@ -2080,10 +2521,10 @@ async function init(r, options) {
|
|
|
2080
2521
|
|
|
2081
2522
|
// src/commands/share.ts
|
|
2082
2523
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2083
|
-
import { chmod as chmod2, mkdir as
|
|
2524
|
+
import { chmod as chmod2, mkdir as mkdir4, rename, rm as rm4, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
|
|
2084
2525
|
import { connect as netConnect } from "node:net";
|
|
2085
2526
|
import { homedir as homedir2 } from "node:os";
|
|
2086
|
-
import { delimiter, join as
|
|
2527
|
+
import { delimiter, join as join8 } from "node:path";
|
|
2087
2528
|
|
|
2088
2529
|
// ../../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
|
|
2089
2530
|
var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
|
|
@@ -2793,9 +3234,9 @@ async function findCloudflared(r) {
|
|
|
2793
3234
|
if (override) return override;
|
|
2794
3235
|
const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
2795
3236
|
for (const dir of (io2.env.PATH ?? "").split(delimiter).filter(Boolean))
|
|
2796
|
-
if (await exists(
|
|
2797
|
-
const binDir =
|
|
2798
|
-
const own =
|
|
3237
|
+
if (await exists(join8(dir, exe))) return join8(dir, exe);
|
|
3238
|
+
const binDir = join8(io2.env.HOME ?? homedir2(), ".hostedge", "bin");
|
|
3239
|
+
const own = join8(binDir, exe);
|
|
2799
3240
|
if (await exists(own)) return own;
|
|
2800
3241
|
const asset = cloudflaredAsset(process.platform, process.arch);
|
|
2801
3242
|
if (!asset)
|
|
@@ -2805,16 +3246,16 @@ async function findCloudflared(r) {
|
|
|
2805
3246
|
r.warn("Downloading the tunnel helper (one time)\u2026\n");
|
|
2806
3247
|
const response = await io2.fetch(`${RELEASES}/${asset}`);
|
|
2807
3248
|
if (!response.ok) throw new CliError(`Could not download the tunnel helper (${response.status}).`);
|
|
2808
|
-
await
|
|
2809
|
-
const tmp =
|
|
2810
|
-
await
|
|
3249
|
+
await mkdir4(binDir, { recursive: true });
|
|
3250
|
+
const tmp = join8(binDir, `${asset}.download`);
|
|
3251
|
+
await writeFile5(tmp, new Uint8Array(await response.arrayBuffer()));
|
|
2811
3252
|
if (asset.endsWith(".tgz")) {
|
|
2812
|
-
await new Promise((
|
|
3253
|
+
await new Promise((resolve7, reject) => {
|
|
2813
3254
|
const tar = nodeSpawn("tar", ["-xzf", tmp, "-C", binDir], { stdio: "ignore" });
|
|
2814
3255
|
tar.on("error", reject);
|
|
2815
|
-
tar.on("exit", (code) => code === 0 ?
|
|
3256
|
+
tar.on("exit", (code) => code === 0 ? resolve7() : reject(new Error(`tar exited ${code}`)));
|
|
2816
3257
|
});
|
|
2817
|
-
await
|
|
3258
|
+
await rm4(tmp, { force: true });
|
|
2818
3259
|
} else {
|
|
2819
3260
|
await rename(tmp, own);
|
|
2820
3261
|
}
|
|
@@ -2822,16 +3263,16 @@ async function findCloudflared(r) {
|
|
|
2822
3263
|
return own;
|
|
2823
3264
|
}
|
|
2824
3265
|
function listening(port) {
|
|
2825
|
-
return new Promise((
|
|
3266
|
+
return new Promise((resolve7) => {
|
|
2826
3267
|
const socket = netConnect({ host: "127.0.0.1", port });
|
|
2827
3268
|
socket.once("connect", () => {
|
|
2828
3269
|
socket.destroy();
|
|
2829
|
-
|
|
3270
|
+
resolve7(true);
|
|
2830
3271
|
});
|
|
2831
|
-
socket.once("error", () =>
|
|
3272
|
+
socket.once("error", () => resolve7(false));
|
|
2832
3273
|
socket.setTimeout(1e3, () => {
|
|
2833
3274
|
socket.destroy();
|
|
2834
|
-
|
|
3275
|
+
resolve7(false);
|
|
2835
3276
|
});
|
|
2836
3277
|
});
|
|
2837
3278
|
}
|
|
@@ -2864,8 +3305,8 @@ async function share(r, options) {
|
|
|
2864
3305
|
"--http-host-header",
|
|
2865
3306
|
hostHeader
|
|
2866
3307
|
];
|
|
2867
|
-
const
|
|
2868
|
-
const child =
|
|
3308
|
+
const spawn3 = options.spawn ?? nodeSpawn;
|
|
3309
|
+
const child = spawn3(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2869
3310
|
const progress = { announced: false };
|
|
2870
3311
|
const tail = [];
|
|
2871
3312
|
const remember = (chunk) => {
|
|
@@ -2924,15 +3365,15 @@ ${s.bold("Your localhost is live:")} ${s.cyan(url)}
|
|
|
2924
3365
|
child.stdout?.on("data", watch);
|
|
2925
3366
|
child.stderr?.on("data", watch);
|
|
2926
3367
|
}
|
|
2927
|
-
const exitCode = await new Promise((
|
|
3368
|
+
const exitCode = await new Promise((resolve7) => {
|
|
2928
3369
|
const stop = () => {
|
|
2929
3370
|
child.kill("SIGTERM");
|
|
2930
3371
|
};
|
|
2931
3372
|
options.signal?.addEventListener("abort", stop, { once: true });
|
|
2932
|
-
child.on("error", () =>
|
|
3373
|
+
child.on("error", () => resolve7(1));
|
|
2933
3374
|
child.on("exit", (code) => {
|
|
2934
3375
|
options.signal?.removeEventListener("abort", stop);
|
|
2935
|
-
|
|
3376
|
+
resolve7(options.signal?.aborted ? 0 : code ?? 1);
|
|
2936
3377
|
});
|
|
2937
3378
|
});
|
|
2938
3379
|
if (api && opened.share) {
|
|
@@ -3034,7 +3475,7 @@ var Reporter = class {
|
|
|
3034
3475
|
};
|
|
3035
3476
|
|
|
3036
3477
|
// src/cli.ts
|
|
3037
|
-
var VERSION = "0.
|
|
3478
|
+
var VERSION = "0.2.0";
|
|
3038
3479
|
var HELP = `hostedge \u2014 hosting your AI agent can use. Deploy from your terminal, CI or an agent.
|
|
3039
3480
|
|
|
3040
3481
|
Usage
|
|
@@ -3046,8 +3487,10 @@ Usage
|
|
|
3046
3487
|
Link a folder to a project; --ai also writes
|
|
3047
3488
|
AGENTS.md, CLAUDE.md, .cursor/rules and a Claude skill
|
|
3048
3489
|
hostedge ai-kit [dir] [--force] Write the assistant instruction files only
|
|
3049
|
-
hostedge deploy [dir] [--preview] [--message <text>] [--wait | --no-wait]
|
|
3050
|
-
|
|
3490
|
+
hostedge deploy [dir] [--preview] [--message <text>] [--wait | --no-wait] [--remote-build]
|
|
3491
|
+
Deploy a folder (waits by default). Projects that need a
|
|
3492
|
+
build are built on this machine and only the output goes
|
|
3493
|
+
up; --remote-build has HostEdge build the source instead
|
|
3051
3494
|
hostedge logs [--follow] [--level <level>] [--search <text>] [--since 30m|1h|24h|7d]
|
|
3052
3495
|
Show runtime logs (--json: one JSON object per line)
|
|
3053
3496
|
hostedge env pull [--environment <name>] [--file <path>]
|
|
@@ -3067,6 +3510,7 @@ Options for every command
|
|
|
3067
3510
|
Environment
|
|
3068
3511
|
HOSTEDGE_TOKEN API token (CI); overrides the saved login. Never printed.
|
|
3069
3512
|
HOSTEDGE_API_URL HostEdge URL (default https://hostedge.app)
|
|
3513
|
+
HOSTEDGE_REMOTE_BUILD Same as --remote-build for every deploy
|
|
3070
3514
|
HOSTEDGE_TUNNEL_HELPER Path to the tunnel helper (otherwise PATH, then a one-time download)
|
|
3071
3515
|
NO_COLOR Disable colors
|
|
3072
3516
|
CI Treated like --yes
|
|
@@ -3121,6 +3565,7 @@ async function run(argv, baseIo, signal) {
|
|
|
3121
3565
|
message: { type: "string", short: "m" },
|
|
3122
3566
|
wait: { type: "boolean" },
|
|
3123
3567
|
"no-wait": { type: "boolean" },
|
|
3568
|
+
"remote-build": { type: "boolean" },
|
|
3124
3569
|
follow: { type: "boolean", short: "f" },
|
|
3125
3570
|
level: { type: "string" },
|
|
3126
3571
|
search: { type: "string" },
|
|
@@ -3186,7 +3631,8 @@ Run \`hostedge --help\` for usage.`, EXIT.usage);
|
|
|
3186
3631
|
dir: rest[0],
|
|
3187
3632
|
preview: values.preview === true,
|
|
3188
3633
|
message: str(values.message),
|
|
3189
|
-
wait: values["no-wait"] !== true
|
|
3634
|
+
wait: values["no-wait"] !== true,
|
|
3635
|
+
remoteBuild: values["remote-build"] === true
|
|
3190
3636
|
});
|
|
3191
3637
|
case "logs":
|
|
3192
3638
|
return await logs(r, {
|
|
@@ -3232,16 +3678,16 @@ Run \`hostedge --help\` for usage.`, EXIT.usage);
|
|
|
3232
3678
|
// src/main.ts
|
|
3233
3679
|
function openUrl(url) {
|
|
3234
3680
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
3235
|
-
return new Promise((
|
|
3681
|
+
return new Promise((resolve7) => {
|
|
3236
3682
|
try {
|
|
3237
|
-
const child =
|
|
3238
|
-
child.on("error", () =>
|
|
3683
|
+
const child = spawn2(command, [url], { stdio: "ignore", detached: true });
|
|
3684
|
+
child.on("error", () => resolve7(false));
|
|
3239
3685
|
child.on("spawn", () => {
|
|
3240
3686
|
child.unref();
|
|
3241
|
-
|
|
3687
|
+
resolve7(true);
|
|
3242
3688
|
});
|
|
3243
3689
|
} catch {
|
|
3244
|
-
|
|
3690
|
+
resolve7(false);
|
|
3245
3691
|
}
|
|
3246
3692
|
});
|
|
3247
3693
|
}
|
|
@@ -3261,7 +3707,7 @@ var io = {
|
|
|
3261
3707
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
3262
3708
|
return Buffer.concat(chunks).toString("utf8");
|
|
3263
3709
|
},
|
|
3264
|
-
sleep: (ms) => new Promise((
|
|
3710
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
3265
3711
|
};
|
|
3266
3712
|
var controller = new AbortController();
|
|
3267
3713
|
process.on("SIGINT", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hostedge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Hosting your AI agent can use. Deploy websites and apps to the edge, read logs and manage env vars from the terminal, CI or an AI agent (Claude Code, Cursor, Codex). JSON output, stable exit codes, MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hostedge",
|
|
@@ -16,11 +16,6 @@
|
|
|
16
16
|
"cloudflare-workers"
|
|
17
17
|
],
|
|
18
18
|
"homepage": "https://hostedge.app/docs/cli",
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/ybenjaa-dev/appdock.git",
|
|
22
|
-
"directory": "apps/cli"
|
|
23
|
-
},
|
|
24
19
|
"type": "module",
|
|
25
20
|
"bin": {
|
|
26
21
|
"hostedge": "dist/hostedge.js"
|
|
@@ -33,23 +28,24 @@
|
|
|
33
28
|
},
|
|
34
29
|
"publishConfig": {
|
|
35
30
|
"access": "public",
|
|
36
|
-
"provenance":
|
|
31
|
+
"provenance": false
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node build.mjs",
|
|
35
|
+
"lint": "eslint .",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"test": "vitest run --coverage"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
40
|
+
"@hostedge/builder": "workspace:*",
|
|
41
|
+
"@hostedge/config": "workspace:*",
|
|
42
|
+
"@hostedge/core": "workspace:*",
|
|
39
43
|
"@types/node": "26.6.5",
|
|
40
44
|
"@vitest/coverage-v8": "5.0.5",
|
|
41
45
|
"esbuild": "0.28.2",
|
|
42
46
|
"eslint": "10.13.0",
|
|
43
47
|
"typescript": "6.0.3",
|
|
44
48
|
"uqr": "0.1.3",
|
|
45
|
-
"vitest": "5.0.5"
|
|
46
|
-
"@hostedge/core": "0.0.0",
|
|
47
|
-
"@hostedge/config": "0.0.0"
|
|
48
|
-
},
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "node build.mjs",
|
|
51
|
-
"lint": "eslint .",
|
|
52
|
-
"typecheck": "tsc --noEmit",
|
|
53
|
-
"test": "vitest run --coverage"
|
|
49
|
+
"vitest": "5.0.5"
|
|
54
50
|
}
|
|
55
|
-
}
|
|
51
|
+
}
|