hostedge 0.1.1 → 0.1.3

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 +97 -39
  2. package/dist/hostedge.js +84 -19
  3. package/package.json +8 -2
package/README.md CHANGED
@@ -1,48 +1,106 @@
1
- # hostedge CLI
1
+ # hostedge
2
2
 
3
- Deploy a folder to HostEdge from your terminal.
3
+ **Hosting your AI agent can use.** The HostEdge command line deploys websites and apps to a global edge network, reads logs, and manages environment variables. It works the same for people at a terminal, in CI, and for AI agents such as Claude Code, Cursor and Codex.
4
4
 
5
- Install once with `npm install --global hostedge` (or `curl -fsSL https://hostedge.app/install.sh | sh`), or run it with `npx`:
5
+ - Site: https://hostedge.app
6
+ - Docs: https://hostedge.app/docs/cli
7
+ - Free plan: 3 projects, no card
8
+
9
+ ## Quick start
10
+
11
+ ```sh
12
+ npx hostedge login # opens your browser and signs you in
13
+ npx hostedge deploy # uploads your project, shows the build, prints the live URL
14
+ ```
15
+
16
+ That is all. Run it in the folder that holds your site (a built `dist` or `out` folder, or the project itself). The first deploy creates the project and links it; later deploys only upload what changed.
17
+
18
+ Install it once if you prefer a short command:
19
+
20
+ ```sh
21
+ npm install --global hostedge # then: hostedge deploy
22
+ ```
23
+
24
+ Requires Node 20.11 or newer.
25
+
26
+ ## Let your AI agent do it
27
+
28
+ There are two ways. Use either or both.
29
+
30
+ **1. Teach the agent HostEdge (no setup in the dashboard):**
31
+
32
+ ```sh
33
+ npx hostedge init --ai
34
+ ```
35
+
36
+ This writes `AGENTS.md`, `CLAUDE.md`, a Cursor rule and a Claude Code skill into your project. Your agent then knows how to build for HostEdge, deploy, read logs and roll back.
37
+
38
+ **2. Connect it over MCP** and just ask ("deploy this and check the logs"):
6
39
 
7
40
  ```sh
8
- npx hostedge login # opens your browser; or: hostedge login --token hostedge_…
9
- npx hostedge deploy # uploads changed files, streams the build log, prints the URL
41
+ claude mcp add --transport http hostedge https://hostedge.app/api/mcp
10
42
  ```
11
43
 
44
+ For Cursor, Windsurf and other clients, add `https://hostedge.app/api/mcp` as a remote MCP server. Details: https://hostedge.app/docs/ai-assistants
45
+
46
+ ### For agents: what to know
47
+
48
+ - Every command takes `--json` (machine output on stdout, human text on stderr), `--yes` (never prompt) and `--quiet`. With `CI=1` or no TTY it never prompts.
49
+ - Authenticate with the `HOSTEDGE_TOKEN` environment variable (create one in Settings, API tokens, scope `deployments:write`) or `hostedge login --token <token>`.
50
+ - `hostedge deploy --wait --json` prints `{ "deploymentId", "status", "url", "failureReason", ... }` and exits `5` if the build fails.
51
+ - `hostedge logs --json` prints one JSON object per line.
52
+ - Exit codes are stable, see the table below.
53
+
54
+ ## Commands
55
+
12
56
  | Command | What it does |
13
57
  | --- | --- |
