ninepm 0.0.1 → 0.26.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 CHANGED
@@ -1,8 +1,237 @@
1
- # 9pm
1
+ # 9pm.ai CLI
2
2
 
3
- This name is reserved for the [9pm.ai](https://9pm.ai) deploy CLI.
3
+ CLI for deploying built applications to 9pm.ai.
4
4
 
5
- The real CLI will be published under this name shortly. Until then, install
6
- it via the instructions at https://9pm.ai.
5
+ This package contains only client-side deploy code. It does not include the 9pm.ai control plane, infrastructure provisioning code, migrations, staging config, operator tokens, or smoke-test scripts.
7
6
 
8
- Published by TMRW (tmrw.com).
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i -g ninepm # persistent `9pm` binary
11
+ npx ninepm <args> # run without installing
12
+ ```
13
+
14
+ ## Configure
15
+
16
+ Authenticate in your own browser. The CLI stores the token in native protected storage: macOS Keychain, Windows DPAPI, or Linux Secret Service when available. If native storage is unavailable, plaintext file storage is opt-in with `--allow-file-credential-store` or `NINEPM_ALLOW_FILE_CREDENTIAL_STORE=1`.
17
+
18
+ ```sh
19
+ 9pm login
20
+ 9pm whoami
21
+ 9pm doctor
22
+ ```
23
+
24
+ For CI, set `NINEPM_API_KEY` yourself in the CI secret store. `--token` and `NINEPM_API_KEY` still override the stored login token.
25
+
26
+ ## Inventory
27
+
28
+ List the apps you have on 9pm.ai before deploying or modifying:
29
+
30
+ ```sh
31
+ 9pm apps
32
+ 9pm apps --json
33
+ ```
34
+
35
+ The slug column is the handle you pass to `access`, `delete`, and `files --project`.
36
+
37
+ ## Versioning
38
+
39
+ Check installed vs published CLI version:
40
+
41
+ ```sh
42
+ 9pm --version # installed
43
+ 9pm latest # installed vs latest, prints update one-liner when behind
44
+ 9pm doctor # surfaces the same drift line alongside credential/account health
45
+ ```
46
+
47
+ Upgrade:
48
+
49
+ ```sh
50
+ npm i -g ninepm@latest
51
+ ```
52
+
53
+ ## Deploy
54
+
55
+ Deploy a static directory:
56
+
57
+ ```sh
58
+ 9pm deploy . --name my-app --slug my-app
59
+ ```
60
+
61
+ Deploy a built framework output directory:
62
+
63
+ ```sh
64
+ npm run build
65
+ 9pm deploy ./dist --check
66
+ 9pm deploy ./dist --name my-app --slug my-app
67
+ ```
68
+
69
+ Deploy with a managed SQL database:
70
+
71
+ ```sh
72
+ 9pm deploy ./dist --name my-app --slug my-app --with-db
73
+ ```
74
+
75
+ Attach an optional short description to the deploy (shown next to the version in the dashboard; up to 200 characters):
76
+
77
+ ```sh
78
+ 9pm deploy ./dist --name my-app --slug my-app --description "Fix login redirect"
79
+ ```
80
+
81
+ The CLI prints the public app URL after upload and finalization.
82
+
83
+ Store and share working files:
84
+
85
+ ```sh
86
+ 9pm files put ./report.pdf
87
+ 9pm files share <file-id> --expires 24h
88
+ 9pm files put ./notes.md --project my-app --path notes/release.md
89
+ 9pm files share <file-id> --project my-app --login-required
90
+ ```
91
+
92
+ Personal files and project files both count toward the account file storage limit. Share links expire and are capped at 7 days. Login-required links are owner-only.
93
+
94
+ Deploy a first-class Worker runtime app:
95
+
96
+ ```sh
97
+ 9pm deploy ./examples/worker-app --name worker-app --slug worker-app --runtime worker --entry src/index.js --with-db
98
+ ```
99
+
100
+ 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.
101
+
102
+ Deploy a container app from a local Docker build:
103
+
104
+ ```sh
105
+ 9pm deploy . --check --runtime container --port 8080
106
+ 9pm deploy . --name container-app --slug container-app --runtime container --port 8080 --health-path /health --region auto
107
+ ```
108
+
109
+ Container deploys require Docker locally. The CLI builds a `linux/amd64` image, exports it, checks image size against the per-app limit and account retained-image quota, uploads through a short-lived session, and finalizes a queued deployment. The container must listen on `0.0.0.0:$PORT` or the selected `--port`; set `--health-path` when the app has a cheap readiness endpoint.
110
+
111
+ Use `--with-db` when the container needs managed SQL. The app receives `NINEPM_SQL_URL` and `NINEPM_DB_BINDINGS`; send `POST` requests to `NINEPM_SQL_URL` with `{ "binding": "DB", "sql": "SELECT 1", "params": [] }`. For local development, use the app's normal local database adapter behind a repository layer and keep deployed SQL compatible with SQLite.
112
+
113
+ Regions are provider-neutral: `auto`, `us-east`, `us-west`, `europe`, `asia`, `south-america`, `middle-east`, `oceania`, `africa`.
114
+
115
+ When a container app fails to come up or you want to investigate it after the fact, retrieve its recent logs:
116
+
117
+ ```sh
118
+ 9pm logs my-app # recent container logs + the failure reason, if any
119
+ 9pm logs my-app --lines 50 # last N lines
120
+ 9pm logs my-app --json # machine-readable
121
+ ```
122
+
123
+ `9pm logs` reports the latest container deployment's status, health, the failure reason, and the captured startup output when a deploy failed to come up. It is read-only and does not affect deploys.
124
+
125
+ Delete an app, its managed database, managed container app, and retained image:
126
+
127
+ ```sh
128
+ 9pm delete my-app --confirm my-app
129
+ ```
130
+
131
+ ## Access Modes
132
+
133
+ Apps are public by default.
134
+
135
+ One-time PIN with email/domain allowlist:
136
+
137
+ ```sh
138
+ 9pm access allow email my-app dev@example.com
139
+ 9pm access allow domain my-app example.com
140
+ 9pm access otp my-app
141
+ 9pm access revoke email my-app dev@example.com
142
+ 9pm access revoke domain my-app example.com
143
+ ```
144
+
145
+ Identity provider access:
146
+
147
+ ```sh
148
+ 9pm access idp my-app --provider google
149
+ ```
150
+
151
+ Machine access:
152
+
153
+ ```sh
154
+ 9pm access service create my-app --name backend
155
+ ```
156
+
157
+ Return to public:
158
+
159
+ ```sh
160
+ 9pm access public my-app
161
+ ```
162
+
163
+ Create an app-scoped runtime token for server-side allowlist management:
164
+
165
+ ```sh
166
+ 9pm access token create my-app --name production-app-server
167
+ ```
168
+
169
+ Use `9pm_app_...` runtime tokens only from server-side code. They can list, add, and remove email/domain grants for one app. They cannot deploy, create databases, create API keys, or manage another app.
170
+ Service token secrets returned by `access service create` are also server-side secrets.
171
+
172
+ ## Commands
173
+
174
+ ```sh
175
+ 9pm --version
176
+ 9pm deploy [directory] --name <name> [--slug <slug>] [--with-db]
177
+ 9pm deploy [directory] --check [--runtime worker --entry <path>]
178
+ 9pm deploy [directory] --runtime container --port 8080 [--health-path /health] [--region auto] [--with-db]
179
+ 9pm signup
180
+ 9pm login
181
+ 9pm logout
182
+ 9pm doctor [--json]
183
+ 9pm whoami
184
+ 9pm apps [--json]
185
+ 9pm latest [--json]
186
+ 9pm files list [--project <site>]
187
+ 9pm files put <local-path> [--path <remote-path>] [--project <site>]
188
+ 9pm files get <file-id> [--output <path>] [--project <site>]
189
+ 9pm files share <file-id> [--expires 24h] [--login-required] [--project <site>]
190
+ 9pm files delete <file-id> [--project <site>]
191
+ 9pm deploy [directory] --runtime worker --entry <path> [--with-db]
192
+ 9pm delete <site> --confirm <slug>
193
+ 9pm access show <site>
194
+ 9pm access public <site>
195
+ 9pm access otp <site>
196
+ 9pm access idp <site> [--provider google|github|entra]
197
+ 9pm access allow email <site> <email>
198
+ 9pm access allow domain <site> <domain>
199
+ 9pm access revoke email <site> <email>
200
+ 9pm access revoke domain <site> <domain>
201
+ 9pm access service create <site> [--name <name>] [--duration 8760h]
202
+ 9pm access service revoke <site> <service-token-id>
203
+ 9pm access token create <site> [--name <name>]
204
+ 9pm access token revoke <site> <token-id>
205
+ ```
206
+
207
+ Options:
208
+
209
+ - `--api-url <url>` overrides `NINEPM_API_URL`
210
+ - `--token <key>` overrides `NINEPM_API_KEY`
211
+ - `--allow-file-credential-store` permits plaintext fallback storage for `login` and `signup` when native protected storage is unavailable
212
+ - `--name <name>` sets the app display name
213
+ - `--slug <slug>` sets the URL-safe app slug
214
+ - `--with-db` requests managed storage for the app
215
+ - `--database-name <name>` customizes the requested database name
216
+ - `--binding <name>` customizes the database binding name
217
+ - `--environment <name>` sets deployment environment metadata
218
+ - `--runtime <static|worker|container>` selects static asset hosting, app-owned Worker code, or local Docker container deployment
219
+ - `--entry <path>` selects the Worker runtime server entry file
220
+ - `--port <port>` selects the HTTP port exposed by a container app
221
+ - `--health-path <path>` optionally lets 9pm.ai check container health before marking it healthy
222
+ - `--region <region>` optionally selects provider-neutral container placement
223
+ - `--project <site>` scopes file commands to an app
224
+ - `--path <remote-path>` stores a file at a specific path
225
+ - `--output <path>` writes a downloaded file to disk
226
+ - `--expires <duration>` sets public share-link expiry, capped at 7 days
227
+ - `--login-required` creates an owner-only share link
228
+ - `--confirm <slug>` confirms app deletion by requiring the slug
229
+ - `--provider <google|github|entra>` selects a configured identity provider
230
+ - `--duration <duration>` sets a service token duration
231
+
232
+ Access command options:
233
+
234
+ - `--api-url <url>` overrides `NINEPM_API_URL`
235
+ - `--token <key>` overrides `NINEPM_API_KEY`
236
+ - `--name <name>` names an app-scoped runtime token
237
+ - `--expires-at <iso-date>` optionally expires an app-scoped runtime token
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_API_URL = "https://api.9pm.ai";
2
+ export declare const CLI_CLIENT_ID = "ninepm";
3
+ export declare const API_URL_HELP = "Control plane API URL";
4
+ export declare const API_KEY_HELP = "API key from 9pm.ai; overrides stored login";
5
+ export declare function reportCliError(error: unknown): void;
@@ -0,0 +1,17 @@
1
+ // Per-entry CLI configuration. This is the ONLY file that intentionally differs
2
+ // between the shipped ninepm tarball (this copy) and the in-repo dev CLI
3
+ // (src/cli/cli-config.ts). Everything else in this package is generated from
4
+ // src/cli by scripts/generate-cli-package.mjs, so behavior changes land once.
5
+ // Keep the two cli-config.ts files in deliberate sync only for the values below.
6
+ //
7
+ // Tarball variant: targets the production control plane and keeps output terse.
8
+ export const DEFAULT_API_URL = "https://api.9pm.ai";
9
+ // Value recorded in deployment metadata's `cli` field.
10
+ export const CLI_CLIENT_ID = "ninepm";
11
+ // Help text for the NINEPM_API_URL / NINEPM_API_KEY environment variables.
12
+ export const API_URL_HELP = "Control plane API URL";
13
+ export const API_KEY_HELP = "API key from 9pm.ai; overrides stored login";
14
+ // Render an error to stderr.
15
+ export function reportCliError(error) {
16
+ console.error(error instanceof Error ? error.message : error);
17
+ }
@@ -0,0 +1,131 @@
1
+ export type DeploymentStatus = "pending_upload" | "queued" | "publishing" | "active" | "failed";
2
+ export type AppDatabaseStatus = "requested" | "provisioned" | "failed";
3
+ export type AccessMode = "public" | "private_otp" | "private_idp" | "private_service";
4
+ export type AccessGrantType = "email" | "domain";
5
+ export type AccessProviderKey = "google" | "github" | "entra";
6
+ export type AccessProvisioningStatus = "not_configured" | "active" | "failed";
7
+ export type AppRuntime = "static" | "worker" | "container";
8
+ export type ContainerRegion = "auto" | "us-east" | "us-west" | "europe" | "asia" | "south-america" | "middle-east" | "oceania" | "africa";
9
+ export type ManagedFileScope = "user" | "project";
10
+ export type ManagedFileShareMode = "public" | "login_required";
11
+ export interface Project {
12
+ id: string;
13
+ name: string;
14
+ slug: string;
15
+ activeDeploymentId: string | null;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ }
19
+ export interface FileManifestEntry {
20
+ path: string;
21
+ size: number;
22
+ sha256: string;
23
+ contentType: string;
24
+ }
25
+ export interface Deployment {
26
+ id: string;
27
+ projectId: string;
28
+ version: number;
29
+ status: DeploymentStatus;
30
+ environment: string;
31
+ url: string;
32
+ manifest: FileManifestEntry[];
33
+ metadata: Record<string, unknown>;
34
+ createdAt: string;
35
+ finalizedAt: string | null;
36
+ }
37
+ export interface ContainerImageUploadInstruction {
38
+ uploadUrl: string;
39
+ completeUrl?: string;
40
+ method: "PUT";
41
+ expiresAt: string;
42
+ maxBytes: number;
43
+ partBytes?: number;
44
+ headers: Record<string, string>;
45
+ }
46
+ export interface AppDatabase {
47
+ id: string;
48
+ projectId: string;
49
+ name: string;
50
+ bindingName: string;
51
+ status: AppDatabaseStatus;
52
+ providerDatabaseId: string | null;
53
+ providerDatabaseName: string | null;
54
+ migrationsStatus: string;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ }
58
+ export interface ManagedFileObject {
59
+ id: string;
60
+ projectId: string | null;
61
+ scope: ManagedFileScope;
62
+ path: string;
63
+ size: number;
64
+ contentType: string;
65
+ sha256: string;
66
+ createdAt: string;
67
+ updatedAt: string;
68
+ deletedAt: string | null;
69
+ }
70
+ export interface ManagedFileShare {
71
+ id: string;
72
+ fileId: string;
73
+ projectId: string | null;
74
+ mode: ManagedFileShareMode;
75
+ expiresAt: string;
76
+ revokedAt: string | null;
77
+ createdAt: string;
78
+ }
79
+ export interface ManagedFileUsage {
80
+ usedBytes: number;
81
+ quotaBytes: number;
82
+ }
83
+ export interface AppAccessSettings {
84
+ projectId: string;
85
+ mode: AccessMode;
86
+ grantsVersion: number;
87
+ accessAppId: string | null;
88
+ accessAppAud: string | null;
89
+ accessPolicyId: string | null;
90
+ accessHostname: string | null;
91
+ providerKeys: AccessProviderKey[];
92
+ allowedIdps: string[];
93
+ provisioningStatus: AccessProvisioningStatus;
94
+ provisioningError: string | null;
95
+ createdAt: string;
96
+ updatedAt: string;
97
+ }
98
+ export interface AccessGrant {
99
+ id: string;
100
+ projectId: string;
101
+ type: AccessGrantType;
102
+ value: string;
103
+ revokedAt: string | null;
104
+ createdAt: string;
105
+ updatedAt: string;
106
+ }
107
+ export interface AppAccessToken {
108
+ id: string;
109
+ projectId: string;
110
+ name: string;
111
+ prefix: string;
112
+ expiresAt: string | null;
113
+ revokedAt: string | null;
114
+ createdAt: string;
115
+ }
116
+ export interface AppAccessServiceToken {
117
+ id: string;
118
+ projectId: string;
119
+ name: string;
120
+ clientId: string;
121
+ duration: string;
122
+ expiresAt: string | null;
123
+ revokedAt: string | null;
124
+ createdAt: string;
125
+ }
126
+ export interface UploadInstruction {
127
+ path: string;
128
+ method: "PUT";
129
+ uploadUrl: string;
130
+ headers: Record<string, string>;
131
+ }
@@ -0,0 +1,7 @@
1
+ // AUTO-GENERATED by scripts/generate-cli-package.mjs from the in-repo CLI source -- do not edit.
2
+ // Edit src/cli or src/shared instead; this is regenerated on typecheck/test/build/pack.
3
+ // Public, platform-opaque shapes the CLI exchanges with the control plane API.
4
+ // Single source of truth for both the in-repo dev CLI (src/cli) and the shipped
5
+ // ninepm tarball (packages/cli), which receives a generated copy at build time
6
+ // (scripts/generate-cli-package.mjs). Keep free of server-only fields and imports.
7
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare const MAX_DEPLOY_DESCRIPTION_LENGTH = 200;
2
+ export declare class DeployDescriptionTooLongError extends Error {
3
+ constructor();
4
+ }
5
+ /**
6
+ * Normalize a raw description value into what we persist:
7
+ * - non-strings, undefined, and whitespace-only -> null ("no description")
8
+ * - otherwise trimmed
9
+ * Throws {@link DeployDescriptionTooLongError} if the trimmed value exceeds the cap.
10
+ */
11
+ export declare function normalizeDeployDescription(value: unknown): string | null;
@@ -0,0 +1,31 @@
1
+ // AUTO-GENERATED by scripts/generate-cli-package.mjs from the in-repo CLI source -- do not edit.
2
+ // Edit src/cli or src/shared instead; this is regenerated on typecheck/test/build/pack.
3
+ // A deploy description is a short, optional, human-readable note attached to a
4
+ // deployment. Both the CLI and the control plane normalize through here
5
+ // so the rules stay identical on either side of the API.
6
+ export const MAX_DEPLOY_DESCRIPTION_LENGTH = 200;
7
+ export class DeployDescriptionTooLongError extends Error {
8
+ constructor() {
9
+ super(`Deploy description must be at most ${MAX_DEPLOY_DESCRIPTION_LENGTH} characters.`);
10
+ this.name = "DeployDescriptionTooLongError";
11
+ }
12
+ }
13
+ /**
14
+ * Normalize a raw description value into what we persist:
15
+ * - non-strings, undefined, and whitespace-only -> null ("no description")
16
+ * - otherwise trimmed
17
+ * Throws {@link DeployDescriptionTooLongError} if the trimmed value exceeds the cap.
18
+ */
19
+ export function normalizeDeployDescription(value) {
20
+ if (typeof value !== "string") {
21
+ return null;
22
+ }
23
+ const trimmed = value.trim();
24
+ if (trimmed.length === 0) {
25
+ return null;
26
+ }
27
+ if (trimmed.length > MAX_DEPLOY_DESCRIPTION_LENGTH) {
28
+ throw new DeployDescriptionTooLongError();
29
+ }
30
+ return trimmed;
31
+ }
@@ -0,0 +1,7 @@
1
+ export declare const MAX_STATIC_ASSET_FILE_BYTES: number;
2
+ export declare function formatMebibytes(bytes: number): string;
3
+ export declare function assetFileTooLargeMessage(filePath: string, size: number): string;
4
+ export declare function oversizedAssets<T extends {
5
+ path: string;
6
+ size: number;
7
+ }>(files: readonly T[]): T[];
@@ -0,0 +1,30 @@
1
+ // AUTO-GENERATED by scripts/generate-cli-package.mjs from the in-repo CLI source -- do not edit.
2
+ // Edit src/cli or src/shared instead; this is regenerated on typecheck/test/build/pack.
3
+ // Single source of truth for the per-file static-asset size limit, shared by the
4
+ // control plane (the create-deployment guard) and the CLI (`--check` and the
5
+ // real-deploy preflight). Keep this module dependency-free: it is copied verbatim
6
+ // into the shipped ninepm tarball, so it must not import server-only or
7
+ // Cloudflare code.
8
+ // Individual static/worker assets are capped at 25 MiB. A file above this can't
9
+ // be served by the platform at all (inlining it into the Worker script blows the
10
+ // finalize CPU/script-size budget — the #169 failure), so it is rejected up front:
11
+ // at `9pm deploy --check`, before the project is created on a real deploy (#195),
12
+ // and at create time on the server.
13
+ export const MAX_STATIC_ASSET_FILE_BYTES = 25 * 1024 * 1024;
14
+ // Mebibyte-accurate formatter for the size-limit messaging. The general-purpose
15
+ // formatBytes() labels 1024²-based values "MB"; here we say "MiB" so the number,
16
+ // the unit, and the actual binary threshold agree (the cap is 25 MiB, not the
17
+ // 26.2 MB that "25 MB" would imply).
18
+ export function formatMebibytes(bytes) {
19
+ const mib = Math.max(0, bytes) / (1024 * 1024);
20
+ // Whole numbers read cleanly (25 MiB); fractional sizes get one decimal.
21
+ return Number.isInteger(mib) ? `${mib} MiB` : `${mib.toFixed(1)} MiB`;
22
+ }
23
+ export function assetFileTooLargeMessage(filePath, size) {
24
+ return `File "${filePath}" is ${formatMebibytes(size)}; individual files are limited to ${formatMebibytes(MAX_STATIC_ASSET_FILE_BYTES)}.`;
25
+ }
26
+ // Manifest entries that exceed the per-file limit. A structural type keeps this
27
+ // module free of cross-tree type imports so it copies cleanly into the CLI.
28
+ export function oversizedAssets(files) {
29
+ return files.filter((file) => file.size > MAX_STATIC_ASSET_FILE_BYTES);
30
+ }