ninepm 0.65.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +115 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,15 @@ Deploy a static directory:
|
|
|
62
62
|
9pm deploy . --name my-app --slug my-app
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### How long an app's address stays the same
|
|
66
|
+
|
|
67
|
+
Check this before printing a link or QR code. The live URL is `<slug>-<suffix>.9pm.ai`, and the suffix comes from the app's internal id, not from the slug:
|
|
68
|
+
|
|
69
|
+
- Redeploying under the same slug keeps the address.
|
|
70
|
+
- Deleting the app and creating it again under the same slug gives it a **new** address, because the new app gets a new id. Anything printed with the old one stops working.
|
|
71
|
+
- There is no rename: a different address means a different app.
|
|
72
|
+
- A custom domain you own, added on the dashboard's Domains page, gives the app an address of your own. Not every plan includes custom domains.
|
|
73
|
+
|
|
65
74
|
Deploy a built framework output directory:
|
|
66
75
|
|
|
67
76
|
```sh
|
|
@@ -106,7 +115,7 @@ Deploy a first-class Worker runtime app:
|
|
|
106
115
|
|
|
107
116
|
The `--runtime worker` entry must be a bundled fetch-compatible JavaScript module with a default export. With `--with-db`, the app receives `env.DB` and should use SQLite-compatible prepared statements.
|
|
108
117
|
|
|
109
|
-
Choose which files are public with `--assets <dir>`: only the files under that directory are served, from the app's root (`public/index.html` answers on `/`), and nothing else in the deploy directory is uploaded. Use `--no-assets` when the entry serves every response itself. The app remembers the choice, so later deploys need not repeat the flag; pass the other flag, or another directory, to change it — there is no way back to publishing everything. A worker app that has never chosen serves **every file in the deploy directory except the entry** publicly, server helpers and leftovers like `index.js.orig` included; every deploy of such an app prints a warning naming those files, and `9pm deploy --check` lists every file a deploy would make public.
|
|
118
|
+
Choose which files are public with `--assets <dir>`: only the files under that directory are served, from the app's root (`public/index.html` answers on `/`), and nothing else in the deploy directory is uploaded. Use `--no-assets` when the entry serves every response itself. The app remembers the choice, so later deploys need not repeat the flag; pass the other flag, or another directory, to change it — there is no way back to publishing everything. A worker app that has never chosen serves **every file in the deploy directory except the entry** publicly, server helpers and leftovers like `index.js.orig` included; every deploy of such an app prints a warning naming those files, and `9pm deploy --check` lists every file a deploy would make public. Its summary counts the files scanned apart from the files selected for upload (the entry and the public files); `--check` itself uploads nothing.
|
|
110
119
|
|
|
111
120
|
Deploy a container app from a local Docker build:
|
|
112
121
|
|
|
@@ -137,6 +146,8 @@ Delete an app, its managed database, managed container app, and retained image:
|
|
|
137
146
|
9pm delete my-app --confirm my-app
|
|
138
147
|
```
|
|
139
148
|
|
|
149
|
+
The app's address goes with it: creating an app again under the same slug gives it a new address, so printed links and QR codes stop working. `9pm delete` prints this warning before it deletes anything.
|
|
150
|
+
|
|
140
151
|
## Access Modes
|
|
141
152
|
|
|
142
153
|
Apps are public by default.
|
|
@@ -221,6 +232,7 @@ Options:
|
|
|
221
232
|
- `--name <name>` sets the app display name
|
|
222
233
|
- `--slug <slug>` sets the URL-safe app slug
|
|
223
234
|
- `--with-db` requests managed storage for the app
|
|
235
|
+
- `--with-auth` gives the app 9pm-managed end-user sign-in (email one-time codes) and implies `--with-db`. Once enabled it stays on for every later deploy of the app, with or without the flag; it can't be turned off
|
|
224
236
|
- `--database-name <name>` customizes the requested database name
|
|
225
237
|
- `--binding <name>` customizes the database binding name
|
|
226
238
|
- `--environment <name>` sets deployment environment metadata
|
package/dist/index.d.ts
CHANGED
|
@@ -38,10 +38,17 @@ export interface DeployResult {
|
|
|
38
38
|
project: Project;
|
|
39
39
|
deployment: Deployment;
|
|
40
40
|
database: AppDatabase | null;
|
|
41
|
+
databases: BoundDatabase[];
|
|
41
42
|
uploaded: number;
|
|
42
43
|
url: string;
|
|
43
44
|
containerImageBytes?: number;
|
|
44
45
|
}
|
|
46
|
+
export interface BoundDatabase {
|
|
47
|
+
bindingName: string;
|
|
48
|
+
name: string | null;
|
|
49
|
+
}
|
|
50
|
+
export declare function boundDatabasesForDeployment(deployment: Deployment, provisioned: AppDatabase | null): BoundDatabase[];
|
|
51
|
+
export declare function deployDatabaseClause(databases: readonly BoundDatabase[]): string | null;
|
|
45
52
|
export interface ApiOptions {
|
|
46
53
|
apiUrl: string;
|
|
47
54
|
token: string;
|
|
@@ -117,7 +124,7 @@ export interface CreatedServiceToken {
|
|
|
117
124
|
clientSecret: string;
|
|
118
125
|
}
|
|
119
126
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
120
|
-
export declare const CLI_VERSION = "0.
|
|
127
|
+
export declare const CLI_VERSION = "0.69.0";
|
|
121
128
|
type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
|
|
122
129
|
export interface StoredCredentialEntry {
|
|
123
130
|
token: string;
|
|
@@ -191,6 +198,10 @@ export interface DeployCheckResult {
|
|
|
191
198
|
path: string;
|
|
192
199
|
url: string;
|
|
193
200
|
}>;
|
|
201
|
+
upload?: {
|
|
202
|
+
files: number;
|
|
203
|
+
bytes: number;
|
|
204
|
+
};
|
|
194
205
|
}
|
|
195
206
|
export type DetectedFramework = "nextjs" | "astro" | "sveltekit" | "vite";
|
|
196
207
|
export declare function detectFramework(directory: string): DetectedFramework | null;
|
|
@@ -230,6 +241,7 @@ export declare function measureWorkerModuleFiles(root: string, modulePaths: read
|
|
|
230
241
|
compressedBytes: number;
|
|
231
242
|
}>;
|
|
232
243
|
export declare function checkDeployDirectory(options: DeployOptions): Promise<DeployCheckResult>;
|
|
244
|
+
export declare function deployCheckSummary(check: DeployCheckResult, publicAssets: string | undefined): string;
|
|
233
245
|
export declare function whoami(options: ApiOptions): Promise<WhoamiResult>;
|
|
234
246
|
export declare function publicWhoamiResult(result: WhoamiResult): PublicWhoamiResult;
|
|
235
247
|
export type CredentialRevocation = {
|
|
@@ -331,6 +343,7 @@ export interface ListSitesResult {
|
|
|
331
343
|
export declare function listSites(options: ApiOptions & {
|
|
332
344
|
owner?: string;
|
|
333
345
|
}): Promise<ListSitesResult>;
|
|
346
|
+
export declare function deleteWarningLines(slug: string): string[];
|
|
334
347
|
export declare function deleteSite(options: DeleteSiteOptions): Promise<DeletedSiteResult>;
|
|
335
348
|
export interface DeviceAuthFeedbackOptions {
|
|
336
349
|
progressStream?: NodeJS.WriteStream;
|
|
@@ -664,6 +677,17 @@ export declare function parseEnvFileEntries(content: string, kind: "secret" | "v
|
|
|
664
677
|
value: string;
|
|
665
678
|
kind: "secret" | "var";
|
|
666
679
|
}>;
|
|
680
|
+
export type ListedDeployment = Pick<Deployment, "id" | "version" | "status" | "createdAt"> & {
|
|
681
|
+
active: boolean;
|
|
682
|
+
};
|
|
683
|
+
export declare function deploymentStatusLabel(deployment: ListedDeployment): string;
|
|
684
|
+
export declare function formatDeploymentsList(deployments: ListedDeployment[]): string[];
|
|
685
|
+
export declare function deploymentsListJson<T extends ListedDeployment>(deployments: T[]): {
|
|
686
|
+
servingDeploymentId: string | null;
|
|
687
|
+
deployments: Array<T & {
|
|
688
|
+
serving: boolean;
|
|
689
|
+
}>;
|
|
690
|
+
};
|
|
667
691
|
export declare function healthWithReason(status: string | null | undefined, reason: string | null | undefined): string;
|
|
668
692
|
export declare function workerPublicFiles(paths: readonly string[], serverEntry: string, selection: string | null): Array<{
|
|
669
693
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,40 @@ import { MAX_STATIC_ASSET_FILE_BYTES, MAX_WORKER_ENTRY_COMPRESSED_BYTES, MAX_WOR
|
|
|
16
16
|
import { buildFileManifest, readManifestFile } from "./manifest.js";
|
|
17
17
|
import { NO_PUBLIC_ASSETS, normalizeAssetsDirectory, privateLookingPaths, publicAssetPath } from "./public-assets.js";
|
|
18
18
|
import { API_KEY_HELP, API_URL_HELP, CLI_CLIENT_ID, DEFAULT_API_URL, reportCliError } from "./cli-config.js";
|
|
19
|
-
|
|
19
|
+
// What the deployment binds, read from the deployment the server returned (#1949). The
|
|
20
|
+
// server binds every provisioned database of the app on each publish whatever flags were
|
|
21
|
+
// passed, so `--with-db` says nothing about the result. Only a deployment carrying no
|
|
22
|
+
// `databases` record at all - the local mock target, or a server that predates recording
|
|
23
|
+
// it for this runtime - falls back to what this run itself provisioned.
|
|
24
|
+
export function boundDatabasesForDeployment(deployment, provisioned) {
|
|
25
|
+
const recorded = deployment.metadata["databases"];
|
|
26
|
+
if (!Array.isArray(recorded)) {
|
|
27
|
+
return provisioned ? [{ bindingName: provisioned.bindingName, name: provisioned.name }] : [];
|
|
28
|
+
}
|
|
29
|
+
const bound = [];
|
|
30
|
+
for (const entry of recorded) {
|
|
31
|
+
if (!entry || typeof entry !== "object") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const record = entry;
|
|
35
|
+
if (typeof record["bindingName"] !== "string") {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const name = typeof record["name"] === "string" ? record["name"] : provisioned && record["id"] === provisioned.id ? provisioned.name : null;
|
|
39
|
+
bound.push({ bindingName: record["bindingName"], name });
|
|
40
|
+
}
|
|
41
|
+
return bound;
|
|
42
|
+
}
|
|
43
|
+
// The database clause of the deploy summary, or null when nothing is bound - the summary
|
|
44
|
+
// then says nothing about a database rather than "no database".
|
|
45
|
+
export function deployDatabaseClause(databases) {
|
|
46
|
+
if (databases.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const listed = databases.map((database) => (database.name ? `${database.bindingName}:${database.name}` : database.bindingName)).join(", ");
|
|
50
|
+
return `${databases.length === 1 ? "database" : "databases"} ${listed}`;
|
|
51
|
+
}
|
|
52
|
+
export const CLI_VERSION = "0.69.0";
|
|
20
53
|
const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
|
|
21
54
|
const TOKEN_SERVICE = "9pm";
|
|
22
55
|
const TOKEN_ACCOUNT = "default";
|
|
@@ -442,6 +475,7 @@ async function runDeploy(initialOptions, slugHolder) {
|
|
|
442
475
|
project,
|
|
443
476
|
deployment: finalized,
|
|
444
477
|
database,
|
|
478
|
+
databases: boundDatabasesForDeployment(finalized, database),
|
|
445
479
|
uploaded: 1,
|
|
446
480
|
url: finalized.url,
|
|
447
481
|
containerImageBytes: image.size,
|
|
@@ -637,6 +671,7 @@ async function runDeploy(initialOptions, slugHolder) {
|
|
|
637
671
|
project,
|
|
638
672
|
deployment: finalized,
|
|
639
673
|
database,
|
|
674
|
+
databases: boundDatabasesForDeployment(finalized, database),
|
|
640
675
|
uploaded,
|
|
641
676
|
url: finalized.url,
|
|
642
677
|
};
|
|
@@ -812,9 +847,12 @@ export async function checkDeployDirectory(options) {
|
|
|
812
847
|
// #1847: `--check` has no project, so it cannot see a choice made on an earlier deploy;
|
|
813
848
|
// it reports what this invocation's flags would publish, and says so for the legacy case.
|
|
814
849
|
let publicFiles;
|
|
850
|
+
let upload;
|
|
815
851
|
if (serverEntry) {
|
|
816
852
|
const selection = options.publicAssets ?? null;
|
|
817
853
|
publicFiles = workerPublicFiles(manifest.map((file) => file.path), serverEntry, selection);
|
|
854
|
+
const selected = uploadedFiles(manifest, serverEntry, selection);
|
|
855
|
+
upload = { files: selected.length, bytes: selected.reduce((sum, file) => sum + file.size, 0) };
|
|
818
856
|
if (selection !== null && selection !== NO_PUBLIC_ASSETS && publicFiles.length === 0) {
|
|
819
857
|
errors.push(missingAssetsDirectoryMessage(selection));
|
|
820
858
|
}
|
|
@@ -847,8 +885,26 @@ export async function checkDeployDirectory(options) {
|
|
|
847
885
|
bytes: totalBytes,
|
|
848
886
|
...(serverCode ? { serverCode } : {}),
|
|
849
887
|
...(publicFiles ? { publicFiles } : {}),
|
|
888
|
+
...(upload ? { upload } : {}),
|
|
850
889
|
};
|
|
851
890
|
}
|
|
891
|
+
// The `--check` success line. Static and bundle deploys upload every file they scan, so one
|
|
892
|
+
// count is the whole story there. An entry-based worker deploy uploads only the entry and
|
|
893
|
+
// its public files, so it reports both counts; with no --assets/--no-assets, a choice the
|
|
894
|
+
// app saved on an earlier deploy applies on the server, which a local check cannot see.
|
|
895
|
+
export function deployCheckSummary(check, publicAssets) {
|
|
896
|
+
if (!check.upload) {
|
|
897
|
+
return `Deploy check passed: ${check.files} file(s), ${check.bytes} byte(s).`;
|
|
898
|
+
}
|
|
899
|
+
const publicCount = check.publicFiles?.length ?? 0;
|
|
900
|
+
const breakdown = publicAssets === NO_PUBLIC_ASSETS ? "the entry only" : `the entry + ${publicCount} public file(s)`;
|
|
901
|
+
const summary = `Deploy check passed: ${check.files} file(s) scanned, ${check.upload.files} selected for upload (${breakdown}), ` +
|
|
902
|
+
`${check.upload.bytes.toLocaleString("en-US")} byte(s) selected.`;
|
|
903
|
+
if (publicAssets !== undefined) {
|
|
904
|
+
return summary;
|
|
905
|
+
}
|
|
906
|
+
return `${summary}\n Counted from the flags supplied (no --assets or --no-assets). If this app saved a public-assets choice on an earlier deploy, that choice applies on deploy and only the entry and the files it makes public are uploaded.`;
|
|
907
|
+
}
|
|
852
908
|
export async function whoami(options) {
|
|
853
909
|
return apiRequest(options, "/v1/me", { method: "GET" });
|
|
854
910
|
}
|
|
@@ -1324,6 +1380,16 @@ export async function listSites(options) {
|
|
|
1324
1380
|
const payload = await apiRequest(options, route, { method: "GET" });
|
|
1325
1381
|
return { projects: payload.projects };
|
|
1326
1382
|
}
|
|
1383
|
+
// Printed before the delete request, never as a prompt: agents run this non-interactively
|
|
1384
|
+
// and `--confirm` is already the consent. The address part is the one nobody expects:
|
|
1385
|
+
// the hostname's suffix comes from the project id (appHostnameForProject), a new app gets
|
|
1386
|
+
// a new random id, and no route renames a project, so the same slug never gets it back.
|
|
1387
|
+
export function deleteWarningLines(slug) {
|
|
1388
|
+
return [
|
|
1389
|
+
`Warning: deleting ${slug} also gives up its address for good. An app created again under the same slug gets a new address, so links, bookmarks and printed QR codes pointing at the current one will stop working.`,
|
|
1390
|
+
"Its deployments, files and managed database (if it has one) are deleted with it and cannot be recovered.",
|
|
1391
|
+
];
|
|
1392
|
+
}
|
|
1327
1393
|
export async function deleteSite(options) {
|
|
1328
1394
|
const route = `/v1/sites/${encodeURIComponent(options.site)}${options.owner ? `?owner=${encodeURIComponent(options.owner)}` : ""}`;
|
|
1329
1395
|
const payload = await apiRequest(options, route, {
|
|
@@ -2305,7 +2371,14 @@ function buildContainerImage(directory) {
|
|
|
2305
2371
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "9pm-container-"));
|
|
2306
2372
|
const tag = `9pm-local-${Date.now()}-${process.pid}`;
|
|
2307
2373
|
const imagePath = path.join(tempDir, "image.tar");
|
|
2308
|
-
|
|
2374
|
+
// Build without Buildx's default provenance attestation. With it, Docker Desktop saves the
|
|
2375
|
+
// image as an index carrying an attestation manifest, and the push to Cloudflare's registry
|
|
2376
|
+
// through `wrangler containers push` fails with "blob unknown to registry"; the same image
|
|
2377
|
+
// without the attestation pushes (#1906: two of two attempts each way). The environment
|
|
2378
|
+
// variable rather than `--provenance=false`: it is what that test pushed, and a builder that
|
|
2379
|
+
// has no attestations - the legacy non-Buildx `docker build` - ignores an unknown variable
|
|
2380
|
+
// where it would reject an unknown flag. SBOM generation is untouched: it is off by default.
|
|
2381
|
+
runDocker(["build", "--platform", "linux/amd64", "-t", tag, root], "build container image", { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" });
|
|
2309
2382
|
const inspected = spawnSync("docker", ["image", "inspect", tag, "--format", "{{.Os}}/{{.Architecture}}"], { encoding: "utf8" });
|
|
2310
2383
|
if (inspected.status !== 0) {
|
|
2311
2384
|
throw new Error(`Failed to inspect container image: ${inspected.stderr || inspected.stdout}`.trim());
|
|
@@ -2318,8 +2391,8 @@ function buildContainerImage(directory) {
|
|
|
2318
2391
|
const sha256 = createHash("sha256").update(readFileSync(imagePath)).digest("hex");
|
|
2319
2392
|
return { tag, path: imagePath, size, sha256 };
|
|
2320
2393
|
}
|
|
2321
|
-
function runDocker(args, action) {
|
|
2322
|
-
const result = spawnSync("docker", args, { stdio: "inherit" });
|
|
2394
|
+
function runDocker(args, action, extraEnv = {}) {
|
|
2395
|
+
const result = spawnSync("docker", args, { stdio: "inherit", env: { ...process.env, ...extraEnv } });
|
|
2323
2396
|
if (result.status !== 0) {
|
|
2324
2397
|
throw new Error(`Failed to ${action}.`);
|
|
2325
2398
|
}
|
|
@@ -3053,21 +3126,45 @@ function parseDeploymentsArgs(argv) {
|
|
|
3053
3126
|
}
|
|
3054
3127
|
throw new Error(`Unknown deployments subcommand: ${subcommand}`);
|
|
3055
3128
|
}
|
|
3129
|
+
export function deploymentStatusLabel(deployment) {
|
|
3130
|
+
if (deployment.active) {
|
|
3131
|
+
return "serving";
|
|
3132
|
+
}
|
|
3133
|
+
return deployment.status === "active" ? "active, not serving" : deployment.status;
|
|
3134
|
+
}
|
|
3135
|
+
export function formatDeploymentsList(deployments) {
|
|
3136
|
+
if (deployments.length === 0) {
|
|
3137
|
+
return ["No deployments."];
|
|
3138
|
+
}
|
|
3139
|
+
const header = ["ID", "VERSION", "STATUS", "CREATED"];
|
|
3140
|
+
const rows = deployments.map((deployment) => [deployment.id, `v${deployment.version}`, deploymentStatusLabel(deployment), deployment.createdAt]);
|
|
3141
|
+
const widths = header.map((title, column) => Math.max(title.length, ...rows.map((row) => (row[column] ?? "").length)));
|
|
3142
|
+
const render = (cells) => cells.map((cell, column) => (column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0))).join(" ");
|
|
3143
|
+
const lines = [render(header), ...rows.map(render), ""];
|
|
3144
|
+
const serving = deployments.find((deployment) => deployment.active);
|
|
3145
|
+
lines.push(serving ? `Serving: v${serving.version} (${serving.id}).` : "No deployment is serving this app.");
|
|
3146
|
+
if (deployments.some((deployment) => !deployment.active && deployment.status === "active")) {
|
|
3147
|
+
lines.push('Rows marked "active, not serving" are earlier deploys that finished; they no longer receive traffic.');
|
|
3148
|
+
}
|
|
3149
|
+
return lines;
|
|
3150
|
+
}
|
|
3151
|
+
export function deploymentsListJson(deployments) {
|
|
3152
|
+
return {
|
|
3153
|
+
servingDeploymentId: deployments.find((deployment) => deployment.active)?.id ?? null,
|
|
3154
|
+
deployments: deployments.map((deployment) => ({ ...deployment, serving: deployment.active })),
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3056
3157
|
async function runDeploymentsCommand(command) {
|
|
3057
3158
|
const base = { apiUrl: command.apiUrl, token: command.token };
|
|
3058
3159
|
const action = command.action;
|
|
3059
3160
|
if (action.kind === "ls") {
|
|
3060
3161
|
const result = await apiRequest(base, `/v1/sites/${encodeURIComponent(action.site)}/deployments`, { method: "GET" });
|
|
3061
3162
|
if (action.json) {
|
|
3062
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3163
|
+
console.log(JSON.stringify({ ...result, ...deploymentsListJson(result.deployments) }, null, 2));
|
|
3063
3164
|
return;
|
|
3064
3165
|
}
|
|
3065
|
-
|
|
3066
|
-
console.log(
|
|
3067
|
-
return;
|
|
3068
|
-
}
|
|
3069
|
-
for (const deployment of result.deployments) {
|
|
3070
|
-
console.log(`${deployment.active ? "*" : " "} ${deployment.id}\tv${deployment.version}\t${deployment.status}\t${deployment.createdAt}`);
|
|
3166
|
+
for (const line of formatDeploymentsList(result.deployments)) {
|
|
3167
|
+
console.log(line);
|
|
3071
3168
|
}
|
|
3072
3169
|
return;
|
|
3073
3170
|
}
|
|
@@ -4389,7 +4486,7 @@ async function main() {
|
|
|
4389
4486
|
console.log("Deploy check passed: Dockerfile found and Docker is available.");
|
|
4390
4487
|
}
|
|
4391
4488
|
else {
|
|
4392
|
-
console.log(
|
|
4489
|
+
console.log(deployCheckSummary(check, command.options.publicAssets));
|
|
4393
4490
|
if (check.serverCode) {
|
|
4394
4491
|
// The compressed server-code figure, printed on success as well as on
|
|
4395
4492
|
// warning/rejection, so the number the size limit is enforced on is always
|
|
@@ -4438,10 +4535,10 @@ async function main() {
|
|
|
4438
4535
|
const siteFlag = slug ? ` --site ${quoteSiteArg(slug)}` : "";
|
|
4439
4536
|
throw new Error(`${base}\n\nStuck? Report it with context: 9pm support${siteFlag} -m "<what happened>"`);
|
|
4440
4537
|
}
|
|
4441
|
-
const
|
|
4538
|
+
const databaseClause = deployDatabaseClause(result.databases);
|
|
4442
4539
|
const queued = result.deployment.status === "queued" || result.deployment.status === "publishing";
|
|
4443
4540
|
console.log(`${queued ? "Queued" : "Deployed"} ${result.project.slug} -> ${result.url}`);
|
|
4444
|
-
console.log(`Deployment ${result.deployment.id} ${result.containerImageBytes ? `uploaded ${formatBytesForCli(result.containerImageBytes)} image` : `uploaded ${result.uploaded} file(s)`}
|
|
4541
|
+
console.log(`Deployment ${result.deployment.id} ${result.containerImageBytes ? `uploaded ${formatBytesForCli(result.containerImageBytes)} image` : `uploaded ${result.uploaded} file(s)`}${databaseClause ? `, ${databaseClause}` : ""}.`);
|
|
4445
4542
|
if (queued) {
|
|
4446
4543
|
console.log(`Status: ${result.deployment.status}. Run 9pm apps to check when it becomes active.`);
|
|
4447
4544
|
}
|
|
@@ -4637,6 +4734,9 @@ async function main() {
|
|
|
4637
4734
|
return;
|
|
4638
4735
|
}
|
|
4639
4736
|
if (command.kind === "delete") {
|
|
4737
|
+
for (const line of deleteWarningLines(command.confirmSlug)) {
|
|
4738
|
+
console.warn(line);
|
|
4739
|
+
}
|
|
4640
4740
|
const deleted = await deleteSite({
|
|
4641
4741
|
apiUrl: command.apiUrl,
|
|
4642
4742
|
token: command.token,
|
|
@@ -5028,7 +5128,7 @@ Container deploys:
|
|
|
5028
5128
|
Requires Docker locally. The CLI builds linux/amd64, exports the image, uploads it through a short-lived session, then finalizes a queued deployment.
|
|
5029
5129
|
The app must listen on 0.0.0.0:$PORT - always read the PORT env var; on some placements the platform holds the selected --port and hands the app a different one. Use --health-path for a cheap readiness endpoint.
|
|
5030
5130
|
With --with-db, containers receive NINEPM_SQL_URL and NINEPM_DB_BINDINGS for the managed SQL bridge, plus NINEPM_SQL_TOKEN where that bridge requires one - send it as "Authorization: Bearer <token>" or every query is refused.
|
|
5031
|
-
With --with-auth, the app gets 9pm-managed end-user accounts (email one-time codes); it implies --with-db and is one-way
|
|
5131
|
+
With --with-auth, the app gets 9pm-managed end-user accounts (email one-time codes); it implies --with-db and is one-way: once enabled it stays on for every later deploy of the app, with or without the flag.
|
|
5032
5132
|
With --with-email, a signed-in end user can send app invitation emails; it implies --with-auth and works on public apps only.
|
|
5033
5133
|
9pm env set <app> KEY=value [KEY2=value2 ...] [--var] (secret by default; --var = readable config)
|
|
5034
5134
|
9pm env set <app> --from-file <path> [--var] (KEY=value lines; # comments, export prefix, quotes ok)
|