@pitcher/cli-scripts 2.2.2 → 2.4.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 +203 -0
- package/dist/index.js +26 -2
- package/dist/src/commands/analytics.d.ts +12 -0
- package/dist/src/commands/analytics.js +45 -0
- package/dist/src/commands/announcements.d.ts +23 -0
- package/dist/src/commands/announcements.js +131 -0
- package/dist/src/commands/canvases.d.ts +25 -0
- package/dist/src/commands/canvases.js +73 -0
- package/dist/src/commands/dsrLanding.d.ts +28 -0
- package/dist/src/commands/dsrLanding.js +150 -0
- package/dist/src/commands/emailTemplate.d.ts +32 -0
- package/dist/src/commands/emailTemplate.js +124 -0
- package/dist/src/commands/files.d.ts +46 -0
- package/dist/src/commands/files.js +167 -0
- package/dist/src/commands/folders.d.ts +33 -0
- package/dist/src/commands/folders.js +120 -0
- package/dist/src/commands/instances.d.ts +21 -0
- package/dist/src/commands/instances.js +58 -0
- package/dist/src/commands/org.d.ts +9 -0
- package/dist/src/commands/org.js +26 -0
- package/dist/src/commands/scaffolds.d.ts +19 -0
- package/dist/src/commands/scaffolds.js +176 -0
- package/dist/src/commands/search.d.ts +13 -0
- package/dist/src/commands/search.js +35 -0
- package/dist/src/commands/sharedLinks.d.ts +16 -0
- package/dist/src/commands/sharedLinks.js +61 -0
- package/dist/src/commands/tags.d.ts +20 -0
- package/dist/src/commands/tags.js +52 -0
- package/dist/src/commands/withAuth.d.ts +3 -0
- package/dist/src/commands/withAuth.js +7 -0
- package/dist/src/commands/withInstance.d.ts +3 -0
- package/dist/src/commands/withInstance.js +4 -0
- package/dist/src/core/baseUrl.js +2 -2
- package/dist/src/core/http.d.ts +10 -0
- package/dist/src/core/http.js +6 -0
- package/dist/src/core/pagination.d.ts +30 -0
- package/dist/src/core/pagination.js +47 -0
- package/dist/src/core/readInput.d.ts +2 -0
- package/dist/src/core/readInput.js +23 -0
- package/dist/src/core/upload.d.ts +15 -0
- package/dist/src/core/upload.js +30 -0
- package/dist/src/schema/content.d.ts +12 -0
- package/dist/src/schema/content.js +63 -0
- package/package.json +3 -3
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Attach the shared auth flags (--api-key/--org/--url) to a (sub)command. */
|
|
2
|
+
export function withAuthFlags(c) {
|
|
3
|
+
return c
|
|
4
|
+
.option('--api-key <apiKey>', 'API Key for authentication')
|
|
5
|
+
.option('--org <organization>', 'Organization subdomain', 'dev')
|
|
6
|
+
.option('--url <url>', 'Explicit base URL override');
|
|
7
|
+
}
|
package/dist/src/core/baseUrl.js
CHANGED
|
@@ -6,10 +6,10 @@ export function resolveBaseUrl(opts) {
|
|
|
6
6
|
parsed = new URL(opts.url);
|
|
7
7
|
}
|
|
8
8
|
catch {
|
|
9
|
-
throw createCliError('VALIDATION', `Invalid
|
|
9
|
+
throw createCliError('VALIDATION', `Invalid base URL: ${opts.url}`);
|
|
10
10
|
}
|
|
11
11
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
12
|
-
throw createCliError('VALIDATION',
|
|
12
|
+
throw createCliError('VALIDATION', `Base URL must be an http(s) URL: ${opts.url}`);
|
|
13
13
|
}
|
|
14
14
|
return parsed.origin;
|
|
15
15
|
}
|
package/dist/src/core/http.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { type AxiosInstance } from 'axios';
|
|
2
2
|
import { type CliError } from './errors.js';
|
|
3
3
|
export declare function makeClient(baseUrl: string, apiKey: string): AxiosInstance;
|
|
4
|
+
export interface CommonOpts {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
org?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Resolve auth + base URL into a client, or use an injected one (tests). */
|
|
11
|
+
export declare function clientFor(opts: CommonOpts, deps?: {
|
|
12
|
+
client?: AxiosInstance;
|
|
13
|
+
}): AxiosInstance;
|
|
4
14
|
/**
|
|
5
15
|
* Build a human-readable message from a server error body. Handles DRF shapes:
|
|
6
16
|
* a `detail`/`error` string, `non_field_errors`, and field-level error arrays
|
package/dist/src/core/http.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { resolveApiKey } from './auth.js';
|
|
3
|
+
import { resolveBaseUrl } from './baseUrl.js';
|
|
2
4
|
import { createCliError, isCliError } from './errors.js';
|
|
3
5
|
export function makeClient(baseUrl, apiKey) {
|
|
4
6
|
return axios.create({
|
|
@@ -9,6 +11,10 @@ export function makeClient(baseUrl, apiKey) {
|
|
|
9
11
|
},
|
|
10
12
|
});
|
|
11
13
|
}
|
|
14
|
+
/** Resolve auth + base URL into a client, or use an injected one (tests). */
|
|
15
|
+
export function clientFor(opts, deps = {}) {
|
|
16
|
+
return deps.client ?? makeClient(resolveBaseUrl({ org: opts.org, url: opts.url }), resolveApiKey(opts));
|
|
17
|
+
}
|
|
12
18
|
/**
|
|
13
19
|
* Build a human-readable message from a server error body. Handles DRF shapes:
|
|
14
20
|
* a `detail`/`error` string, `non_field_errors`, and field-level error arrays
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const MAX_PAGE_SIZE = 500;
|
|
2
|
+
export interface PageOpts {
|
|
3
|
+
page?: number;
|
|
4
|
+
pageSize?: number;
|
|
5
|
+
}
|
|
6
|
+
/** Page query params to merge into a list request (clamped so sent === echoed). */
|
|
7
|
+
export declare function listPageParams(opts: PageOpts): Record<string, number>;
|
|
8
|
+
export interface LimitOffsetOpts {
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Limit/offset query params to merge into a list request. Some next-core routes
|
|
14
|
+
* (email-template, announcements) paginate with limit/offset rather than the
|
|
15
|
+
* Django page/page_size scheme, so this lives beside `listPageParams` and stays
|
|
16
|
+
* the single place those two flags are serialized.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listLimitOffsetParams(opts: LimitOffsetOpts): Record<string, number>;
|
|
19
|
+
/**
|
|
20
|
+
* Pagination block echoed in the envelope. `count`/`next`/`previous` are
|
|
21
|
+
* server-truth. `page_size` is echoed ONLY when the caller set it (the clamped
|
|
22
|
+
* value we sent) — we don't fabricate a default, because the server can apply a
|
|
23
|
+
* different window than requested (e.g. Django ignores page_size when `id__in`
|
|
24
|
+
* is present), so a guessed value would misreport what actually happened.
|
|
25
|
+
*/
|
|
26
|
+
export declare function listPaginationEcho(opts: PageOpts, res: {
|
|
27
|
+
count?: unknown;
|
|
28
|
+
next?: unknown;
|
|
29
|
+
previous?: unknown;
|
|
30
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shared list-pagination helpers so the page-size cap and default live in ONE
|
|
2
|
+
// place (files.ts and folders.ts both consume these — see PR #4956 review).
|
|
3
|
+
export const MAX_PAGE_SIZE = 500;
|
|
4
|
+
function clampPageSize(pageSize) {
|
|
5
|
+
return pageSize ? Math.min(pageSize, MAX_PAGE_SIZE) : undefined;
|
|
6
|
+
}
|
|
7
|
+
/** Page query params to merge into a list request (clamped so sent === echoed). */
|
|
8
|
+
export function listPageParams(opts) {
|
|
9
|
+
const params = {};
|
|
10
|
+
if (opts.page !== undefined)
|
|
11
|
+
params.page = opts.page;
|
|
12
|
+
const pageSize = clampPageSize(opts.pageSize);
|
|
13
|
+
if (pageSize !== undefined)
|
|
14
|
+
params.page_size = pageSize;
|
|
15
|
+
return params;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Limit/offset query params to merge into a list request. Some next-core routes
|
|
19
|
+
* (email-template, announcements) paginate with limit/offset rather than the
|
|
20
|
+
* Django page/page_size scheme, so this lives beside `listPageParams` and stays
|
|
21
|
+
* the single place those two flags are serialized.
|
|
22
|
+
*/
|
|
23
|
+
export function listLimitOffsetParams(opts) {
|
|
24
|
+
const params = {};
|
|
25
|
+
if (opts.limit !== undefined)
|
|
26
|
+
params.limit = opts.limit;
|
|
27
|
+
if (opts.offset !== undefined)
|
|
28
|
+
params.offset = opts.offset;
|
|
29
|
+
return params;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pagination block echoed in the envelope. `count`/`next`/`previous` are
|
|
33
|
+
* server-truth. `page_size` is echoed ONLY when the caller set it (the clamped
|
|
34
|
+
* value we sent) — we don't fabricate a default, because the server can apply a
|
|
35
|
+
* different window than requested (e.g. Django ignores page_size when `id__in`
|
|
36
|
+
* is present), so a guessed value would misreport what actually happened.
|
|
37
|
+
*/
|
|
38
|
+
export function listPaginationEcho(opts, res) {
|
|
39
|
+
const pageSize = clampPageSize(opts.pageSize);
|
|
40
|
+
return {
|
|
41
|
+
count: res.count,
|
|
42
|
+
next: res.next,
|
|
43
|
+
previous: res.previous,
|
|
44
|
+
page: opts.page ?? 1,
|
|
45
|
+
...(pageSize !== undefined ? { page_size: pageSize } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { createCliError } from './errors.js';
|
|
3
|
+
export function readTextFile(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
// Covers missing AND unreadable (permissions, directory, etc.) paths — fs
|
|
9
|
+
// throws the same way for both, so don't claim "not found" specifically.
|
|
10
|
+
throw createCliError('VALIDATION', `could not read file: ${filePath}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function readJsonFile(filePath) {
|
|
14
|
+
const text = readTextFile(filePath);
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Keep the message generic: the JSON.parse error echoes a snippet of the
|
|
20
|
+
// file, which may contain customer PII.
|
|
21
|
+
throw createCliError('VALIDATION', `invalid JSON in ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import axios, { type AxiosInstance } from 'axios';
|
|
2
|
+
export interface UploadDeps {
|
|
3
|
+
put?: typeof axios.put;
|
|
4
|
+
readFile?: (p: string) => Buffer;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Upload a local file via the presigned-URL flow and return its content_upload_id.
|
|
8
|
+
* The S3 PUT uses a BARE axios call (not the Pitcher client) so it carries no
|
|
9
|
+
* x-api-key, and sends Content-Type equal to the server-signed `type` — omitting
|
|
10
|
+
* or mismatching it yields a 403 SignatureDoesNotMatch.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uploadFile(client: AxiosInstance, opts: {
|
|
13
|
+
instanceId: string;
|
|
14
|
+
filePath: string;
|
|
15
|
+
}, deps?: UploadDeps): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { createCliError } from './errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Upload a local file via the presigned-URL flow and return its content_upload_id.
|
|
7
|
+
* The S3 PUT uses a BARE axios call (not the Pitcher client) so it carries no
|
|
8
|
+
* x-api-key, and sends Content-Type equal to the server-signed `type` — omitting
|
|
9
|
+
* or mismatching it yields a 403 SignatureDoesNotMatch.
|
|
10
|
+
*/
|
|
11
|
+
export async function uploadFile(client, opts, deps = {}) {
|
|
12
|
+
const readFile = deps.readFile ?? fs.readFileSync;
|
|
13
|
+
const put = deps.put ?? axios.put;
|
|
14
|
+
const resolved = path.resolve(opts.filePath);
|
|
15
|
+
const buffer = readFile(resolved);
|
|
16
|
+
const name = path.basename(resolved);
|
|
17
|
+
const createRes = await client.post('/api/v1/uploads/', {
|
|
18
|
+
name,
|
|
19
|
+
size: buffer.length,
|
|
20
|
+
instance_id: opts.instanceId,
|
|
21
|
+
});
|
|
22
|
+
const { id, presigned_url, type } = createRes.data;
|
|
23
|
+
// `type` is required: it's the server-signed Content-Type; sending undefined
|
|
24
|
+
// yields an opaque S3 403 SignatureDoesNotMatch instead of a clear error.
|
|
25
|
+
if (!id || !presigned_url || !type) {
|
|
26
|
+
throw createCliError('NETWORK', 'Upload init failed: /uploads response missing id, presigned_url, or type');
|
|
27
|
+
}
|
|
28
|
+
await put(presigned_url, buffer, { headers: { 'Content-Type': type } });
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
|
|
2
|
+
export declare function parseJsonObject(raw: string, flagName: string): Record<string, unknown>;
|
|
3
|
+
export declare function parseJsonArray(raw: string, flagName: string): unknown[];
|
|
4
|
+
export declare function parseTags(raw: string): string[];
|
|
5
|
+
export declare function validateAccessType(v: string | undefined): string | undefined;
|
|
6
|
+
export declare function assertIntFlags(opts: {
|
|
7
|
+
page?: number;
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
}): void;
|
|
12
|
+
export declare function deepMerge(base: unknown, patch: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createCliError } from '../core/errors.js';
|
|
2
|
+
const ACCESS_TYPES = ['public', 'restricted', 'personal'];
|
|
3
|
+
export function isPlainObject(v) {
|
|
4
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
5
|
+
}
|
|
6
|
+
function parseJson(raw, flagName) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw createCliError('VALIDATION', `--${flagName} must be valid JSON`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseJsonObject(raw, flagName) {
|
|
15
|
+
const parsed = parseJson(raw, flagName);
|
|
16
|
+
if (!isPlainObject(parsed))
|
|
17
|
+
throw createCliError('VALIDATION', `--${flagName} must be a JSON object`);
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
export function parseJsonArray(raw, flagName) {
|
|
21
|
+
const parsed = parseJson(raw, flagName);
|
|
22
|
+
if (!Array.isArray(parsed))
|
|
23
|
+
throw createCliError('VALIDATION', `--${flagName} must be a JSON array`);
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
export function parseTags(raw) {
|
|
27
|
+
return raw
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((t) => t.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
export function validateAccessType(v) {
|
|
33
|
+
if (v === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (!ACCESS_TYPES.includes(v)) {
|
|
36
|
+
throw createCliError('VALIDATION', `--access-type must be one of: ${ACCESS_TYPES.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
// Guards against NaN reaching the request (commander's parseInt yields NaN on
|
|
41
|
+
// non-numeric input, e.g. `--page abc`).
|
|
42
|
+
export function assertIntFlags(opts) {
|
|
43
|
+
// page/page-size are 1-indexed (min 1); limit/offset are counts/skips (min 0).
|
|
44
|
+
const checks = [
|
|
45
|
+
['page', opts.page, 1],
|
|
46
|
+
['page-size', opts.pageSize, 1],
|
|
47
|
+
['limit', opts.limit, 0],
|
|
48
|
+
['offset', opts.offset, 0],
|
|
49
|
+
];
|
|
50
|
+
for (const [flag, val, min] of checks) {
|
|
51
|
+
if (val !== undefined && (!Number.isInteger(val) || val < min)) {
|
|
52
|
+
throw createCliError('VALIDATION', `--${flag} must be a${min === 0 ? ' non-negative' : ' positive'} integer`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function deepMerge(base, patch) {
|
|
57
|
+
const out = isPlainObject(base) ? { ...base } : {};
|
|
58
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
59
|
+
const cur = out[k];
|
|
60
|
+
out[k] = isPlainObject(v) && isPlainObject(cur) ? deepMerge(cur, v) : v;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pitcher/cli-scripts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "The Pitcher CLI for various tasks in its ecosystem.",
|
|
5
5
|
"author": "Pitcher AG",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/archiver": "^7.0.0",
|
|
29
|
-
"@types/node": "^24.
|
|
29
|
+
"@types/node": "^24.13.2",
|
|
30
30
|
"typescript": "^6.0.3",
|
|
31
31
|
"vitest": "^4.1.9"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"archiver": "^7.0.1",
|
|
35
|
-
"axios": "^1.
|
|
35
|
+
"axios": "^1.18.1",
|
|
36
36
|
"chokidar": "^5.0.0",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"form-data": "^4.0.6"
|