ninepm 0.52.0 → 0.55.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/cli-types.d.ts +7 -0
- package/dist/deploy-limits.js +8 -1
- package/dist/index.d.ts +27 -3
- package/dist/index.js +150 -19
- package/package.json +1 -1
package/dist/cli-types.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface Project {
|
|
|
16
16
|
createdAt: string;
|
|
17
17
|
updatedAt: string;
|
|
18
18
|
}
|
|
19
|
+
export interface AppUserFilesUsage {
|
|
20
|
+
usedBytes: number;
|
|
21
|
+
pendingBytes: number;
|
|
22
|
+
ceilingBytes: number;
|
|
23
|
+
ceilingIsDefault: boolean;
|
|
24
|
+
fileCount: number;
|
|
25
|
+
}
|
|
19
26
|
export interface FileManifestEntry {
|
|
20
27
|
path: string;
|
|
21
28
|
size: number;
|
package/dist/deploy-limits.js
CHANGED
|
@@ -33,7 +33,14 @@ export function formatKilobytes(bytes) {
|
|
|
33
33
|
return Number.isInteger(kb) ? `${kb} kB` : `${kb.toFixed(1)} kB`;
|
|
34
34
|
}
|
|
35
35
|
export function assetFileTooLargeMessage(filePath, size) {
|
|
36
|
-
|
|
36
|
+
// Carries the remedy as well as the ceiling, and only a remedy that holds for EVERY
|
|
37
|
+
// oversized file: this runs over the whole manifest, so the file may be the configured
|
|
38
|
+
// worker entry, where "leave it out" produces a missing-entry failure instead. Shrinking
|
|
39
|
+
// is the one option that is always available. Deliberately silent on HOW — that depends
|
|
40
|
+
// on the file, and "compress the image" reads as advice from something that has looked
|
|
41
|
+
// at it.
|
|
42
|
+
return (`File "${filePath}" is ${formatMebibytes(size)}; individual files are limited to ${formatMebibytes(MAX_STATIC_ASSET_FILE_BYTES)}. ` +
|
|
43
|
+
"Shrink the file before deploying again.");
|
|
37
44
|
}
|
|
38
45
|
// Manifest entries that exceed the per-file limit. A structural type keeps this
|
|
39
46
|
// module free of cross-tree type imports so it copies cleanly into the CLI.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Agent } from "undici";
|
|
3
|
-
import type { AccessGrant, AccessGrantType, AccessProviderKey, AppAccessSettings, AppAccessServiceToken, AppAccessToken, AppDatabase, AppRuntime, ContainerImageUploadInstruction, ContainerRegion, Deployment, ManagedFileObject, ManagedFileShare, ManagedFileUsage, Project } from "./cli-types.js";
|
|
3
|
+
import type { AccessGrant, AccessGrantType, AccessProviderKey, AppAccessSettings, AppAccessServiceToken, AppAccessToken, AppDatabase, AppRuntime, AppUserFilesUsage, ContainerImageUploadInstruction, ContainerRegion, Deployment, ManagedFileObject, ManagedFileShare, ManagedFileUsage, Project } from "./cli-types.js";
|
|
4
4
|
export interface DeployOptions {
|
|
5
5
|
directory: string;
|
|
6
6
|
apiUrl: string;
|
|
@@ -114,7 +114,7 @@ export interface CreatedServiceToken {
|
|
|
114
114
|
clientSecret: string;
|
|
115
115
|
}
|
|
116
116
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
117
|
-
export declare const CLI_VERSION = "0.
|
|
117
|
+
export declare const CLI_VERSION = "0.55.0";
|
|
118
118
|
type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
|
|
119
119
|
export interface StoredCredentialEntry {
|
|
120
120
|
token: string;
|
|
@@ -192,8 +192,24 @@ export declare function defaultNextWranglerConfig(appName: string): string;
|
|
|
192
192
|
export declare function isOpenNextInstalled(directory: string): boolean;
|
|
193
193
|
export declare class DeployError extends Error {
|
|
194
194
|
readonly slug: string | undefined;
|
|
195
|
-
|
|
195
|
+
readonly deliberate: boolean;
|
|
196
|
+
constructor(message: string, slug: string | undefined, deliberate?: boolean);
|
|
196
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* A refusal the platform MEANT — an operator paused deploys, a queue is full —
|
|
200
|
+
* as opposed to something going wrong.
|
|
201
|
+
*
|
|
202
|
+
* The distinction earns its keep in two places: the server-written message is
|
|
203
|
+
* the entire response and prints alone rather than inside the raw JSON envelope,
|
|
204
|
+
* and the "Stuck? Report it" prompt is suppressed. There is nothing to report
|
|
205
|
+
* about a pause somebody chose, and inviting a ticket for every blocked deploy
|
|
206
|
+
* generates precisely the support volume that announcing the pause was meant to
|
|
207
|
+
* prevent (#1061).
|
|
208
|
+
*/
|
|
209
|
+
export declare class DeliberateRefusalError extends Error {
|
|
210
|
+
constructor(message: string);
|
|
211
|
+
}
|
|
212
|
+
export declare function isDeliberateRefusal(error: unknown): boolean;
|
|
197
213
|
export declare function deployDirectory(initialOptions: DeployOptions): Promise<DeployResult>;
|
|
198
214
|
export type ApiReachability = "reachable" | "blocked";
|
|
199
215
|
export declare function probeApiReachability(apiUrl: string, fetchFn: typeof fetch, timeoutMs?: number): Promise<ApiReachability>;
|
|
@@ -335,6 +351,9 @@ export declare function signupDevice(options: {
|
|
|
335
351
|
}>;
|
|
336
352
|
export declare function expiredDeviceAuthMessage(purpose: string, userCode: string, lifetimeMs: number, verificationUri: string): string;
|
|
337
353
|
export declare function listManagedFiles(options: FileCommandOptions): Promise<ManagedFileListResult>;
|
|
354
|
+
export declare function getAppUserFilesUsage(options: ApiOptions & {
|
|
355
|
+
site: string;
|
|
356
|
+
}): Promise<AppUserFilesUsage>;
|
|
338
357
|
export declare function putManagedFile(options: FilePutOptions): Promise<ManagedFilePutResult>;
|
|
339
358
|
export declare function getManagedFile(options: FileGetOptions): Promise<{
|
|
340
359
|
fileId: string;
|
|
@@ -434,6 +453,10 @@ type CliCommand = {
|
|
|
434
453
|
action: {
|
|
435
454
|
kind: "list";
|
|
436
455
|
project?: string;
|
|
456
|
+
} | {
|
|
457
|
+
kind: "usage";
|
|
458
|
+
project?: string;
|
|
459
|
+
json: boolean;
|
|
437
460
|
} | {
|
|
438
461
|
kind: "put";
|
|
439
462
|
localPath: string;
|
|
@@ -573,6 +596,7 @@ export declare function classifyBrowserOpen(input: {
|
|
|
573
596
|
error?: unknown;
|
|
574
597
|
}): BrowserOpenResult;
|
|
575
598
|
export declare function hasNoGraphicalDisplay(platform: string, env: Record<string, string | undefined>): boolean;
|
|
599
|
+
export declare function formatAppUserFilesUsageLine(slug: string, usage: AppUserFilesUsage): string;
|
|
576
600
|
export declare class UsageError extends Error {
|
|
577
601
|
}
|
|
578
602
|
export {};
|
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.
|
|
17
|
+
export const CLI_VERSION = "0.55.0";
|
|
18
18
|
const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
|
|
19
19
|
const TOKEN_SERVICE = "9pm";
|
|
20
20
|
const TOKEN_ACCOUNT = "default";
|
|
@@ -184,12 +184,59 @@ const DOCKERFILE_STATIC_HINT = "Found a Dockerfile but no runtime was selected,
|
|
|
184
184
|
// slug (server-normalized to "my-app"), so the raw name would not resolve.
|
|
185
185
|
export class DeployError extends Error {
|
|
186
186
|
slug;
|
|
187
|
-
|
|
187
|
+
// Whether the refusal underneath was a decision rather than a fault. Carried
|
|
188
|
+
// here because deployDirectory re-wraps everything it catches into a
|
|
189
|
+
// DeployError, which would otherwise erase the distinction before the command
|
|
190
|
+
// that needs it ever sees the error.
|
|
191
|
+
deliberate;
|
|
192
|
+
constructor(message, slug, deliberate = false) {
|
|
188
193
|
super(message);
|
|
189
194
|
this.name = "DeployError";
|
|
190
195
|
this.slug = slug;
|
|
196
|
+
this.deliberate = deliberate;
|
|
191
197
|
}
|
|
192
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* A refusal the platform MEANT — an operator paused deploys, a queue is full —
|
|
201
|
+
* as opposed to something going wrong.
|
|
202
|
+
*
|
|
203
|
+
* The distinction earns its keep in two places: the server-written message is
|
|
204
|
+
* the entire response and prints alone rather than inside the raw JSON envelope,
|
|
205
|
+
* and the "Stuck? Report it" prompt is suppressed. There is nothing to report
|
|
206
|
+
* about a pause somebody chose, and inviting a ticket for every blocked deploy
|
|
207
|
+
* generates precisely the support volume that announcing the pause was meant to
|
|
208
|
+
* prevent (#1061).
|
|
209
|
+
*/
|
|
210
|
+
export class DeliberateRefusalError extends Error {
|
|
211
|
+
constructor(message) {
|
|
212
|
+
super(message);
|
|
213
|
+
this.name = "DeliberateRefusalError";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Error codes whose message is written for the person reading it, by someone who
|
|
218
|
+
* decided to refuse them.
|
|
219
|
+
*
|
|
220
|
+
* Deliberately an allowlist of CODES rather than a rule about status. 503 is also
|
|
221
|
+
* what a genuinely broken dependency returns, and 426 is a general
|
|
222
|
+
* protocol-upgrade status an intermediary can raise on its own — treating either
|
|
223
|
+
* as intentional on the strength of the number would hide a real fault behind a
|
|
224
|
+
* calm sentence, and drop the support prompt exactly when it is wanted. A code
|
|
225
|
+
* has to be added here on purpose.
|
|
226
|
+
*
|
|
227
|
+
* `cli_version_outdated` is the control plane's enforced CLI floor (#996). It had
|
|
228
|
+
* its own status-only branch here until #1061, which is the shape this comment
|
|
229
|
+
* warns against: any 426, from anywhere, was treated as the floor.
|
|
230
|
+
*/
|
|
231
|
+
const DELIBERATE_REFUSAL_CODES = new Set([
|
|
232
|
+
"deploys_frozen",
|
|
233
|
+
"cli_version_outdated",
|
|
234
|
+
]);
|
|
235
|
+
export function isDeliberateRefusal(error) {
|
|
236
|
+
if (error instanceof DeliberateRefusalError)
|
|
237
|
+
return true;
|
|
238
|
+
return error instanceof DeployError && error.deliberate;
|
|
239
|
+
}
|
|
193
240
|
// Shell-quote a value for a copy-pasteable command. Clean slugs pass through
|
|
194
241
|
// unquoted; anything with shell-significant characters is single-quoted.
|
|
195
242
|
function quoteSiteArg(value) {
|
|
@@ -231,7 +278,7 @@ export async function deployDirectory(initialOptions) {
|
|
|
231
278
|
}
|
|
232
279
|
if (error instanceof DeployError)
|
|
233
280
|
throw error;
|
|
234
|
-
throw new DeployError(error instanceof Error ? error.message : String(error), slugHolder.slug);
|
|
281
|
+
throw new DeployError(error instanceof Error ? error.message : String(error), slugHolder.slug, isDeliberateRefusal(error));
|
|
235
282
|
}
|
|
236
283
|
}
|
|
237
284
|
// Cheap unauthenticated probe of the control-plane API before a deploy. The
|
|
@@ -1403,6 +1450,11 @@ async function apiRequest(options, route, init) {
|
|
|
1403
1450
|
export async function listManagedFiles(options) {
|
|
1404
1451
|
return apiRequest(options, filesRoute(options), { method: "GET" });
|
|
1405
1452
|
}
|
|
1453
|
+
// Per-app user-file usage + ceiling (files an app's signed-in end users uploaded through
|
|
1454
|
+
// ctx.files) — a different endpoint from the owner's own managed-file library above.
|
|
1455
|
+
export async function getAppUserFilesUsage(options) {
|
|
1456
|
+
return apiRequest(options, `/v1/sites/${encodeURIComponent(options.site)}/files/usage`, { method: "GET" });
|
|
1457
|
+
}
|
|
1406
1458
|
export async function putManagedFile(options) {
|
|
1407
1459
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1408
1460
|
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
@@ -1954,15 +2006,32 @@ async function expectOk(response, action) {
|
|
|
1954
2006
|
if (response.ok) {
|
|
1955
2007
|
return;
|
|
1956
2008
|
}
|
|
1957
|
-
// 426 is the control plane's enforced CLI floor (#996): the server-written
|
|
1958
|
-
// message IS the response — surface it alone rather than burying it in the
|
|
1959
|
-
// raw envelope the generic path prints.
|
|
1960
|
-
if (response.status === 426) {
|
|
1961
|
-
throw new Error(await apiErrorMessage(response, action));
|
|
1962
|
-
}
|
|
1963
2009
|
const text = await response.text();
|
|
2010
|
+
// A refusal somebody chose: print what they wrote, not the envelope it came
|
|
2011
|
+
// in. Both the code and the message have to be there — an allowlisted code
|
|
2012
|
+
// with no usable message falls through, so an empty reason can never turn
|
|
2013
|
+
// into an empty error. The raw form still applies to everything else, so no
|
|
2014
|
+
// detail is lost on the failures that genuinely are faults.
|
|
2015
|
+
const envelope = parseErrorEnvelope(text);
|
|
2016
|
+
const code = typeof envelope?.code === "string" ? envelope.code : "";
|
|
2017
|
+
const message = typeof envelope?.message === "string" ? envelope.message.trim() : "";
|
|
2018
|
+
if (code && message && DELIBERATE_REFUSAL_CODES.has(code)) {
|
|
2019
|
+
throw new DeliberateRefusalError(message);
|
|
2020
|
+
}
|
|
1964
2021
|
throw new Error(`Failed to ${action}: ${response.status} ${text}`);
|
|
1965
2022
|
}
|
|
2023
|
+
// Takes the already-consumed body text rather than the Response: a body can only
|
|
2024
|
+
// be read once, and expectOk needs the code and the message from the same read.
|
|
2025
|
+
function parseErrorEnvelope(text) {
|
|
2026
|
+
try {
|
|
2027
|
+
const parsed = JSON.parse(text);
|
|
2028
|
+
return parsed.error ?? null;
|
|
2029
|
+
}
|
|
2030
|
+
catch {
|
|
2031
|
+
// Not JSON (proxy error page, empty body).
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1966
2035
|
// Advisory half of the control plane's CLI floor (#996): when a response says
|
|
1967
2036
|
// this version is below the platform's recommended minimum, say so once per
|
|
1968
2037
|
// invocation, on stderr so command output (including --json) stays parseable.
|
|
@@ -1991,15 +2060,9 @@ export function resetCliOutdatedAdvisoryForTests() {
|
|
|
1991
2060
|
// expectOk's text when the body isn't that shape, so this can never lose detail.
|
|
1992
2061
|
async function apiErrorMessage(response, action) {
|
|
1993
2062
|
const text = await response.text();
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
if (typeof message === "string" && message.trim()) {
|
|
1998
|
-
return message.trim();
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
catch {
|
|
2002
|
-
// Not JSON (proxy error page, empty body) — fall through to the raw form.
|
|
2063
|
+
const message = parseErrorEnvelope(text)?.message;
|
|
2064
|
+
if (typeof message === "string" && message.trim()) {
|
|
2065
|
+
return message.trim();
|
|
2003
2066
|
}
|
|
2004
2067
|
return `Failed to ${action}: ${response.status} ${text}`;
|
|
2005
2068
|
}
|
|
@@ -2368,6 +2431,15 @@ function parseFilesArgs(argv) {
|
|
|
2368
2431
|
if (subcommand === "list") {
|
|
2369
2432
|
return { kind: "files", ...base, action: { kind: "list", ...withProject } };
|
|
2370
2433
|
}
|
|
2434
|
+
if (subcommand === "usage") {
|
|
2435
|
+
// Per-app user-file usage. --project scopes to one app; omitting it entirely is the
|
|
2436
|
+
// account-wide overview. But a present-but-empty --project/--site is a typo, not the
|
|
2437
|
+
// overview: reject it rather than silently fanning out over every app.
|
|
2438
|
+
if ((flags.has("project") || flags.has("site")) && !project) {
|
|
2439
|
+
throw new Error("--project needs an app slug, e.g. `9pm files usage --project my-app`.");
|
|
2440
|
+
}
|
|
2441
|
+
return { kind: "files", ...base, action: { kind: "usage", ...withProject, json: flags.has("json") } };
|
|
2442
|
+
}
|
|
2371
2443
|
if (subcommand === "put") {
|
|
2372
2444
|
const localPath = argv[1];
|
|
2373
2445
|
if (!localPath || localPath.startsWith("--")) {
|
|
@@ -3773,6 +3845,19 @@ function formatBytesForCli(value) {
|
|
|
3773
3845
|
}
|
|
3774
3846
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
|
|
3775
3847
|
}
|
|
3848
|
+
// One `9pm files usage` row: "<slug>\t<used> / <ceiling> (<pct>%)\t<n> files", mirroring
|
|
3849
|
+
// the `used of total (percent%)` disk-usage style.
|
|
3850
|
+
export function formatAppUserFilesUsageLine(slug, usage) {
|
|
3851
|
+
const ceiling = usage.ceilingIsDefault ? `${formatBytesForCli(usage.ceilingBytes)} (default)` : formatBytesForCli(usage.ceilingBytes);
|
|
3852
|
+
// Used-over-ceiling has no meaningful percent — a zero-byte ceiling with files, or a
|
|
3853
|
+
// ceiling lowered below current usage — so say "over limit" instead of a misleading 0%.
|
|
3854
|
+
const status = usage.usedBytes > usage.ceilingBytes
|
|
3855
|
+
? "over limit"
|
|
3856
|
+
: usage.ceilingBytes > 0
|
|
3857
|
+
? `${Math.round((usage.usedBytes / usage.ceilingBytes) * 100)}%`
|
|
3858
|
+
: "0%";
|
|
3859
|
+
return `${slug}\t${formatBytesForCli(usage.usedBytes)} / ${ceiling} (${status})\t${usage.fileCount} files`;
|
|
3860
|
+
}
|
|
3776
3861
|
// Human remaining time for the device-auth wait (#509). Ceil so a window with
|
|
3777
3862
|
// any time left never reads "0s", and clamp so a passed deadline reads "0s"
|
|
3778
3863
|
// rather than a negative.
|
|
@@ -3840,9 +3925,16 @@ async function main() {
|
|
|
3840
3925
|
// exists) so `9pm support --site <slug>` resolves and /v1/me/support can
|
|
3841
3926
|
// attach the failed deploy's id/status/error. Bare form when no slug is
|
|
3842
3927
|
// known yet (failure before the project was created).
|
|
3928
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
3929
|
+
// Nothing to report: the platform refused on purpose and said why.
|
|
3930
|
+
// Pointing the reader at support here would send them to open a ticket
|
|
3931
|
+
// about a decision we made, which is the opposite of what announcing it
|
|
3932
|
+
// was for (#1061).
|
|
3933
|
+
if (isDeliberateRefusal(error)) {
|
|
3934
|
+
throw new Error(base);
|
|
3935
|
+
}
|
|
3843
3936
|
const slug = error instanceof DeployError ? error.slug : command.options.slug;
|
|
3844
3937
|
const siteFlag = slug ? ` --site ${quoteSiteArg(slug)}` : "";
|
|
3845
|
-
const base = error instanceof Error ? error.message : String(error);
|
|
3846
3938
|
throw new Error(`${base}\n\nStuck? Report it with context: 9pm support${siteFlag} -m "<what happened>"`);
|
|
3847
3939
|
}
|
|
3848
3940
|
const databaseLine = result.database ? `database ${result.database.bindingName}:${result.database.name}` : "no database";
|
|
@@ -4220,6 +4312,44 @@ async function runFilesCommand(command) {
|
|
|
4220
4312
|
console.log(`Usage: ${formatBytesForCli(listed.usage.usedBytes)} / ${formatBytesForCli(listed.usage.quotaBytes)}`);
|
|
4221
4313
|
return;
|
|
4222
4314
|
}
|
|
4315
|
+
if (command.action.kind === "usage") {
|
|
4316
|
+
const action = command.action;
|
|
4317
|
+
if (action.project) {
|
|
4318
|
+
const usage = await getAppUserFilesUsage({ ...base, site: action.project });
|
|
4319
|
+
if (action.json) {
|
|
4320
|
+
console.log(JSON.stringify({ project: action.project, ...usage }, null, 2));
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
console.log(formatAppUserFilesUsageLine(action.project, usage));
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
// No --project: usage across every app the account owns. A dedicated overview, so the
|
|
4327
|
+
// per-app fan-out (one usage call each, in parallel) lives here rather than slowing the
|
|
4328
|
+
// plain `9pm apps` inventory.
|
|
4329
|
+
const { projects } = await listSites(base);
|
|
4330
|
+
// Isolate per-app failures: one app erroring (a transient 5xx, or a project deleted
|
|
4331
|
+
// between listSites and here) must not blank the rest of the table.
|
|
4332
|
+
const rows = await Promise.all(projects.map(async (p) => {
|
|
4333
|
+
try {
|
|
4334
|
+
return { slug: p.slug, usage: await getAppUserFilesUsage({ ...base, site: p.id }) };
|
|
4335
|
+
}
|
|
4336
|
+
catch {
|
|
4337
|
+
return { slug: p.slug, usage: null };
|
|
4338
|
+
}
|
|
4339
|
+
}));
|
|
4340
|
+
if (action.json) {
|
|
4341
|
+
console.log(JSON.stringify(rows.map((r) => (r.usage ? { project: r.slug, ...r.usage } : { project: r.slug, unavailable: true })), null, 2));
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
if (rows.length === 0) {
|
|
4345
|
+
console.log("No apps. Run `9pm deploy` to create one.");
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
for (const row of rows) {
|
|
4349
|
+
console.log(row.usage ? formatAppUserFilesUsageLine(row.slug, row.usage) : `${row.slug}\t(usage unavailable)`);
|
|
4350
|
+
}
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4223
4353
|
if (command.action.kind === "put") {
|
|
4224
4354
|
const result = await putManagedFile({ ...base, ...project, localPath: command.action.localPath, ...(command.action.remotePath ? { remotePath: command.action.remotePath } : {}) });
|
|
4225
4355
|
console.log(`Stored ${result.file.path} (${formatBytesForCli(result.file.size)}).`);
|
|
@@ -4281,6 +4411,7 @@ function printUsage() {
|
|
|
4281
4411
|
9pm latest [--json]
|
|
4282
4412
|
9pm support -m "<what happened>" [--subject <subject>] [--site <app>]
|
|
4283
4413
|
9pm files list [--project <site>]
|
|
4414
|
+
9pm files usage [--project <site>] [--json]
|
|
4284
4415
|
9pm files put <local-path> [--path <remote-path>] [--project <site>]
|
|
4285
4416
|
9pm files get <file-id> [--output <path>] [--project <site>]
|
|
4286
4417
|
9pm files share <file-id> [--expires 24h] [--public] [--project <site>]
|