ninepm 0.55.0 → 0.57.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
@@ -76,6 +76,7 @@ Attach an optional short description to the deploy (shown next to the version in
76
76
 
77
77
  ```sh
78
78
  9pm deploy ./dist --name my-app --slug my-app --description "Fix login redirect"
79
+ 9pm deploy ./dist --name my-app --slug my-app --owner acme # into an organisation you are an admin of
79
80
  ```
80
81
 
81
82
  The CLI prints the public app URL after upload and finalization.
@@ -110,7 +111,7 @@ Deploy a container app from a local Docker build:
110
111
 
111
112
  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` - always read the `PORT` env var rather than hardcoding the selected `--port`, since on some placements the platform holds the selected port and hands the app a different one; set `--health-path` when the app has a cheap readiness endpoint.
112
113
 
113
- 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.
114
+ 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": [] }`. Some placements also set `NINEPM_SQL_TOKEN` — when it is present, send `Authorization: Bearer <that token>` on every request, or the bridge refuses them all with `401 {"error":"unauthorized"}`. Read the URL and the token from the environment on each call rather than hardcoding either: the URL differs between placements, and the token is reissued on every deploy. For local development, use the app's normal local database adapter behind a repository layer and keep deployed SQL compatible with SQLite.
114
115
 
115
116
  Regions are provider-neutral: `auto`, `us-east`, `us-west`, `europe`, `asia`, `south-america`, `middle-east`, `oceania`, `africa`.
116
117
 
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface DeployOptions {
7
7
  token: string;
8
8
  name?: string;
9
9
  slug?: string;
10
+ owner?: string;
10
11
  description?: string;
11
12
  environment?: string;
12
13
  withDatabase?: boolean;
@@ -50,6 +51,7 @@ export interface SiteAccessOptions extends ApiOptions {
50
51
  }
51
52
  export interface DeleteSiteOptions extends SiteAccessOptions {
52
53
  confirmSlug: string;
54
+ owner?: string;
53
55
  keepRecord?: boolean;
54
56
  }
55
57
  export interface AccessState {
@@ -114,7 +116,7 @@ export interface CreatedServiceToken {
114
116
  clientSecret: string;
115
117
  }
116
118
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
117
- export declare const CLI_VERSION = "0.55.0";
119
+ export declare const CLI_VERSION = "0.57.0";
118
120
  type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
119
121
  export interface StoredCredentialEntry {
120
122
  token: string;
@@ -321,7 +323,9 @@ export declare function checkSkillFreshness(options?: {
321
323
  export interface ListSitesResult {
322
324
  projects: Project[];
323
325
  }
324
- export declare function listSites(options: ApiOptions): Promise<ListSitesResult>;
326
+ export declare function listSites(options: ApiOptions & {
327
+ owner?: string;
328
+ }): Promise<ListSitesResult>;
325
329
  export declare function deleteSite(options: DeleteSiteOptions): Promise<DeletedSiteResult>;
326
330
  export interface DeviceAuthFeedbackOptions {
327
331
  progressStream?: NodeJS.WriteStream;
@@ -430,6 +434,7 @@ type CliCommand = {
430
434
  apiUrl: string;
431
435
  token: string;
432
436
  json: boolean;
437
+ owner?: string;
433
438
  } | {
434
439
  kind: "logs";
435
440
  site: string;
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { MAX_DEPLOY_DESCRIPTION_LENGTH, normalizeDeployDescription } from "./dep
14
14
  import { MAX_STATIC_ASSET_FILE_BYTES, MAX_WORKER_ENTRY_COMPRESSED_BYTES, MAX_WORKER_SCRIPT_COMPRESSED_BYTES, WORKER_ENTRY_COMPRESSED_WARN_BYTES, assetFileTooLargeMessage, formatMebibytes, formatMegabytes, oversizedAssets, workerBundleNearLimitMessage, workerBundleTooLargeMessage, workerEntryNearLimitMessage, workerEntryTooLargeMessage, } from "./deploy-limits.js";
15
15
  import { buildFileManifest, readManifestFile } from "./manifest.js";
16
16
  import { API_KEY_HELP, API_URL_HELP, CLI_CLIENT_ID, DEFAULT_API_URL, reportCliError } from "./cli-config.js";
17
- export const CLI_VERSION = "0.55.0";
17
+ export const CLI_VERSION = "0.57.0";
18
18
  const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
19
19
  const TOKEN_SERVICE = "9pm";
20
20
  const TOKEN_ACCOUNT = "default";
@@ -269,6 +269,7 @@ export async function deployDirectory(initialOptions) {
269
269
  ...(initialOptions.fetchFn ? { fetchFn: initialOptions.fetchFn } : {}),
270
270
  site: slugHolder.slug,
271
271
  confirmSlug: slugHolder.slug,
272
+ ...(initialOptions.owner ? { owner: initialOptions.owner } : {}),
272
273
  ...(slugHolder.retain ? { keepRecord: true } : {}),
273
274
  });
274
275
  }
@@ -339,8 +340,8 @@ async function runDeploy(initialOptions, slugHolder) {
339
340
  }
340
341
  const image = buildContainerImage(options.directory);
341
342
  try {
342
- const projectInput = { name: options.name ?? path.basename(path.resolve(options.directory)) };
343
- const { project, created: projectCreated } = await createProject(fetchFn, apiUrl, options.token, options.slug ? { ...projectInput, slug: options.slug } : projectInput);
343
+ const projectInput = { name: options.name ?? path.basename(path.resolve(options.directory)), ...(options.slug ? { slug: options.slug } : {}), ...(options.owner ? { owner: options.owner } : {}) };
344
+ const { project, created: projectCreated } = await createProject(fetchFn, apiUrl, options.token, projectInput);
344
345
  slugHolder.slug = project.slug;
345
346
  slugHolder.created = projectCreated;
346
347
  slugHolder.neverActive = project.activeDeploymentId === null;
@@ -504,8 +505,8 @@ async function runDeploy(initialOptions, slugHolder) {
504
505
  console.warn(`Warning: ${workerBundleNearLimitMessage(modulePaths.length, rawBytes, compressedBytes)}`);
505
506
  }
506
507
  }
507
- const projectInput = { name: options.name ?? path.basename(path.resolve(options.directory)) };
508
- const { project, created: projectCreated } = await createProject(fetchFn, apiUrl, options.token, options.slug ? { ...projectInput, slug: options.slug } : projectInput);
508
+ const projectInput = { name: options.name ?? path.basename(path.resolve(options.directory)), ...(options.slug ? { slug: options.slug } : {}), ...(options.owner ? { owner: options.owner } : {}) };
509
+ const { project, created: projectCreated } = await createProject(fetchFn, apiUrl, options.token, projectInput);
509
510
  slugHolder.slug = project.slug;
510
511
  slugHolder.created = projectCreated;
511
512
  const database = options.withDatabase
@@ -1199,11 +1200,13 @@ function readUpdateCheckCache(cachePath) {
1199
1200
  }
1200
1201
  }
1201
1202
  export async function listSites(options) {
1202
- const payload = await apiRequest(options, "/v1/sites", { method: "GET" });
1203
+ const route = options.owner ? `/v1/sites?owner=${encodeURIComponent(options.owner)}` : "/v1/sites";
1204
+ const payload = await apiRequest(options, route, { method: "GET" });
1203
1205
  return { projects: payload.projects };
1204
1206
  }
1205
1207
  export async function deleteSite(options) {
1206
- const payload = await apiRequest(options, `/v1/sites/${encodeURIComponent(options.site)}`, {
1208
+ const route = `/v1/sites/${encodeURIComponent(options.site)}${options.owner ? `?owner=${encodeURIComponent(options.owner)}` : ""}`;
1209
+ const payload = await apiRequest(options, route, {
1207
1210
  method: "DELETE",
1208
1211
  body: JSON.stringify({ confirmSlug: options.confirmSlug, ...(options.keepRecord ? { keepRecord: true } : {}) }),
1209
1212
  });
@@ -2154,6 +2157,7 @@ const VALID_DEPLOY_FLAGS = new Set([
2154
2157
  "email",
2155
2158
  "name",
2156
2159
  "slug",
2160
+ "owner",
2157
2161
  "description",
2158
2162
  "environment",
2159
2163
  "database-name",
@@ -2231,7 +2235,8 @@ export function parseArgs(argv) {
2231
2235
  }
2232
2236
  if (command === "apps" || command === "list" || command === "ls" || command === "sites" || command === "projects") {
2233
2237
  const flags = parseFlags(argv);
2234
- return { kind: "apps", ...readBaseApiOptions(flags), json: flags.has("json") };
2238
+ const owner = readStringFlag(flags, "owner");
2239
+ return { kind: "apps", ...readBaseApiOptions(flags), json: flags.has("json"), ...(owner ? { owner } : {}) };
2235
2240
  }
2236
2241
  if (command === "latest" || command === "upgrade" || command === "update") {
2237
2242
  const flags = parseFlags(argv);
@@ -2292,6 +2297,7 @@ export function parseArgs(argv) {
2292
2297
  };
2293
2298
  const name = readStringFlag(flags, "name");
2294
2299
  const slug = readStringFlag(flags, "slug");
2300
+ const owner = readStringFlag(flags, "owner");
2295
2301
  const description = readStringFlag(flags, "description");
2296
2302
  const environment = readStringFlag(flags, "environment");
2297
2303
  const databaseName = readStringFlag(flags, "database-name");
@@ -2307,6 +2313,9 @@ export function parseArgs(argv) {
2307
2313
  if (slug) {
2308
2314
  options.slug = slug;
2309
2315
  }
2316
+ if (owner) {
2317
+ options.owner = owner;
2318
+ }
2310
2319
  let normalizedDescription;
2311
2320
  try {
2312
2321
  normalizedDescription = normalizeDeployDescription(description);
@@ -3944,7 +3953,7 @@ async function main() {
3944
3953
  if (queued) {
3945
3954
  console.log(`Status: ${result.deployment.status}. Run 9pm apps to check when it becomes active.`);
3946
3955
  }
3947
- console.log(`Manage: ${PUBLIC_DASHBOARD_URL}`);
3956
+ console.log(`Manage: ${PUBLIC_DASHBOARD_URL}${command.options.owner ? `?owner=${encodeURIComponent(command.options.owner)}` : ""}`);
3948
3957
  // Gated the same way as the reachability probe above: a programmatic
3949
3958
  // caller injecting fetchFn opted out of the CLI's own network side trips.
3950
3959
  if (!command.options.fetchFn) {
@@ -4144,7 +4153,7 @@ async function main() {
4144
4153
  return;
4145
4154
  }
4146
4155
  if (command.kind === "apps") {
4147
- const result = await listSites({ apiUrl: command.apiUrl, token: command.token });
4156
+ const result = await listSites({ apiUrl: command.apiUrl, token: command.token, ...(command.owner ? { owner: command.owner } : {}) });
4148
4157
  if (command.json) {
4149
4158
  console.log(JSON.stringify(result, null, 2));
4150
4159
  return;
@@ -4395,7 +4404,7 @@ async function runFilesCommand(command) {
4395
4404
  function printUsage() {
4396
4405
  console.log(`Usage:
4397
4406
  9pm --version
4398
- 9pm deploy [directory] --name <site-name> [--slug <slug>] [--description "<note>"] [--with-db] [--with-auth] [--with-email]
4407
+ 9pm deploy [directory] --name <site-name> [--slug <slug>] [--owner <organisation>] [--description "<note>"] [--with-db] [--with-auth] [--with-email]
4399
4408
  9pm deploy [directory] --runtime worker --entry src/index.js [--with-db]
4400
4409
  9pm deploy [directory] --runtime container --port 8080 [--health-path /health] [--region auto] [--with-db] [--always-on] [--volume <gb> | --no-volume] [--cpus <n>] [--memory <size>]
4401
4410
  9pm deploy [directory] framework projects (Next.js, Vite) are detected and built automatically; --no-build skips
@@ -4407,7 +4416,7 @@ function printUsage() {
4407
4416
  9pm logout [--api-url <url>] [--local] revokes the credential server-side, then removes it from this machine (--local skips the revoke)
4408
4417
  9pm doctor [--json]
4409
4418
  9pm whoami [--json]
4410
- 9pm apps [--json]
4419
+ 9pm apps [--owner <organisation>] [--json]
4411
4420
  9pm latest [--json]
4412
4421
  9pm support -m "<what happened>" [--subject <subject>] [--site <app>]
4413
4422
  9pm files list [--project <site>]
@@ -4451,7 +4460,7 @@ Size limits:
4451
4460
  Container deploys:
4452
4461
  Requires Docker locally. The CLI builds linux/amd64, exports the image, uploads it through a short-lived session, then finalizes a queued deployment.
4453
4462
  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.
4454
- With --with-db, containers receive NINEPM_SQL_URL and NINEPM_DB_BINDINGS for the managed SQL bridge.
4463
+ 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.
4455
4464
  With --with-auth, the app gets 9pm-managed end-user accounts (email one-time codes); it implies --with-db and is one-way (enable-only).
4456
4465
  With --with-email, a signed-in end user can send app invitation emails; it implies --with-auth and works on public apps only.
4457
4466
  9pm env set <app> KEY=value [KEY2=value2 ...] [--var] (secret by default; --var = readable config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninepm",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "CLI for deploying built apps to 9pm.ai.",
5
5
  "homepage": "https://9pm.ai",
6
6
  "keywords": [