pockethost 3.0.0 → 3.1.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/CHANGELOG.md +19 -0
- package/README.md +43 -6
- package/dist/cli.js +432 -42
- package/package.json +11 -9
- package/LICENSE.md +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Formerly published as `phio`. Versions below 3.0.0 are phio's history.
|
|
4
4
|
|
|
5
|
+
## 3.1.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- `deploy` and `dev` now upload over HTTPS instead of SFTP. The CLI sends a list of file hashes, uploads only the
|
|
10
|
+
files PocketHost doesn't have yet (compressed), and PocketHost swaps the new set in all at once, restarting the
|
|
11
|
+
instance when anything outside `pb_public` changed. Files in the deployed folders that aren't in your project are
|
|
12
|
+
removed. Dotfiles (for example `pb_public/.well-known/`) are now deployed; `.git*` and `node_modules` stay excluded.
|
|
13
|
+
- Personal access tokens: set `POCKETHOST_TOKEN`, or run `pockethost token set` / `pockethost login --token`. Deploys
|
|
14
|
+
need no password or SSH key, so CI works on fresh runners (previously a new runner generated a new deploy key that
|
|
15
|
+
didn't match the registered `PocketHost CLI` key).
|
|
16
|
+
- New `pockethost token [set|clear|status]`. `info` shows the token in use. `logout` also removes a saved token.
|
|
17
|
+
- `deploy --commit <sha>` and `-m/--message <note>` are recorded with each deploy (`$GITHUB_SHA` is used
|
|
18
|
+
automatically).
|
|
19
|
+
- `--sftp` on `deploy` and `dev` keeps the previous SFTP sync.
|
|
20
|
+
- PocketHost no longer runs `bun install` when `package.json`, `bun.lock(b)` or `patches/` change (over SFTP or the
|
|
21
|
+
deploy API). Those files are copied as-is, and an existing `node_modules` on the instance is left alone. Bundle npm
|
|
22
|
+
code your hooks need into `pb_hooks`.
|
|
23
|
+
|
|
5
24
|
## 3.0.0
|
|
6
25
|
|
|
7
26
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ you rename them to `POCKETHOST_*`.
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
pockethost login
|
|
26
|
+
pockethost login # or: pockethost login --token
|
|
27
|
+
pockethost token [set|clear|status]
|
|
27
28
|
pockethost logout
|
|
28
29
|
pockethost info # alias: pockethost whoami
|
|
29
30
|
pockethost list # alias: pockethost ls
|
|
@@ -34,12 +35,14 @@ pockethost sftp [instance]
|
|
|
34
35
|
pockethost logs [instance]
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
- **`pockethost dev`**: watch local changes and
|
|
38
|
-
- **`pockethost deploy`**:
|
|
38
|
+
- **`pockethost dev`**: watch local changes and deploy them
|
|
39
|
+
- **`pockethost deploy`**: deploy once
|
|
40
|
+
- **`pockethost token`**: save, clear, or check a personal access token
|
|
39
41
|
- **`pockethost sftp`**: interactive SFTP session (uses system `sftp` when available)
|
|
40
42
|
- **`pockethost logs`**: tail instance logs
|
|
41
43
|
|
|
42
|
-
`dev` and `deploy` accept `-v/--verbose`, `-i/--include`,
|
|
44
|
+
`dev` and `deploy` accept `-v/--verbose`, `-i/--include`, `-e/--exclude`, and `--sftp` (sync over SFTP instead of
|
|
45
|
+
HTTPS). `deploy` also takes `--commit <sha>` and `-m/--message <note>`.
|
|
43
46
|
|
|
44
47
|
Default includes: `pb_*`, `package.json`, `bun.lock`, `bun.lockb`, `patches/**`.
|
|
45
48
|
|
|
@@ -64,9 +67,42 @@ This writes `.pockethostconfig`:
|
|
|
64
67
|
Older project files (`.phioconfig`, `package.json` → `pockethost.instanceName`, `pockethost.json`) are migrated to
|
|
65
68
|
`.pockethostconfig` automatically on first use.
|
|
66
69
|
|
|
67
|
-
##
|
|
70
|
+
## How deploys work
|
|
68
71
|
|
|
69
|
-
`
|
|
72
|
+
`deploy` and `dev` talk to the instance over HTTPS (`https://<instance>.pockethost.io/_ph/deploy/v1`):
|
|
73
|
+
|
|
74
|
+
1. Send a manifest of `{ path: sha256 }` for the matched files.
|
|
75
|
+
2. Upload only the files the server is missing, as one gzip bundle per ~32 MB.
|
|
76
|
+
3. Commit: the server swaps the file set in atomically (stopping the instance when anything outside `pb_public`
|
|
77
|
+
changes) and starts the instance again.
|
|
78
|
+
|
|
79
|
+
PocketHost doesn't install npm packages. `package.json`, `bun.lock(b)` and `patches/` are copied as plain files. Bundle
|
|
80
|
+
any npm code your hooks need into `pb_hooks`.
|
|
81
|
+
|
|
82
|
+
Only `pb_*` folders (never `pb_data`), `package.json`, `bun.lock(b)` and `patches/` are deployed. Files in those folders
|
|
83
|
+
on the instance that aren't in your project are removed. `node_modules` and `.git*` are never touched.
|
|
84
|
+
|
|
85
|
+
## Authentication
|
|
86
|
+
|
|
87
|
+
`deploy` and `dev` use, in order:
|
|
88
|
+
|
|
89
|
+
1. `POCKETHOST_TOKEN` (a personal access token, `ph_pat_…`)
|
|
90
|
+
2. a token saved with `pockethost token set` (or `pockethost login --token`)
|
|
91
|
+
3. your email/password login (`pockethost login`, or `POCKETHOST_USERNAME`/`POCKETHOST_PASSWORD`)
|
|
92
|
+
|
|
93
|
+
Create tokens under Account → API Tokens in the dashboard. Deploy from GitHub Actions:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
- run: npx pockethost@latest deploy myapp-${{ github.ref_name }}
|
|
97
|
+
env:
|
|
98
|
+
POCKETHOST_TOKEN: ${{ secrets.POCKETHOST_TOKEN }}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Docs: https://pockethost.io/docs/deploy-from-ci
|
|
102
|
+
|
|
103
|
+
## Deploy key (`--sftp` and `pockethost sftp`)
|
|
104
|
+
|
|
105
|
+
`pockethost dev --sftp`, `pockethost deploy --sftp` and `pockethost sftp` use **SFTP** (`ftp.pockethost.io:2222`) using an Ed25519 deploy key
|
|
70
106
|
stored under `POCKETHOST_HOME` (default `~/.config/pockethost/`). The CLI auto-registers a **`PocketHost CLI`** key
|
|
71
107
|
under Account → Keys on first use (an existing **`Phio`** key from phio is reused). Run `pockethost info` to inspect
|
|
72
108
|
key status.
|
|
@@ -77,6 +113,7 @@ Customer docs: https://pockethost.io/docs/cli
|
|
|
77
113
|
|
|
78
114
|
| Variable | Purpose |
|
|
79
115
|
| -------- | ------- |
|
|
116
|
+
| `POCKETHOST_TOKEN` | Personal access token for `deploy`/`dev` (CI) |
|
|
80
117
|
| `POCKETHOST_USERNAME` | Override saved email (non-interactive login) |
|
|
81
118
|
| `POCKETHOST_PASSWORD` | Override saved password |
|
|
82
119
|
| `POCKETHOST_INSTANCE_NAME` | Override linked instance name |
|
package/dist/cli.js
CHANGED
|
@@ -6448,7 +6448,7 @@ function useColor() {
|
|
|
6448
6448
|
var program = new Command();
|
|
6449
6449
|
|
|
6450
6450
|
// package.json
|
|
6451
|
-
var version = "3.
|
|
6451
|
+
var version = "3.1.0";
|
|
6452
6452
|
|
|
6453
6453
|
// src/lib/projectRoot.ts
|
|
6454
6454
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -6617,6 +6617,13 @@ var MOTHERSHIP_URL = (...paths) => {
|
|
|
6617
6617
|
url.pathname = join(url.pathname, ...paths);
|
|
6618
6618
|
return url.toString();
|
|
6619
6619
|
};
|
|
6620
|
+
var INSTANCE_URL = (name) => {
|
|
6621
|
+
const m = new URL(MOTHERSHIP_URL());
|
|
6622
|
+
const apex = m.hostname.split(".").slice(1).join(".");
|
|
6623
|
+
const host = name.includes(".") ? name : `${name}.${apex}`;
|
|
6624
|
+
return `${m.protocol}//${host}${m.port ? `:${m.port}` : ""}`;
|
|
6625
|
+
};
|
|
6626
|
+
var TOKEN = () => envVar("TOKEN") ?? "";
|
|
6620
6627
|
var USERNAME = () => envVar("USERNAME") ?? "";
|
|
6621
6628
|
var PASSWORD = () => envVar("PASSWORD") ?? "";
|
|
6622
6629
|
var INSTANCE_NAME = () => envVar("INSTANCE_NAME") ?? "";
|
|
@@ -19272,6 +19279,31 @@ var ensureLoggedIn = async () => {
|
|
|
19272
19279
|
}
|
|
19273
19280
|
};
|
|
19274
19281
|
|
|
19282
|
+
// src/lib/deployApi.ts
|
|
19283
|
+
import { execFileSync } from "child_process";
|
|
19284
|
+
import { lstatSync as lstatSync2 } from "fs";
|
|
19285
|
+
import { readFile } from "fs/promises";
|
|
19286
|
+
import { sep as sep2 } from "path";
|
|
19287
|
+
import { gzipSync } from "zlib";
|
|
19288
|
+
|
|
19289
|
+
// src/lib/codeSet.ts
|
|
19290
|
+
var ROOT_CODE_FILES = ["package.json", "bun.lock", "bun.lockb"];
|
|
19291
|
+
var isCodePath = (p) => {
|
|
19292
|
+
if (!p || p.startsWith("/") || p.includes("\\") || p.includes("\0"))
|
|
19293
|
+
return false;
|
|
19294
|
+
const segs = p.split("/");
|
|
19295
|
+
if (segs.some((s2) => s2 === "" || s2 === "." || s2 === "..")) return false;
|
|
19296
|
+
if (segs.some((s2) => s2 === "node_modules" || s2.startsWith(".git")))
|
|
19297
|
+
return false;
|
|
19298
|
+
const [first] = segs;
|
|
19299
|
+
if (!first) return false;
|
|
19300
|
+
if (segs.length === 1) {
|
|
19301
|
+
return ROOT_CODE_FILES.includes(first) || first.startsWith("pb_") && first !== "pb_data";
|
|
19302
|
+
}
|
|
19303
|
+
if (first === "pb_data") return false;
|
|
19304
|
+
return first.startsWith("pb_") || first === "patches";
|
|
19305
|
+
};
|
|
19306
|
+
|
|
19275
19307
|
// src/lib/deployIncludes.ts
|
|
19276
19308
|
var DEFAULT_INCLUDES = [
|
|
19277
19309
|
`pb_*`,
|
|
@@ -19290,6 +19322,267 @@ var shouldSyncFile = (file, include, exclude, match2) => {
|
|
|
19290
19322
|
return isIncluded && !isExcluded;
|
|
19291
19323
|
};
|
|
19292
19324
|
|
|
19325
|
+
// src/lib/deployBundle.ts
|
|
19326
|
+
import { createHash as createHash2 } from "crypto";
|
|
19327
|
+
var BUNDLE_MAGIC = Buffer.from("PHB1");
|
|
19328
|
+
var sha256 = (data) => createHash2("sha256").update(data).digest("hex");
|
|
19329
|
+
var encodeBundle = (entries) => {
|
|
19330
|
+
const parts = [BUNDLE_MAGIC];
|
|
19331
|
+
for (const data of entries) {
|
|
19332
|
+
const len = Buffer.alloc(4);
|
|
19333
|
+
len.writeUInt32BE(data.length);
|
|
19334
|
+
parts.push(createHash2("sha256").update(data).digest(), len, data);
|
|
19335
|
+
}
|
|
19336
|
+
return Buffer.concat(parts);
|
|
19337
|
+
};
|
|
19338
|
+
var batchBySize = (items, maxBytes) => {
|
|
19339
|
+
const batches = [];
|
|
19340
|
+
let cur = [];
|
|
19341
|
+
let curBytes = 0;
|
|
19342
|
+
for (const item of items) {
|
|
19343
|
+
if (cur.length > 0 && curBytes + item.size > maxBytes) {
|
|
19344
|
+
batches.push(cur);
|
|
19345
|
+
cur = [];
|
|
19346
|
+
curBytes = 0;
|
|
19347
|
+
}
|
|
19348
|
+
cur.push(item);
|
|
19349
|
+
curBytes += item.size;
|
|
19350
|
+
}
|
|
19351
|
+
if (cur.length > 0) batches.push(cur);
|
|
19352
|
+
return batches;
|
|
19353
|
+
};
|
|
19354
|
+
|
|
19355
|
+
// src/lib/token.ts
|
|
19356
|
+
var PAT_PREFIX = "ph_pat_";
|
|
19357
|
+
var PAT_RE = /^ph_pat_[a-z0-9]{15}_[0-9A-Za-z]{43}$/;
|
|
19358
|
+
var isPat = (v2) => PAT_RE.test(v2.trim());
|
|
19359
|
+
var maskToken = (token) => token.startsWith(PAT_PREFIX) ? `${PAT_PREFIX}\u2026${token.slice(-4)}` : "\u2026";
|
|
19360
|
+
var savedToken = () => config("token") || "";
|
|
19361
|
+
var resolveDeployCredential = async () => {
|
|
19362
|
+
const env3 = TOKEN().trim();
|
|
19363
|
+
if (env3) return { token: env3, kind: "pat", source: "POCKETHOST_TOKEN" };
|
|
19364
|
+
const saved = savedToken().trim();
|
|
19365
|
+
if (saved && !USERNAME())
|
|
19366
|
+
return { token: saved, kind: "pat", source: "saved token" };
|
|
19367
|
+
const client2 = await getClient();
|
|
19368
|
+
if (client2.authStore.isValid && client2.authStore.token) {
|
|
19369
|
+
const email = client2.authStore.record?.email || "login";
|
|
19370
|
+
return { token: client2.authStore.token, kind: "login", source: email };
|
|
19371
|
+
}
|
|
19372
|
+
throw new Error(
|
|
19373
|
+
`Not authenticated. Set POCKETHOST_TOKEN (create a token at https://app.pockethost.io/account/tokens), run 'pockethost token set', or run 'pockethost login'.`
|
|
19374
|
+
);
|
|
19375
|
+
};
|
|
19376
|
+
var tokenWhoami = async (token) => {
|
|
19377
|
+
const res = await fetch(MOTHERSHIP_URL("/api/pat/whoami"), {
|
|
19378
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
19379
|
+
});
|
|
19380
|
+
const body = await res.json().catch(() => ({}));
|
|
19381
|
+
if (!res.ok) {
|
|
19382
|
+
throw new Error(body.message || `Token check failed (HTTP ${res.status})`);
|
|
19383
|
+
}
|
|
19384
|
+
return body;
|
|
19385
|
+
};
|
|
19386
|
+
var describeToken = (w) => {
|
|
19387
|
+
const scope = w.instances ? w.instances.map((i2) => i2.subdomain).join(", ") || "(no instances)" : "all instances";
|
|
19388
|
+
const expires = w.expires ? `expires ${w.expires.slice(0, 10)}` : "no expiry";
|
|
19389
|
+
return `"${w.label}" (${PAT_PREFIX}\u2026${w.hint}) for ${w.email}: ${w.scopes.join(", ")} on ${scope}, ${expires}`;
|
|
19390
|
+
};
|
|
19391
|
+
|
|
19392
|
+
// src/lib/deployApi.ts
|
|
19393
|
+
var DEPLOY_API_PREFIX = "/_ph/deploy/v1";
|
|
19394
|
+
var UPLOAD_BATCH_BYTES = 32 * 1024 * 1024;
|
|
19395
|
+
var DeployApiError = class extends Error {
|
|
19396
|
+
constructor(status, message, body = {}) {
|
|
19397
|
+
super(message);
|
|
19398
|
+
this.status = status;
|
|
19399
|
+
this.body = body;
|
|
19400
|
+
}
|
|
19401
|
+
status;
|
|
19402
|
+
body;
|
|
19403
|
+
};
|
|
19404
|
+
var collectLocalFiles = async (include, exclude, cwd = process.cwd()) => {
|
|
19405
|
+
const matches = ts(include, {
|
|
19406
|
+
ignore: mergeDeployExcludes(excludeDefaults, exclude),
|
|
19407
|
+
cwd,
|
|
19408
|
+
nodir: true,
|
|
19409
|
+
// The server deletes code-set files missing from the manifest, so dotfiles (pb_public/.well-known/…) must ship.
|
|
19410
|
+
dot: true
|
|
19411
|
+
});
|
|
19412
|
+
const files = [];
|
|
19413
|
+
const skipped = [];
|
|
19414
|
+
for (const raw of [...new Set(matches)].sort()) {
|
|
19415
|
+
const path5 = raw.split(sep2).join("/");
|
|
19416
|
+
const abs = `${cwd}/${path5}`;
|
|
19417
|
+
const st2 = lstatSync2(abs);
|
|
19418
|
+
if (!st2.isFile()) {
|
|
19419
|
+
skipped.push(path5);
|
|
19420
|
+
continue;
|
|
19421
|
+
}
|
|
19422
|
+
if (!isCodePath(path5)) {
|
|
19423
|
+
skipped.push(path5);
|
|
19424
|
+
continue;
|
|
19425
|
+
}
|
|
19426
|
+
files.push({ path: path5, size: st2.size, hash: sha256(await readFile(abs)) });
|
|
19427
|
+
}
|
|
19428
|
+
return { files, skipped };
|
|
19429
|
+
};
|
|
19430
|
+
var detectCommit = (cwd) => {
|
|
19431
|
+
const fromEnv = process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA;
|
|
19432
|
+
if (fromEnv) return fromEnv;
|
|
19433
|
+
try {
|
|
19434
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
19435
|
+
cwd,
|
|
19436
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19437
|
+
}).toString().trim();
|
|
19438
|
+
} catch {
|
|
19439
|
+
return "";
|
|
19440
|
+
}
|
|
19441
|
+
};
|
|
19442
|
+
var detectSource = () => process.env.GITHUB_ACTIONS ? "github-actions" : process.env.CI ? "ci" : `cli/${version}`;
|
|
19443
|
+
var createDeployClient = (instanceName, cred) => {
|
|
19444
|
+
const base = `${INSTANCE_URL(instanceName)}${DEPLOY_API_PREFIX}`;
|
|
19445
|
+
const call = async (method, path5, body, raw) => {
|
|
19446
|
+
let res;
|
|
19447
|
+
try {
|
|
19448
|
+
res = await fetch(`${base}${path5}`, {
|
|
19449
|
+
method,
|
|
19450
|
+
headers: {
|
|
19451
|
+
Authorization: `Bearer ${cred.token}`,
|
|
19452
|
+
"User-Agent": `pockethost-cli/${version}`,
|
|
19453
|
+
...raw ? { "Content-Type": "application/octet-stream" } : body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
19454
|
+
},
|
|
19455
|
+
body: raw ? new Uint8Array(raw) : body !== void 0 ? JSON.stringify(body) : void 0
|
|
19456
|
+
});
|
|
19457
|
+
} catch (e2) {
|
|
19458
|
+
throw new DeployApiError(
|
|
19459
|
+
0,
|
|
19460
|
+
`Could not reach ${base}: ${e2.message}`
|
|
19461
|
+
);
|
|
19462
|
+
}
|
|
19463
|
+
const text = await res.text();
|
|
19464
|
+
let json = {};
|
|
19465
|
+
try {
|
|
19466
|
+
json = text ? JSON.parse(text) : {};
|
|
19467
|
+
} catch {
|
|
19468
|
+
json = { message: text.slice(0, 300) };
|
|
19469
|
+
}
|
|
19470
|
+
if (!res.ok) {
|
|
19471
|
+
const msg = json.message || `HTTP ${res.status}`;
|
|
19472
|
+
if (res.status === 401) {
|
|
19473
|
+
throw new DeployApiError(
|
|
19474
|
+
401,
|
|
19475
|
+
`Authentication failed (${cred.source}): ${msg}`,
|
|
19476
|
+
json
|
|
19477
|
+
);
|
|
19478
|
+
}
|
|
19479
|
+
if (res.status === 404) {
|
|
19480
|
+
throw new DeployApiError(
|
|
19481
|
+
404,
|
|
19482
|
+
`Instance '${instanceName}' not found, or ${cred.kind === "pat" ? "this token cannot access it" : "it is not yours"}.`,
|
|
19483
|
+
json
|
|
19484
|
+
);
|
|
19485
|
+
}
|
|
19486
|
+
throw new DeployApiError(res.status, msg, json);
|
|
19487
|
+
}
|
|
19488
|
+
return json;
|
|
19489
|
+
};
|
|
19490
|
+
return {
|
|
19491
|
+
base,
|
|
19492
|
+
whoami: () => call("GET", "/whoami"),
|
|
19493
|
+
check: (files) => call("POST", "/check", { files }),
|
|
19494
|
+
upload: (bundle) => call(
|
|
19495
|
+
"POST",
|
|
19496
|
+
"/blobs",
|
|
19497
|
+
void 0,
|
|
19498
|
+
bundle
|
|
19499
|
+
),
|
|
19500
|
+
commit: (body) => call("POST", "/commit", body)
|
|
19501
|
+
};
|
|
19502
|
+
};
|
|
19503
|
+
var deployViaApi = async (opts) => {
|
|
19504
|
+
const log2 = opts.log ?? console.log;
|
|
19505
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
19506
|
+
const cred = await resolveDeployCredential();
|
|
19507
|
+
const client2 = createDeployClient(opts.instanceName, cred);
|
|
19508
|
+
const { files, skipped } = await collectLocalFiles(
|
|
19509
|
+
opts.include,
|
|
19510
|
+
opts.exclude,
|
|
19511
|
+
cwd
|
|
19512
|
+
);
|
|
19513
|
+
if (skipped.length > 0) {
|
|
19514
|
+
log2(
|
|
19515
|
+
`\u26A0\uFE0F Skipping ${skipped.length} file(s) outside the deployable set (pb_* except pb_data, package.json, bun.lock(b), patches/)${opts.verbose ? `: ${skipped.join(", ")}` : ""}`
|
|
19516
|
+
);
|
|
19517
|
+
}
|
|
19518
|
+
const manifest = Object.fromEntries(files.map((f) => [f.path, f.hash]));
|
|
19519
|
+
const byHash = new Map(files.map((f) => [f.hash, f]));
|
|
19520
|
+
log2(
|
|
19521
|
+
`\u{1F69A} Deploying ${files.length} files to ${opts.instanceName} (${cred.kind === "pat" ? `token from ${cred.source}` : `login ${cred.source}`})`
|
|
19522
|
+
);
|
|
19523
|
+
let uploadedCount = 0;
|
|
19524
|
+
let uploadedBytes = 0;
|
|
19525
|
+
const upload = async (hashes) => {
|
|
19526
|
+
const toSend = [...new Set(hashes)].map((h) => byHash.get(h)).filter((f) => !!f);
|
|
19527
|
+
if (toSend.length === 0) return;
|
|
19528
|
+
const bytes = toSend.reduce((n8, f) => n8 + f.size, 0);
|
|
19529
|
+
log2(`\u2B06\uFE0F Uploading ${toSend.length} file(s) (${prettyBytes(bytes)})`);
|
|
19530
|
+
for (const batch of batchBySize(toSend, UPLOAD_BATCH_BYTES)) {
|
|
19531
|
+
const buffers = await Promise.all(
|
|
19532
|
+
batch.map((f) => readFile(`${cwd}/${f.path}`))
|
|
19533
|
+
);
|
|
19534
|
+
for (let i2 = 0; i2 < batch.length; i2++) {
|
|
19535
|
+
if (sha256(buffers[i2]) !== batch[i2].hash) {
|
|
19536
|
+
throw new Error(
|
|
19537
|
+
`${batch[i2].path} changed during deploy; run it again`
|
|
19538
|
+
);
|
|
19539
|
+
}
|
|
19540
|
+
}
|
|
19541
|
+
await client2.upload(gzipSync(encodeBundle(buffers)));
|
|
19542
|
+
if (opts.verbose) for (const f of batch) log2(` ${f.path}`);
|
|
19543
|
+
}
|
|
19544
|
+
uploadedCount += toSend.length;
|
|
19545
|
+
uploadedBytes += bytes;
|
|
19546
|
+
};
|
|
19547
|
+
const { missing } = await client2.check(manifest);
|
|
19548
|
+
await upload(missing);
|
|
19549
|
+
const commitBody = {
|
|
19550
|
+
files: manifest,
|
|
19551
|
+
commit: opts.commit ?? detectCommit(cwd),
|
|
19552
|
+
message: opts.message ?? "",
|
|
19553
|
+
source: detectSource(),
|
|
19554
|
+
uploadedCount,
|
|
19555
|
+
uploadedBytes
|
|
19556
|
+
};
|
|
19557
|
+
let result;
|
|
19558
|
+
try {
|
|
19559
|
+
result = await client2.commit(commitBody);
|
|
19560
|
+
} catch (e2) {
|
|
19561
|
+
if (e2 instanceof DeployApiError && e2.status === 409 && Array.isArray(e2.body.missing)) {
|
|
19562
|
+
await upload(e2.body.missing);
|
|
19563
|
+
result = await client2.commit({
|
|
19564
|
+
...commitBody,
|
|
19565
|
+
uploadedCount,
|
|
19566
|
+
uploadedBytes
|
|
19567
|
+
});
|
|
19568
|
+
} else {
|
|
19569
|
+
throw e2;
|
|
19570
|
+
}
|
|
19571
|
+
}
|
|
19572
|
+
if (opts.verbose) {
|
|
19573
|
+
for (const p of result.written) log2(` + ${p}`);
|
|
19574
|
+
for (const p of result.deleted) log2(` - ${p}`);
|
|
19575
|
+
}
|
|
19576
|
+
if (result.nothingChanged) {
|
|
19577
|
+
log2(`\u2705 ${opts.instanceName} is up to date: no changes.`);
|
|
19578
|
+
return result;
|
|
19579
|
+
}
|
|
19580
|
+
const summary = `${result.writtenCount} changed, ${result.deletedCount} deleted, ${result.unchanged} unchanged`;
|
|
19581
|
+
const restart = result.restarted ? " Instance restarted." : result.startError ? ` Instance failed to start: ${result.startError} (check 'pockethost logs').` : "";
|
|
19582
|
+
log2(`\u{1F680} Deployed ${result.deployId}: ${summary}.${restart}`);
|
|
19583
|
+
return result;
|
|
19584
|
+
};
|
|
19585
|
+
|
|
19293
19586
|
// src/commands/DevCommand.ts
|
|
19294
19587
|
var watchAndDeploy = async (instanceName, options2 = {
|
|
19295
19588
|
include: DEFAULT_INCLUDES,
|
|
@@ -19302,20 +19595,27 @@ var watchAndDeploy = async (instanceName, options2 = {
|
|
|
19302
19595
|
);
|
|
19303
19596
|
}
|
|
19304
19597
|
console.log(`Dev mode`);
|
|
19305
|
-
|
|
19306
|
-
const
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19598
|
+
const { include, exclude, verbose, sftp } = options2;
|
|
19599
|
+
const subdomain = await (async () => {
|
|
19600
|
+
if (sftp) {
|
|
19601
|
+
await ensureLoggedIn();
|
|
19602
|
+
try {
|
|
19603
|
+
return (await getInstanceBySubdomainCnameOrId(instanceName)).subdomain;
|
|
19604
|
+
} catch (error2) {
|
|
19605
|
+
throw new Error(`Instance ${instanceName} not found`);
|
|
19606
|
+
}
|
|
19312
19607
|
}
|
|
19608
|
+
const { instance } = await createDeployClient(
|
|
19609
|
+
instanceName,
|
|
19610
|
+
await resolveDeployCredential()
|
|
19611
|
+
).whoami();
|
|
19612
|
+
return instanceName.includes(".") ? instanceName : instance.subdomain;
|
|
19313
19613
|
})();
|
|
19314
19614
|
const limiter = new import_bottleneck.default({ maxConcurrent: 1 });
|
|
19315
19615
|
const upload = debounce(
|
|
19316
19616
|
() => limiter.schedule(
|
|
19317
|
-
() => deployMyCode(
|
|
19318
|
-
console.error
|
|
19617
|
+
() => deployMyCode(subdomain, include, exclude, verbose, { sftp }).catch(
|
|
19618
|
+
(e2) => console.error(`Deploy failed: ${e2.message ?? e2}`)
|
|
19319
19619
|
)
|
|
19320
19620
|
),
|
|
19321
19621
|
200
|
|
@@ -19337,7 +19637,26 @@ var watchAndDeploy = async (instanceName, options2 = {
|
|
|
19337
19637
|
};
|
|
19338
19638
|
watcher.on("add", handle).on("change", handle).on("unlink", handle);
|
|
19339
19639
|
};
|
|
19340
|
-
async function deployMyCode(instanceName, include, exclude, verbose) {
|
|
19640
|
+
async function deployMyCode(instanceName, include, exclude, verbose, extra = {}) {
|
|
19641
|
+
if (!instanceName) {
|
|
19642
|
+
throw new Error(
|
|
19643
|
+
`No instance name provided and none was found in ${PROJECT_CONFIG_FILE}. Use 'pockethost link <instance>'`
|
|
19644
|
+
);
|
|
19645
|
+
}
|
|
19646
|
+
if (!extra.sftp) {
|
|
19647
|
+
await deployViaApi({
|
|
19648
|
+
instanceName,
|
|
19649
|
+
include,
|
|
19650
|
+
exclude,
|
|
19651
|
+
verbose,
|
|
19652
|
+
commit: extra.commit,
|
|
19653
|
+
message: extra.message
|
|
19654
|
+
});
|
|
19655
|
+
return;
|
|
19656
|
+
}
|
|
19657
|
+
return deployMyCodeOverSftp(instanceName, include, exclude, verbose);
|
|
19658
|
+
}
|
|
19659
|
+
async function deployMyCodeOverSftp(instanceName, include, exclude, verbose) {
|
|
19341
19660
|
await ensureLoggedIn();
|
|
19342
19661
|
const client2 = await getClient();
|
|
19343
19662
|
const { privateKeyPath } = await ensureDeployKey(client2);
|
|
@@ -19361,7 +19680,7 @@ async function deployMyCode(instanceName, include, exclude, verbose) {
|
|
|
19361
19680
|
console.log("\u{1F680} Deploy done!");
|
|
19362
19681
|
}
|
|
19363
19682
|
var DevCommand = () => {
|
|
19364
|
-
return new Command("dev").argument(`[instanceName]`, `Instance name`, savedInstanceName()).description(`Watch for local modifications and
|
|
19683
|
+
return new Command("dev").argument(`[instanceName]`, `Instance name`, savedInstanceName()).description(`Watch for local modifications and deploy them`).option(`-v, --verbose`, `Verbose output`).option(`--sftp`, `Sync over SFTP instead of the deploy API`).option(
|
|
19365
19684
|
"-i, --include <include...>",
|
|
19366
19685
|
"Files to include in the sync",
|
|
19367
19686
|
(val, prev) => [...prev, ...val.split(",")],
|
|
@@ -19378,7 +19697,12 @@ var DevCommand = () => {
|
|
|
19378
19697
|
|
|
19379
19698
|
// src/commands/DeployCommand.ts
|
|
19380
19699
|
var DeployCommand = () => {
|
|
19381
|
-
return new Command(`deploy`).argument(`[instanceName]`, `Instance name`, savedInstanceName()).description(
|
|
19700
|
+
return new Command(`deploy`).argument(`[instanceName]`, `Instance name`, savedInstanceName()).description(
|
|
19701
|
+
`Deploy pb_hooks, pb_migrations, pb_public and packages to an instance`
|
|
19702
|
+
).option(`-v, --verbose`, `Verbose output`).option(`--sftp`, `Sync over SFTP instead of the deploy API`).option(
|
|
19703
|
+
`--commit <sha>`,
|
|
19704
|
+
`Commit SHA to record (default: $GITHUB_SHA or git HEAD)`
|
|
19705
|
+
).option(`-m, --message <message>`, `Note to record with this deploy`).option(
|
|
19382
19706
|
"-i, --include <include...>",
|
|
19383
19707
|
"Files to include in the sync",
|
|
19384
19708
|
(val, prev) => [...prev, ...val.split(",")],
|
|
@@ -19391,35 +19715,15 @@ var DeployCommand = () => {
|
|
|
19391
19715
|
},
|
|
19392
19716
|
DEFAULT_EXCLUDES
|
|
19393
19717
|
).action(async (instanceName, options2) => {
|
|
19394
|
-
const { include, exclude, verbose } = options2;
|
|
19395
|
-
await deployMyCode(instanceName, include, exclude, verbose
|
|
19718
|
+
const { include, exclude, verbose, sftp, commit, message } = options2;
|
|
19719
|
+
await deployMyCode(instanceName, include, exclude, verbose, {
|
|
19720
|
+
sftp,
|
|
19721
|
+
commit,
|
|
19722
|
+
message
|
|
19723
|
+
});
|
|
19396
19724
|
});
|
|
19397
19725
|
};
|
|
19398
19726
|
|
|
19399
|
-
// src/commands/InfoCommand.ts
|
|
19400
|
-
var showInfo = async () => {
|
|
19401
|
-
const auth = await resolveAuthStatus();
|
|
19402
|
-
if (auth.state !== "authenticated") {
|
|
19403
|
-
console.log(`Not logged in. Run 'pockethost login'.`);
|
|
19404
|
-
return;
|
|
19405
|
-
}
|
|
19406
|
-
await ensureDeployKey(auth.client);
|
|
19407
|
-
const status = await getDeployKeyStatus(auth.client);
|
|
19408
|
-
console.log(`Config root: ${CLI_HOME()}`);
|
|
19409
|
-
console.log(`Logged in as: ${auth.email}`);
|
|
19410
|
-
console.log(`Instance: ${savedInstanceName() || "(not linked)"}`);
|
|
19411
|
-
console.log(`Deploy key label: ${DEPLOY_KEY_LABEL}`);
|
|
19412
|
-
console.log(`Deploy key private: ${status.privateKeyPath}`);
|
|
19413
|
-
console.log(`Deploy key public: ${status.publicKeyPath}`);
|
|
19414
|
-
console.log(`Deploy key fingerprint: ${status.fingerprint}`);
|
|
19415
|
-
console.log(
|
|
19416
|
-
`Deploy key remote: ${formatDeployKeyRemoteStatus(status.remote)}`
|
|
19417
|
-
);
|
|
19418
|
-
};
|
|
19419
|
-
var InfoCommand = () => {
|
|
19420
|
-
return new Command(`info`).alias(`whoami`).description(`Show login and config info`).action(showInfo);
|
|
19421
|
-
};
|
|
19422
|
-
|
|
19423
19727
|
// ../../node_modules/.pnpm/@inquirer+core@11.2.1_@types+node@20.19.43/node_modules/@inquirer/core/dist/lib/key.js
|
|
19424
19728
|
var keybindings = ["emacs", "vim"];
|
|
19425
19729
|
var keybindingLookup = new Set(keybindings);
|
|
@@ -21008,6 +21312,85 @@ var dist_default6 = createPrompt((config2, done) => {
|
|
|
21008
21312
|
return `${lines}${cursorHide}`;
|
|
21009
21313
|
});
|
|
21010
21314
|
|
|
21315
|
+
// src/commands/TokenCommand.ts
|
|
21316
|
+
var TOKENS_URL = "https://app.pockethost.io/account/tokens";
|
|
21317
|
+
var saveToken = async (raw) => {
|
|
21318
|
+
const token = (raw ?? await dist_default5({
|
|
21319
|
+
message: `Paste your PocketHost token (create one at ${TOKENS_URL})`,
|
|
21320
|
+
mask: true
|
|
21321
|
+
})).trim();
|
|
21322
|
+
if (!isPat(token)) {
|
|
21323
|
+
throw new Error(
|
|
21324
|
+
`That doesn't look like a PocketHost token (expected ph_pat_\u2026). Create one at ${TOKENS_URL}`
|
|
21325
|
+
);
|
|
21326
|
+
}
|
|
21327
|
+
const who = await tokenWhoami(token);
|
|
21328
|
+
config("token", token);
|
|
21329
|
+
console.log(`Saved token ${describeToken(who)}`);
|
|
21330
|
+
};
|
|
21331
|
+
var showTokenStatus = async () => {
|
|
21332
|
+
const env3 = TOKEN();
|
|
21333
|
+
const token = env3 || savedToken();
|
|
21334
|
+
if (!token) {
|
|
21335
|
+
console.log(
|
|
21336
|
+
`No token. Set POCKETHOST_TOKEN or run 'pockethost token set'. Create one at ${TOKENS_URL}`
|
|
21337
|
+
);
|
|
21338
|
+
return;
|
|
21339
|
+
}
|
|
21340
|
+
console.log(
|
|
21341
|
+
`Using ${maskToken(token)} from ${env3 ? "POCKETHOST_TOKEN" : "saved config"}`
|
|
21342
|
+
);
|
|
21343
|
+
try {
|
|
21344
|
+
console.log(describeToken(await tokenWhoami(token)));
|
|
21345
|
+
} catch (e2) {
|
|
21346
|
+
console.log(`Token check failed: ${e2.message}`);
|
|
21347
|
+
process.exitCode = 1;
|
|
21348
|
+
}
|
|
21349
|
+
};
|
|
21350
|
+
var TokenCommand = () => new Command("token").description(`Manage the personal access token used for deploys`).addCommand(
|
|
21351
|
+
new Command("set").argument("[token]", "Token (ph_pat_\u2026); prompts when omitted").description("Verify and save a token").action((token) => saveToken(token))
|
|
21352
|
+
).addCommand(
|
|
21353
|
+
new Command("clear").description("Remove the saved token").action(() => {
|
|
21354
|
+
config("token", "");
|
|
21355
|
+
console.log("Saved token removed.");
|
|
21356
|
+
})
|
|
21357
|
+
).addCommand(
|
|
21358
|
+
new Command("status").description("Show which token is in use").action(showTokenStatus)
|
|
21359
|
+
).action(showTokenStatus);
|
|
21360
|
+
|
|
21361
|
+
// src/commands/InfoCommand.ts
|
|
21362
|
+
var showInfo = async () => {
|
|
21363
|
+
const token = TOKEN() || savedToken();
|
|
21364
|
+
if (token) {
|
|
21365
|
+
console.log(`Config root: ${CLI_HOME()}`);
|
|
21366
|
+
console.log(`Instance: ${savedInstanceName() || "(not linked)"}`);
|
|
21367
|
+
await showTokenStatus();
|
|
21368
|
+
return;
|
|
21369
|
+
}
|
|
21370
|
+
const auth = await resolveAuthStatus();
|
|
21371
|
+
if (auth.state !== "authenticated") {
|
|
21372
|
+
console.log(
|
|
21373
|
+
`Not logged in. Run 'pockethost login', or set POCKETHOST_TOKEN / run 'pockethost token set'.`
|
|
21374
|
+
);
|
|
21375
|
+
return;
|
|
21376
|
+
}
|
|
21377
|
+
await ensureDeployKey(auth.client);
|
|
21378
|
+
const status = await getDeployKeyStatus(auth.client);
|
|
21379
|
+
console.log(`Config root: ${CLI_HOME()}`);
|
|
21380
|
+
console.log(`Logged in as: ${auth.email}`);
|
|
21381
|
+
console.log(`Instance: ${savedInstanceName() || "(not linked)"}`);
|
|
21382
|
+
console.log(`Deploy key label: ${DEPLOY_KEY_LABEL}`);
|
|
21383
|
+
console.log(`Deploy key private: ${status.privateKeyPath}`);
|
|
21384
|
+
console.log(`Deploy key public: ${status.publicKeyPath}`);
|
|
21385
|
+
console.log(`Deploy key fingerprint: ${status.fingerprint}`);
|
|
21386
|
+
console.log(
|
|
21387
|
+
`Deploy key remote: ${formatDeployKeyRemoteStatus(status.remote)}`
|
|
21388
|
+
);
|
|
21389
|
+
};
|
|
21390
|
+
var InfoCommand = () => {
|
|
21391
|
+
return new Command(`info`).alias(`whoami`).description(`Show login and config info`).action(showInfo);
|
|
21392
|
+
};
|
|
21393
|
+
|
|
21011
21394
|
// src/commands/LinkCommand.ts
|
|
21012
21395
|
var link = async (instanceName) => {
|
|
21013
21396
|
saveInstanceName(instanceName);
|
|
@@ -21108,12 +21491,19 @@ var loginWithUserInput = async () => {
|
|
|
21108
21491
|
}
|
|
21109
21492
|
console.log(`Logged in!`);
|
|
21110
21493
|
};
|
|
21111
|
-
var LoginCommand = () => new Command("login").description(`Log in to PocketHost`).
|
|
21494
|
+
var LoginCommand = () => new Command("login").description(`Log in to PocketHost (email/password, or --token)`).option(`--token [token]`, `Log in with a personal access token instead`).action(async (opts) => {
|
|
21495
|
+
if (opts.token !== void 0) {
|
|
21496
|
+
await saveToken(typeof opts.token === "string" ? opts.token : void 0);
|
|
21497
|
+
return;
|
|
21498
|
+
}
|
|
21499
|
+
await loginWithUserInput();
|
|
21500
|
+
});
|
|
21112
21501
|
|
|
21113
21502
|
// src/commands/LogoutCommand.ts
|
|
21114
21503
|
var logout = async () => {
|
|
21115
21504
|
config(`pb_auth`, "");
|
|
21116
|
-
|
|
21505
|
+
config(`token`, "");
|
|
21506
|
+
console.log(`Logged out! (saved login and token removed)`);
|
|
21117
21507
|
};
|
|
21118
21508
|
var LogoutCommand = () => new Command("logout").description(`Log out of PocketHost`).helpOption(false).action(logout);
|
|
21119
21509
|
|
|
@@ -21520,7 +21910,7 @@ var SftpCommand = () => {
|
|
|
21520
21910
|
|
|
21521
21911
|
// src/cli.ts
|
|
21522
21912
|
ensureProjectRoot();
|
|
21523
|
-
program.name(`pockethost`).version(version).description(`Deploy, sync, and tail logs for your PocketHost instances`).addCommand(LoginCommand()).addCommand(LogsCommand()).addCommand(DevCommand()).addCommand(ListCommand()).addCommand(LinkCommand()).addCommand(DeployCommand()).addCommand(SftpCommand()).addCommand(LogoutCommand()).addCommand(InfoCommand());
|
|
21913
|
+
program.name(`pockethost`).version(version).description(`Deploy, sync, and tail logs for your PocketHost instances`).addCommand(LoginCommand()).addCommand(TokenCommand()).addCommand(LogsCommand()).addCommand(DevCommand()).addCommand(ListCommand()).addCommand(LinkCommand()).addCommand(DeployCommand()).addCommand(SftpCommand()).addCommand(LogoutCommand()).addCommand(InfoCommand());
|
|
21524
21914
|
program.exitOverride();
|
|
21525
21915
|
program.parseAsync(process.argv).catch((err) => {
|
|
21526
21916
|
if (err.code === "commander.helpDisplayed" || err.code === "commander.help" || err.code === "commander.version") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pockethost",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "The PocketHost CLI: deploy, sync, and tail logs for your PocketHost instances",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=20"
|
|
27
27
|
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node scripts/build.mjs",
|
|
30
|
+
"check:types": "tsc --noEmit",
|
|
31
|
+
"dev": "node --import tsx ./src/cli.ts",
|
|
32
|
+
"start": "node --import tsx ./src/cli.ts",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"prepublishOnly": "pnpm check:types && pnpm test",
|
|
35
|
+
"prepack": "pnpm build"
|
|
36
|
+
},
|
|
28
37
|
"files": [
|
|
29
38
|
"dist",
|
|
30
39
|
"CHANGELOG.md",
|
|
@@ -58,12 +67,5 @@
|
|
|
58
67
|
"semi": false,
|
|
59
68
|
"singleQuote": true,
|
|
60
69
|
"trailingComma": "es5"
|
|
61
|
-
},
|
|
62
|
-
"scripts": {
|
|
63
|
-
"build": "node scripts/build.mjs",
|
|
64
|
-
"check:types": "tsc --noEmit",
|
|
65
|
-
"dev": "node --import tsx ./src/cli.ts",
|
|
66
|
-
"start": "node --import tsx ./src/cli.ts",
|
|
67
|
-
"test": "vitest run"
|
|
68
70
|
}
|
|
69
|
-
}
|
|
71
|
+
}
|
package/LICENSE.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
4
|
-
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
5
|
-
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
6
|
-
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
|
|
7
|
-
is furnished to do so, subject to the following conditions:
|
|
8
|
-
|
|
9
|
-
The above copyright notice and this permission notice shall be included in all copies or
|
|
10
|
-
substantial portions of the Software.
|
|
11
|
-
|
|
12
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
13
|
-
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
14
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
15
|
-
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
16
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|