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.
Files changed (3) hide show
  1. package/README.md +1 -5
  2. package/dist/hostedge.js +594 -148
  3. 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
- ## Development
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.1.3");
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((resolve6) => {
1391
- resolveToken = resolve6;
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((resolve6) => server.listen(0, "127.0.0.1", resolve6));
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 readFile4 } from "node:fs/promises";
1514
- import { basename, join as join4, resolve as resolve2 } from "node:path";
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 upload = files.filter((f) => !secrets.has(f.path));
1567
- if (limits.hash !== false)
1568
- for (const file of upload) file.hash = await contentHash(await readFile3(file.absolute));
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((resolve6) => setTimeout(resolve6, wait));
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 = resolve2(io2.cwd, options.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
- const total = files.reduce((sum, f) => sum + f.size, 0);
1739
- const { missing } = await api.request("/api/v1/uploads", {
1740
- json: { hashes: [...new Set(files.map((f) => f.hash))] }
1741
- });
1742
- const needed = new Set(missing);
1743
- const toUpload = files.filter((f) => {
1744
- if (!needed.has(f.hash)) return false;
1745
- needed.delete(f.hash);
1746
- return true;
1747
- });
1748
- const uploadBytes = toUpload.reduce((sum, f) => sum + f.size, 0);
1749
- r.say(
1750
- `Uploading ${toUpload.length} of ${files.length} files (${formatBytes(uploadBytes)} of ${formatBytes(total)}; the rest are already on HostEdge)
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
- let uploaded = 0;
1754
- let reported = 0;
1755
- const step = Math.max(1, Math.ceil(toUpload.length / 10));
1756
- const progress = (count2) => {
1757
- uploaded += count2;
1758
- if (toUpload.length > 20 && uploaded - reported >= step && uploaded < toUpload.length) {
1759
- reported = uploaded;
1760
- r.say(`${s.dim(` ${uploaded}/${toUpload.length} uploaded`)}
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
- const send = (path, init2, bytes) => withRetries(
1765
- () => api.raw(path, {
1766
- ...init2,
1767
- // 30 s plus a few more for every MB, so a big file on a slow line can finish.
1768
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS + Math.ceil(bytes / 1048576) * 4e3)
1769
- }),
1770
- temporary,
1771
- 6,
1772
- retryDelay
1773
- );
1774
- let batching = true;
1775
- await pool(batchesOf(toUpload), UPLOAD_CONCURRENCY, async (batch) => {
1776
- if (batching && (batch.length > 1 || (batch[0]?.size ?? 0) <= BATCH_BYTES)) {
1777
- const form = new FormData();
1778
- for (const file of batch)
1779
- form.append(file.hash, new Blob([await readFile4(file.absolute)]), file.hash);
1780
- try {
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
- "/api/v1/uploads/batch",
1783
- { method: "POST", body: form },
1784
- batch.reduce((sum, file) => sum + file.size, 0)
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(batch.length);
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
- for (const file of batch) {
1795
- await send(
1796
- `/api/v1/uploads/${file.hash}`,
1797
- {
1798
- method: "PUT",
1799
- headers: { "content-type": "application/octet-stream" },
1800
- body: await readFile4(file.absolute)
1801
- },
1802
- file.size
1803
- );
1804
- progress(1);
1805
- }
1806
- });
1807
- let project;
1808
- if (!link) {
1809
- let pkg = null;
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
- pkg = await readFile4(join4(dir, "package.json"), "utf8");
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
- const environment = options.preview ? "preview" : "production";
1823
- const created = await api.request("/api/v1/deployments", {
1824
- json: {
1825
- projectId: link?.projectId,
1826
- project,
1827
- files: files.map((f) => ({ path: f.path, hash: f.hash, size: f.size })),
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 writeFile3 } from "node:fs/promises";
1889
- import { resolve as resolve4 } from "node:path";
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 resolve3 } from "node:path";
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(resolve3(io2.cwd, dir));
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 = resolve4(io2.cwd, file);
1979
- await writeFile3(path, text, { mode: 384 });
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 readFile5 } from "node:fs/promises";
1993
- import { basename as basename2, join as join5, resolve as resolve5 } from "node:path";
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 = resolve5(io2.cwd, options.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 readFile5(join5(dir, "package.json"), "utf8");
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 mkdir3, rename, rm as rm2, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
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 join6 } from "node:path";
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(join6(dir, exe))) return join6(dir, exe);
2797
- const binDir = join6(io2.env.HOME ?? homedir2(), ".hostedge", "bin");
2798
- const own = join6(binDir, exe);
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 mkdir3(binDir, { recursive: true });
2809
- const tmp = join6(binDir, `${asset}.download`);
2810
- await writeFile4(tmp, new Uint8Array(await response.arrayBuffer()));
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((resolve6, reject) => {
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 ? resolve6() : reject(new Error(`tar exited ${code}`)));
3256
+ tar.on("exit", (code) => code === 0 ? resolve7() : reject(new Error(`tar exited ${code}`)));
2816
3257
  });
2817
- await rm2(tmp, { force: true });
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((resolve6) => {
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
- resolve6(true);
3270
+ resolve7(true);
2830
3271
  });
2831
- socket.once("error", () => resolve6(false));
3272
+ socket.once("error", () => resolve7(false));
2832
3273
  socket.setTimeout(1e3, () => {
2833
3274
  socket.destroy();
2834
- resolve6(false);
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 spawn2 = options.spawn ?? nodeSpawn;
2868
- const child = spawn2(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
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((resolve6) => {
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", () => resolve6(1));
3373
+ child.on("error", () => resolve7(1));
2933
3374
  child.on("exit", (code) => {
2934
3375
  options.signal?.removeEventListener("abort", stop);
2935
- resolve6(options.signal?.aborted ? 0 : code ?? 1);
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.1.3";
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
- Upload and deploy a folder (waits by default)
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((resolve6) => {
3681
+ return new Promise((resolve7) => {
3236
3682
  try {
3237
- const child = spawn(command, [url], { stdio: "ignore", detached: true });
3238
- child.on("error", () => resolve6(false));
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
- resolve6(true);
3687
+ resolve7(true);
3242
3688
  });
3243
3689
  } catch {
3244
- resolve6(false);
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((resolve6) => setTimeout(resolve6, ms))
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.1.3",
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": true
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
+ }