14
- | `hostedge login [--token <t> \| --with-token] [--api-url <url>]` | Signs in. The browser flow creates a 90-day token scoped to the CLI; `--with-token` reads the token from stdin. |
15
- | `hostedge logout` | Forgets the saved token (revoke it in Settings → API tokens). |
16
- | `hostedge whoami` | Shows the account, workspace and scopes in use. |
17
- | `hostedge ai-kit [dir] [--force]` | Writes `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/hostedge.mdc` and `.claude/skills/hostedge/SKILL.md` (never overwrites without `--force`). |
18
- | `hostedge init [dir] [--name] [--project <id\|slug>] [--ai]` | Links a folder to a new or existing project (`.hostedge/project.json`); `--ai` also writes the agent kit. |
19
- | `hostedge deploy [dir] [--preview] [-m <message>] [--no-wait]` | Deploys; links the folder on first deploy. Exits 5 if the build fails. |
20
- | `hostedge logs [-f] [--level <l>] [--search <text>] [--since 30m\|1h\|24h\|7d]` | Runtime logs, oldest first; `-f` follows. |
21
- | `hostedge env pull [-e <environment>] [--file <path>]` | Writes variables to `.env.local` (0600, added to `.gitignore`). |
22
-
23
- For scripts and AI agents every command accepts `--json` (JSON on stdout, human text on
24
- stderr), `--yes` (never prompt; `CI=1` or no TTY also disables prompts), `--quiet`,
25
- `--api-url <url>` and `--no-color` (`NO_COLOR` is respected). `hostedge deploy --wait --json`
26
- prints `{deploymentId,status,url,failureReason,…}` and exits 5 if the build fails;
27
- `hostedge logs --json` prints one JSON object per line.
28
-
29
- | Exit code | Meaning |
58
+ | `hostedge login [--token <t> \| --with-token] [--api-url <url>]` | Sign in. The browser flow creates a 90-day token limited to the CLI. `--with-token` reads the token from stdin. |
59
+ | `hostedge logout` | Forget the saved token (revoke it in Settings, API tokens). |
60
+ | `hostedge whoami` | Show the account, workspace and scopes in use. |
61
+ | `hostedge init [dir] [--name] [--project <id\|slug>] [--ai]` | Link a folder to a new or existing project. `--ai` also writes the agent kit. |
62
+ | `hostedge deploy [dir] [--preview] [-m <message>] [--no-wait]` | Deploy. Links the folder on first use. Exits `5` if the build fails. |
63
+ | `hostedge logs [-f] [--level <l>] [--search <text>] [--since 30m\|1h\|24h\|7d]` | Runtime logs, oldest first. `-f` follows. |
64
+ | `hostedge env pull [-e <environment>] [--file <path>]` | Write variables to `.env.local` (private file, added to `.gitignore`). |
65
+ | `hostedge ai-kit [dir] [--force]` | Write the agent files only. Never overwrites without `--force`. |
66
+
67
+ Global flags: `--json`, `--yes`, `--quiet`, `--api-url <url>`, `--no-color` (`NO_COLOR` is respected).
68
+
69
+ ## Exit codes
70
+
71
+ | Code | Meaning |
30
72
  | --- | --- |
31
- | 0 | ok |
32
- | 1 | generic failure |
33
- | 2 | usage error |
34
- | 3 | auth error (not signed in, token invalid or missing a scope) |
35
- | 4 | not found (project, deployment, folder not linked) |
36
- | 5 | deployment failed or was canceled |
37
- | 6 | plan limit reached |
38
-
39
- In CI, set `HOSTEDGE_TOKEN` (create one in Settings → API tokens with the `deployments:write`
40
- scope) and run `npx hostedge deploy`. `HOSTEDGE_API_URL` points the CLI at another HostEdge
41
- (e.g. `http://localhost:3000` in development).
42
-
43
- The CLI follows the dashboard's upload rules: `.git`, `node_modules` and similar folders are
44
- skipped, secret-looking files (`.env*`, keys) are left behind, and symbolic links are refused.
45
- Only files HostEdge doesn't already have are uploaded.
46
-
47
- Development: `pnpm --filter hostedge build` bundles `dist/hostedge.js` (no runtime
48
- dependencies); `pnpm --filter hostedge test` runs the commands against a fake API.
73
+ | 0 | OK |
74
+ | 1 | Generic failure |
75
+ | 2 | Usage error (bad flag or argument) |
76
+ | 3 | Auth error: not signed in, token invalid, or missing a scope |
77
+ | 4 | Not found: project, deployment, or folder not linked |
78
+ | 5 | Deployment failed or was canceled |
79
+ | 6 | Plan limit reached |
80
+
81
+ ## CI
82
+
83
+ ```yaml
84
+ - run: npx hostedge deploy --yes
85
+ env:
86
+ HOSTEDGE_TOKEN: ${{ secrets.HOSTEDGE_TOKEN }}
87
+ ```
88
+
89
+ `HOSTEDGE_API_URL` points the CLI at another HostEdge, for example `http://localhost:3000` in development.
90
+
91
+ ## What gets uploaded
92
+
93
+ The CLI follows the same rules as the dashboard. `.git`, `node_modules` and similar folders are skipped. Files that look like secrets (`.env*`, private keys) are left behind. Symbolic links are refused. Only files HostEdge does not already have are uploaded, in batches, with progress and automatic retries.
94
+
95
+ ## Troubleshooting
96
+
97
+ - **"Not signed in" (exit 3):** run `npx hostedge login`, or set `HOSTEDGE_TOKEN`.
98
+ - **"Not linked" (exit 4):** run `npx hostedge init` in the project folder, or pass `--project <slug>`.
99
+ - **Too many requests:** the CLI waits and retries by itself; if it keeps failing, run it again in a minute.
100
+ - **Plan limit (exit 6):** upgrade at https://hostedge.app/pricing or remove an unused project.
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
package/dist/hostedge.js CHANGED
@@ -1320,19 +1320,21 @@ function exitCodeFor(status, code) {
1320
1320
  return EXIT.failure;
1321
1321
  }
