ninepm 0.32.0 → 0.34.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/dist/index.d.ts +5 -1
- package/dist/index.js +73 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface DeployOptions {
|
|
|
11
11
|
environment?: string;
|
|
12
12
|
withDatabase?: boolean;
|
|
13
13
|
withAuth?: boolean;
|
|
14
|
+
withEmail?: boolean;
|
|
14
15
|
databaseName?: string;
|
|
15
16
|
bindingName?: string;
|
|
16
17
|
runtime?: AppRuntime;
|
|
@@ -112,7 +113,7 @@ export interface CreatedServiceToken {
|
|
|
112
113
|
clientSecret: string;
|
|
113
114
|
}
|
|
114
115
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
115
|
-
export declare const CLI_VERSION = "0.
|
|
116
|
+
export declare const CLI_VERSION = "0.34.0";
|
|
116
117
|
type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
|
|
117
118
|
export interface CredentialDiagnostics {
|
|
118
119
|
platform: NodeJS.Platform;
|
|
@@ -183,6 +184,9 @@ export declare class DeployError extends Error {
|
|
|
183
184
|
constructor(message: string, slug: string | undefined);
|
|
184
185
|
}
|
|
185
186
|
export declare function deployDirectory(initialOptions: DeployOptions): Promise<DeployResult>;
|
|
187
|
+
export type ApiReachability = "reachable" | "blocked";
|
|
188
|
+
export declare function probeApiReachability(apiUrl: string, fetchFn: typeof fetch, timeoutMs?: number): Promise<ApiReachability>;
|
|
189
|
+
export declare function apiUnreachableGuidance(apiUrl: string): string;
|
|
186
190
|
export declare function checkDeployDirectory(options: DeployOptions): Promise<DeployCheckResult>;
|
|
187
191
|
export declare function whoami(options: ApiOptions): Promise<WhoamiResult>;
|
|
188
192
|
export declare function publicWhoamiResult(result: WhoamiResult): PublicWhoamiResult;
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { MAX_DEPLOY_DESCRIPTION_LENGTH, normalizeDeployDescription } from "./dep
|
|
|
12
12
|
import { assetFileTooLargeMessage, oversizedAssets } from "./deploy-limits.js";
|
|
13
13
|
import { buildFileManifest, readManifestFile } from "./manifest.js";
|
|
14
14
|
import { API_KEY_HELP, API_URL_HELP, CLI_CLIENT_ID, DEFAULT_API_URL, reportCliError } from "./cli-config.js";
|
|
15
|
-
export const CLI_VERSION = "0.
|
|
15
|
+
export const CLI_VERSION = "0.34.0";
|
|
16
16
|
const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
|
|
17
17
|
const TOKEN_SERVICE = "9pm";
|
|
18
18
|
const TOKEN_ACCOUNT = "default";
|
|
@@ -229,6 +229,44 @@ export async function deployDirectory(initialOptions) {
|
|
|
229
229
|
throw new DeployError(error instanceof Error ? error.message : String(error), slugHolder.slug);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
+
// Cheap unauthenticated probe of the control-plane API before a deploy. The
|
|
233
|
+
// sandbox egress proxy blocks at CONNECT, so ANY HTTP response (even a 5xx)
|
|
234
|
+
// proves the host is reachable — only a thrown/aborted request signals a block.
|
|
235
|
+
// A tight timeout keeps the reachable path fast and the blocked path from
|
|
236
|
+
// hanging.
|
|
237
|
+
export async function probeApiReachability(apiUrl, fetchFn,
|
|
238
|
+
// Comfortably above /health tail latency so a slow-but-reachable link isn't
|
|
239
|
+
// aborted and misread as blocked; the reachable path returns well under this.
|
|
240
|
+
timeoutMs = 5000) {
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
243
|
+
try {
|
|
244
|
+
await fetchFn(`${apiUrl}/health`, { method: "GET", signal: controller.signal });
|
|
245
|
+
return "reachable";
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return "blocked";
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Actionable message when the API host can't be reached — the two fixes for a
|
|
255
|
+
// default ("Trusted") sandbox, matching the #258 byte-channel design.
|
|
256
|
+
export function apiUnreachableGuidance(apiUrl) {
|
|
257
|
+
let host = apiUrl;
|
|
258
|
+
try {
|
|
259
|
+
host = new URL(apiUrl).host;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// keep the raw value if it isn't a full URL
|
|
263
|
+
}
|
|
264
|
+
return (`Can't reach ${host}. Most often a default ("Trusted") cloud sandbox is blocking the host, ` +
|
|
265
|
+
`but it can also be a transient network issue or a ${host} outage (check status if the fixes below don't help). ` +
|
|
266
|
+
`If it's the sandbox, fix it either way:\n` +
|
|
267
|
+
` 1. Add ${host} to your environment's Allowed domains (switch network access to Custom), then re-run.\n` +
|
|
268
|
+
` 2. Or deploy through the 9pm MCP connector — your agent can enable it and deploy with the 9pm tools, no network change needed.`);
|
|
269
|
+
}
|
|
232
270
|
async function runDeploy(initialOptions, slugHolder) {
|
|
233
271
|
const options = prepareFrameworkDeploy(initialOptions);
|
|
234
272
|
// #374: enrich network-level fetch rejections (EPIPE, header timeouts, …)
|
|
@@ -259,6 +297,9 @@ async function runDeploy(initialOptions, slugHolder) {
|
|
|
259
297
|
if (options.withAuth) {
|
|
260
298
|
await enableEndUserAuth(fetchFn, apiUrl, options.token, project.id);
|
|
261
299
|
}
|
|
300
|
+
if (options.withEmail) {
|
|
301
|
+
await enableAppEmail(fetchFn, apiUrl, options.token, project.id);
|
|
302
|
+
}
|
|
262
303
|
const created = await createDeployment(fetchFn, apiUrl, options.token, project.id, {
|
|
263
304
|
files: [],
|
|
264
305
|
environment: options.environment ?? "production",
|
|
@@ -385,6 +426,9 @@ async function runDeploy(initialOptions, slugHolder) {
|
|
|
385
426
|
if (options.withAuth) {
|
|
386
427
|
await enableEndUserAuth(fetchFn, apiUrl, options.token, project.id);
|
|
387
428
|
}
|
|
429
|
+
if (options.withEmail) {
|
|
430
|
+
await enableAppEmail(fetchFn, apiUrl, options.token, project.id);
|
|
431
|
+
}
|
|
388
432
|
const created = await createDeployment(fetchFn, apiUrl, options.token, project.id, {
|
|
389
433
|
files: manifest,
|
|
390
434
|
environment: options.environment ?? "production",
|
|
@@ -835,6 +879,14 @@ async function enableEndUserAuth(fetchFn, apiUrl, token, projectId) {
|
|
|
835
879
|
});
|
|
836
880
|
await expectJson(response, "enable end-user auth");
|
|
837
881
|
}
|
|
882
|
+
async function enableAppEmail(fetchFn, apiUrl, token, projectId) {
|
|
883
|
+
const response = await fetchFn(`${apiUrl}/v1/sites/${encodeURIComponent(projectId)}/email/enable`, {
|
|
884
|
+
method: "POST",
|
|
885
|
+
headers: jsonHeaders(token),
|
|
886
|
+
body: "{}",
|
|
887
|
+
});
|
|
888
|
+
await expectJson(response, "enable app email");
|
|
889
|
+
}
|
|
838
890
|
async function createDeployment(fetchFn, apiUrl, token, projectId, body) {
|
|
839
891
|
const response = await fetchFn(`${apiUrl}/v1/sites/${encodeURIComponent(projectId)}/deployments`, {
|
|
840
892
|
method: "POST",
|
|
@@ -1323,6 +1375,8 @@ const VALID_DEPLOY_FLAGS = new Set([
|
|
|
1323
1375
|
"db",
|
|
1324
1376
|
"with-auth",
|
|
1325
1377
|
"auth",
|
|
1378
|
+
"with-email",
|
|
1379
|
+
"email",
|
|
1326
1380
|
"name",
|
|
1327
1381
|
"slug",
|
|
1328
1382
|
"description",
|
|
@@ -1442,9 +1496,12 @@ export function parseArgs(argv) {
|
|
|
1442
1496
|
directory,
|
|
1443
1497
|
apiUrl: readApiUrl(flags, DEFAULT_API_URL),
|
|
1444
1498
|
token: token ?? "",
|
|
1445
|
-
// --with-auth implies a managed database (somewhere to store per-user data)
|
|
1446
|
-
|
|
1447
|
-
|
|
1499
|
+
// --with-auth implies a managed database (somewhere to store per-user data);
|
|
1500
|
+
// --with-email implies --with-auth (invitations need managed accounts), so it
|
|
1501
|
+
// pulls in the database too.
|
|
1502
|
+
withDatabase: flags.has("with-db") || flags.has("db") || flags.has("with-auth") || flags.has("auth") || flags.has("with-email") || flags.has("email"),
|
|
1503
|
+
withAuth: flags.has("with-auth") || flags.has("auth") || flags.has("with-email") || flags.has("email"),
|
|
1504
|
+
withEmail: flags.has("with-email") || flags.has("email"),
|
|
1448
1505
|
check,
|
|
1449
1506
|
};
|
|
1450
1507
|
const name = readStringFlag(flags, "name");
|
|
@@ -2549,6 +2606,16 @@ async function main() {
|
|
|
2549
2606
|
}
|
|
2550
2607
|
return;
|
|
2551
2608
|
}
|
|
2609
|
+
// Fail fast with actionable guidance when the API host is unreachable — a
|
|
2610
|
+
// default ("Trusted") cloud sandbox blocks it at CONNECT, and the deploy
|
|
2611
|
+
// would otherwise hang/error cryptically on the first request (#83/#258).
|
|
2612
|
+
// Real invocations only; a programmatic caller injecting fetchFn opts out.
|
|
2613
|
+
if (!command.options.fetchFn &&
|
|
2614
|
+
(await probeApiReachability(normalizeApiUrl(command.options.apiUrl), fetch)) === "blocked") {
|
|
2615
|
+
console.error(apiUnreachableGuidance(command.options.apiUrl));
|
|
2616
|
+
process.exitCode = 1;
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2552
2619
|
let result;
|
|
2553
2620
|
try {
|
|
2554
2621
|
result = await deployDirectory(command.options);
|
|
@@ -2829,7 +2896,7 @@ async function runFilesCommand(command) {
|
|
|
2829
2896
|
function printUsage() {
|
|
2830
2897
|
console.log(`Usage:
|
|
2831
2898
|
9pm --version
|
|
2832
|
-
9pm deploy [directory] --name <site-name> [--slug <slug>] [--description "<note>"] [--with-db] [--with-auth]
|
|
2899
|
+
9pm deploy [directory] --name <site-name> [--slug <slug>] [--description "<note>"] [--with-db] [--with-auth] [--with-email]
|
|
2833
2900
|
9pm deploy [directory] --runtime worker --entry src/index.js [--with-db]
|
|
2834
2901
|
9pm deploy [directory] --runtime container --port 8080 [--health-path /health] [--region auto] [--with-db] [--always-on] [--volume <gb> | --no-volume] [--cpus <n>] [--memory <size>]
|
|
2835
2902
|
9pm deploy [directory] framework projects (Next.js, Vite) are detected and built automatically; --no-build skips
|
|
@@ -2877,6 +2944,7 @@ Container deploys:
|
|
|
2877
2944
|
The app must listen on 0.0.0.0:$PORT or the selected --port. Use --health-path for a cheap readiness endpoint.
|
|
2878
2945
|
With --with-db, containers receive NINEPM_SQL_URL and NINEPM_DB_BINDINGS for the managed SQL bridge.
|
|
2879
2946
|
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).
|
|
2947
|
+
With --with-email, a signed-in end user can send app invitation emails; it implies --with-auth and works on public apps only.
|
|
2880
2948
|
9pm env set <app> KEY=value [KEY2=value2 ...] [--var] (secret by default; --var = readable config)
|
|
2881
2949
|
9pm env set <app> --from-file <path> [--var] (KEY=value lines; # comments, export prefix, quotes ok)
|
|
2882
2950
|
9pm env set <app> <NAME> --stdin [--var] (value read verbatim from stdin)
|