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.
- package/README.md +97 -39
- package/dist/hostedge.js +84 -19
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,48 +1,106 @@
|
|
|
1
|
-
# hostedge
|
|
1
|
+
# hostedge
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>]` |
|
|
15
|
-
| `hostedge logout` |
|
|
16
|
-
| `hostedge whoami` |
|
|
17
|
-
| `hostedge
|
|
18
|
-
| `hostedge
|
|
19
|
-
| `hostedge
|
|
20
|
-
| `hostedge
|
|
21
|
-
| `hostedge
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 |
|
|
32
|
-
| 1 |
|
|
33
|
-
| 2 |
|
|
34
|
-
| 3 |
|
|
35
|
-
| 4 |
|
|
36
|
-
| 5 |
|
|
37
|
-
| 6 |
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1725
|
-
|
|
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.
|
|
2973
|
-
var HELP = `hostedge \u2014
|
|
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.
|
|
4
|
-
"description": "Deploy to
|
|
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",
|