1322
1322
  var ApiError = class extends CliError {
1323
- constructor(message, status, code) {
1323
+ constructor(message, status, code, retryAfter = null) {
1324
1324
  super(message, exitCodeFor(status, code));
1325
1325
  this.status = status;
1326
1326
  this.code = code;
1327
+ this.retryAfter = retryAfter;
1327
1328
  }
1328
1329
  status;
1329
1330
  code;
1331
+ retryAfter;
1330
1332
  };
1331
1333
  function createApi(io2, apiUrl, token) {
1332
1334
  async function raw(path, init2 = {}) {
1333
1335
  const headers = new Headers(init2.headers);
1334
1336
  headers.set("authorization", `Bearer ${token}`);
1335
- headers.set("user-agent", "hostedge-cli/0.1.1");
1337
+ headers.set("user-agent", "hostedge-cli/0.1.3");
1336
1338
  let response;
1337
1339
  try {
1338
1340
  response = await io2.fetch(`${apiUrl}${path}`, { ...init2, headers });
@@ -1343,7 +1345,13 @@ function createApi(io2, apiUrl, token) {
1343
1345
  const body2 = await response.json().catch(() => ({}));
1344
1346
  const fields = body2.error?.fields ? ` ${Object.entries(body2.error.fields).map(([k, v]) => `${k}: ${v}`).join("; ")}` : "";
1345
1347
  const message = response.status === 401 ? `${body2.error?.message ?? "Not signed in."} Run \`hostedge login\`.` : `${body2.error?.message ?? `HostEdge answered ${response.status}.`}${fields}`;
1346
- throw new ApiError(message, response.status, body2.error?.code ?? null);
1348
+ const wait = Number(response.headers.get("retry-after"));
1349
+ throw new ApiError(
1350
+ message,
1351
+ response.status,
1352
+ body2.error?.code ?? null,
1353
+ Number.isFinite(wait) && wait > 0 ? wait : null
1354
+ );
1347
1355
  }
1348
1356
  return response;
1349
1357
  }
@@ -1566,7 +1574,8 @@ async function withRetries(work, retryable, attempts = 4, delayMs = 500) {
1566
1574
  return await work();
1567
1575
  } catch (error) {
1568
1576
  if (attempt >= attempts || !retryable(error)) throw error;
1569
- await new Promise((resolve6) => setTimeout(resolve6, delayMs * 2 ** (attempt - 1)));
1577
+ const wait = typeof delayMs === "function" ? delayMs(error, attempt) : delayMs * 2 ** (attempt - 1);
1578
+ await new Promise((resolve6) => setTimeout(resolve6, wait));
1570
1579
  }
1571
1580
  }
1572
1581
  }
@@ -1639,8 +1648,31 @@ async function* readSse(body2) {
1639
1648
 
1640
1649
  // src/commands/deploy.ts
1641
1650
  var FINISHED = /* @__PURE__ */ new Set(["ready", "failed", "canceled"]);
1642
- var UPLOAD_CONCURRENCY = 16;
1651
+ var UPLOAD_CONCURRENCY = 6;
1652
+ var BATCH_BYTES = 3 * 1024 * 1024;
1653
+ var BATCH_FILES = 100;
1643
1654
  var UPLOAD_TIMEOUT_MS = 3e4;
1655
+ function batchesOf(files) {
1656
+ const batches = [];
1657
+ let current = [];
1658
+ let bytes = 0;
1659
+ for (const file of files) {
1660
+ if (current.length > 0 && (bytes + file.size > BATCH_BYTES || current.length >= BATCH_FILES)) {
1661
+ batches.push(current);
1662
+ current = [];
1663
+ bytes = 0;
1664
+ }
1665
+ current.push(file);
1666
+ bytes += file.size;
1667
+ }
1668
+ if (current.length > 0) batches.push(current);
1669
+ return batches;
1670
+ }
1671
+ function retryDelay(error, attempt) {
1672
+ if (error instanceof ApiError && error.retryAfter !== null)
1673
+ return Math.min(6e4, (error.retryAfter + 1) * 1e3);
1674
+ return Math.min(6e4, 500 * 2 ** (attempt - 1));
1675
+ }
1644
1676
  function temporary(error) {
1645
1677
  if (error instanceof ApiError) return error.status === 429 || error.status >= 500;
1646
1678
  return error instanceof CliError;
@@ -1721,23 +1753,56 @@ async function deploy(r, options) {
1721
1753
  let uploaded = 0;
1722
1754
  let reported = 0;
1723
1755
  const step = Math.max(1, Math.ceil(toUpload.length / 10));
1724
- await pool(toUpload, UPLOAD_CONCURRENCY, async (file) => {
1725
- const bytes = await readFile4(file.absolute);
1726
- await withRetries(
1727
- () => api.raw(`/api/v1/uploads/${file.hash}`, {
1728
- method: "PUT",
1729
- headers: { "content-type": "application/octet-stream" },
1730
- body: bytes,
1731
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
1732
- }),
1733
- temporary
1734
- );
1735
- uploaded++;
1756
+ const progress = (count2) => {
1757
+ uploaded += count2;
1736
1758
  if (toUpload.length > 20 && uploaded - reported >= step && uploaded < toUpload.length) {
1737
1759
  reported = uploaded;
1738
1760
  r.say(`${s.dim(` ${uploaded}/${toUpload.length} uploaded`)}
1739
1761
  `);
1740
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 {
1781
+ await send(
1782
+ "/api/v1/uploads/batch",
1783
+ { method: "POST", body: form },
1784
+ batch.reduce((sum, file) => sum + file.size, 0)
1785
+ );
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;
1792
+ }
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
+ }
1741
1806
  });
1742
1807
  let project;
1743
1808
  if (!link) {
@@ -2969,8 +3034,8 @@ var Reporter = class {
2969
3034
  };
2970
3035
 
2971
3036
  // src/cli.ts
2972
- var VERSION = "0.1.1";
2973
- var HELP = `hostedge \u2014 deploy to HostEdge from your terminal
3037
+ var VERSION = "0.1.3";
3038
+ var HELP = `hostedge \u2014 hosting your AI agent can use. Deploy from your terminal, CI or an agent.
2974
3039
 
2975
3040
  Usage
2976
3041
  hostedge login [--token <t> | --with-token] [--api-url <url>]
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "hostedge",
3
- "version": "0.1.1",
4
- "description": "Deploy to HostEdge from your terminal",
3
+ "version": "0.1.3",
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",
7
7
  "deploy",
8
8
  "hosting",
9
9
  "cli",
10
+ "ai-agents",
11
+ "mcp",
12
+ "claude-code",
13
+ "cursor",
14
+ "edge",
15
+ "static-site",
10
16
  "cloudflare-workers"
11
17
  ],
12
18
  "homepage": "https://hostedge.app/docs/cli",