@pitcher/cli-scripts 2.3.0 → 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.
Files changed (37) hide show
  1. package/README.md +203 -0
  2. package/dist/index.js +22 -2
  3. package/dist/src/commands/analytics.d.ts +12 -0
  4. package/dist/src/commands/analytics.js +45 -0
  5. package/dist/src/commands/announcements.d.ts +23 -0
  6. package/dist/src/commands/announcements.js +131 -0
  7. package/dist/src/commands/canvases.d.ts +25 -0
  8. package/dist/src/commands/canvases.js +73 -0
  9. package/dist/src/commands/dsrLanding.d.ts +28 -0
  10. package/dist/src/commands/dsrLanding.js +150 -0
  11. package/dist/src/commands/emailTemplate.d.ts +32 -0
  12. package/dist/src/commands/emailTemplate.js +124 -0
  13. package/dist/src/commands/files.js +5 -8
  14. package/dist/src/commands/folders.js +5 -8
  15. package/dist/src/commands/instances.d.ts +21 -0
  16. package/dist/src/commands/instances.js +58 -0
  17. package/dist/src/commands/org.d.ts +9 -0
  18. package/dist/src/commands/org.js +26 -0
  19. package/dist/src/commands/scaffolds.d.ts +19 -0
  20. package/dist/src/commands/scaffolds.js +176 -0
  21. package/dist/src/commands/search.d.ts +13 -0
  22. package/dist/src/commands/search.js +35 -0
  23. package/dist/src/commands/sharedLinks.d.ts +16 -0
  24. package/dist/src/commands/sharedLinks.js +61 -0
  25. package/dist/src/commands/tags.d.ts +20 -0
  26. package/dist/src/commands/tags.js +52 -0
  27. package/dist/src/commands/withAuth.d.ts +3 -0
  28. package/dist/src/commands/withAuth.js +7 -0
  29. package/dist/src/commands/withInstance.d.ts +3 -0
  30. package/dist/src/commands/withInstance.js +4 -0
  31. package/dist/src/core/pagination.d.ts +11 -0
  32. package/dist/src/core/pagination.js +14 -0
  33. package/dist/src/core/readInput.d.ts +2 -0
  34. package/dist/src/core/readInput.js +23 -0
  35. package/dist/src/schema/content.d.ts +3 -0
  36. package/dist/src/schema/content.js +13 -7
  37. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ import fs from 'fs';
2
+ import { ok, fail } from '../core/envelope.js';
3
+ import { createCliError } from '../core/errors.js';
4
+ // Annotated starter-content constants for the `scaffold` subcommands. Each
5
+ // constant is a working example, never a blank template — see design §5.
6
+ // Task 1 adds EMAIL_SCAFFOLD_HTML; Tasks 2/3 add DSR + announcement consts.
7
+ export function emitScaffold(content, out) {
8
+ if (!out)
9
+ return ok(content);
10
+ try {
11
+ fs.writeFileSync(out, content);
12
+ return ok({ path: out });
13
+ }
14
+ catch {
15
+ // A bad --out path is user input, so classify it VALIDATION (not UNKNOWN).
16
+ return fail(createCliError('VALIDATION', `could not write file: ${out}`));
17
+ }
18
+ }
19
+ /**
20
+ * Starter HTML email (inline CSS, table layout) for `p email-template scaffold`.
21
+ * Contains the common {{recipient.*}} tokens in context, each documented with
22
+ * an HTML comment. The emails app resolves tokens at send time; unknown
23
+ * tokens render blank (no server-side validation).
24
+ */
25
+ export const EMAIL_SCAFFOLD_HTML = `<!doctype html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
30
+ <title>Sample Email Template</title>
31
+ </head>
32
+ <body style="margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif;">
33
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f4;padding:24px 0;">
34
+ <tr>
35
+ <td align="center">
36
+ <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden;">
37
+ <tr>
38
+ <td style="background-color:#29335C;padding:24px;text-align:center;">
39
+ <span style="color:#ffffff;font-size:20px;font-weight:bold;">Pitcher</span>
40
+ </td>
41
+ </tr>
42
+ <tr>
43
+ <td style="padding:32px 24px;color:#212121;font-size:15px;line-height:1.5;">
44
+ <!-- token: recipient.firstName -> the recipient's first name -->
45
+ <!-- token: recipient.lastName -> the recipient's last name -->
46
+ <p style="margin:0 0 16px 0;">Dear {{recipient.firstName}} {{recipient.lastName}},</p>
47
+ <p style="margin:0 0 16px 0;">
48
+ Thanks for your time today. As promised, here is the material we discussed.
49
+ Reach out any time if you have questions.
50
+ </p>
51
+ <table role="presentation" cellpadding="0" cellspacing="0" style="margin:24px 0;">
52
+ <tr>
53
+ <td style="background-color:#29335C;border-radius:4px;">
54
+ <a href="https://example.com" style="display:inline-block;padding:12px 24px;color:#ffffff;text-decoration:none;font-size:14px;font-weight:bold;">
55
+ View content
56
+ </a>
57
+ </td>
58
+ </tr>
59
+ </table>
60
+ <!-- token: recipient.email -> the recipient's email address, e.g. for a footer mailto: link -->
61
+ <p style="margin:0;color:#757575;font-size:12px;">
62
+ Sent to {{recipient.email}}. If you'd rather not receive these, let your rep know.
63
+ </p>
64
+ </td>
65
+ </tr>
66
+ </table>
67
+ </td>
68
+ </tr>
69
+ </table>
70
+ </body>
71
+ </html>
72
+ `;
73
+ /**
74
+ * Starter DSR (Digital Sales Room) landing page HTML for `p dsr-landing scaffold`.
75
+ * Contains the required `data-pitcher-email-gate` marker, an editable-region
76
+ * example via `data-field-*` attributes, a themeable accent color via the
77
+ * `--p-primary` CSS variable, and the Handlebars tokens the platform resolves
78
+ * at render time, each documented with an HTML comment. No `<script>` or
79
+ * external `<link>` tags — the server strips both via DOMPurify.
80
+ */
81
+ export const DSR_SCAFFOLD_HTML = `<!doctype html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="utf-8" />
85
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
86
+ <title>Sample DSR Landing Page</title>
87
+ </head>
88
+ <body style="margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif;color:#212121;">
89
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="padding:32px 0;">
90
+ <tr>
91
+ <td align="center">
92
+ <table role="presentation" width="640" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden;">
93
+ <tr>
94
+ <td style="background-color:var(--p-primary, #29335C);padding:28px;text-align:center;">
95
+ <!-- token: organization.name -> the sharing org's display name -->
96
+ <span style="color:#ffffff;font-size:20px;font-weight:bold;">{{organization.name}}</span>
97
+ </td>
98
+ </tr>
99
+ <tr>
100
+ <td style="padding:32px;">
101
+ <!-- token: recipient.firstName -> the recipient's first name -->
102
+ <!-- token: customer.name -> the recipient's company name -->
103
+ <h1 style="margin:0 0 8px 0;font-size:22px;">Welcome, {{recipient.firstName}}</h1>
104
+ <p style="margin:0 0 24px 0;color:#757575;font-size:14px;">Prepared for {{customer.name}}</p>
105
+
106
+ <!--
107
+ Editable region: admins can edit this block's text in the DSR
108
+ landing editor without touching markup. data-field-char-limit
109
+ enforces a max length in that editor.
110
+ -->
111
+ <div
112
+ data-field-id="welcome-message"
113
+ data-field-editable="true"
114
+ data-field-char-limit="120"
115
+ style="padding:16px;margin:0 0 24px 0;border-left:3px solid var(--p-primary, #29335C);background-color:#f7f8fb;font-size:15px;line-height:1.5;"
116
+ >
117
+ Thanks for stopping by. Everything you need for this conversation is right here.
118
+ </div>
119
+
120
+ <!-- token: rep.name -> the sharing rep's display name -->
121
+ <p style="margin:0 0 24px 0;font-size:14px;color:#757575;">Shared by {{rep.name}}</p>
122
+
123
+ <!--
124
+ Required marker: the platform injects the email-gate form
125
+ into this div at render time. It MUST be present verbatim
126
+ (a literal data-pitcher-email-gate attribute) or the
127
+ server rejects the template on create/update.
128
+ -->
129
+ <div data-pitcher-email-gate></div>
130
+ </td>
131
+ </tr>
132
+ <tr>
133
+ <td style="padding:16px 32px;border-top:1px solid #e0e2ed;text-align:center;color:#757575;font-size:12px;">
134
+ <!-- token: sharedLink.valid_until -> when this share expires, if the share sets an expiry -->
135
+ This link is valid until {{sharedLink.valid_until}}.
136
+ </td>
137
+ </tr>
138
+ </table>
139
+ </td>
140
+ </tr>
141
+ </table>
142
+ </body>
143
+ </html>
144
+ `;
145
+ /**
146
+ * Starter announcement `data` object for `p announcements scaffold`, used by
147
+ * the wa-announcements app. Every field the app understands is present with
148
+ * a sensible default so users edit a working example rather than a blank
149
+ * file. JSON cannot hold comments, so the per-field legend and the
150
+ * naive-local-date note live in `create --help` instead (see announcements.ts).
151
+ *
152
+ * `start_at`/`expires_at` are naive local date(-time) strings compared as
153
+ * wall-clock time by the app, NOT ISO/UTC — see the note in `create --help`.
154
+ */
155
+ const ANNOUNCEMENT_SCAFFOLD_DATA = {
156
+ title: 'New feature: Q3 product launch',
157
+ body: '<p>Check out what is new this quarter and how it helps your customers.</p>',
158
+ media_type: 'none',
159
+ media_url: '',
160
+ link_type: 'url',
161
+ link_url: '',
162
+ link_label: '',
163
+ link_file_id: '',
164
+ link_target_name: '',
165
+ priority: 'normal',
166
+ author_name: 'Sales Enablement Team',
167
+ published_at: '',
168
+ start_at: '',
169
+ expires_at: '',
170
+ target_groups: [],
171
+ display_mode: ['inline'],
172
+ popup_title: null,
173
+ popup_body: null,
174
+ popup_link_label: null,
175
+ };
176
+ export const ANNOUNCEMENT_SCAFFOLD_JSON = JSON.stringify(ANNOUNCEMENT_SCAFFOLD_DATA, null, 2);
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ import type { AxiosInstance } from 'axios';
3
+ import { type CommonOpts } from '../core/http.js';
4
+ import { type Envelope } from '../core/envelope.js';
5
+ export interface SearchDeps {
6
+ client?: AxiosInstance;
7
+ }
8
+ export declare function runSearch(opts: CommonOpts & {
9
+ instanceId?: string;
10
+ query?: string;
11
+ includeExpired?: boolean;
12
+ }, deps?: SearchDeps): Promise<Envelope<unknown>>;
13
+ export declare function createSearchCommand(): Command;
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander';
2
+ import { clientFor, toCliError } from '../core/http.js';
3
+ import { ok, fail } from '../core/envelope.js';
4
+ import { createCliError } from '../core/errors.js';
5
+ import { emitEnvelope } from '../core/output.js';
6
+ import { withAuthFlags } from './withAuth.js';
7
+ export async function runSearch(opts, deps = {}) {
8
+ try {
9
+ if (!opts.instanceId)
10
+ throw createCliError('VALIDATION', '--instance-id is required for search');
11
+ if (!opts.query)
12
+ throw createCliError('VALIDATION', '--query is required for search');
13
+ const client = clientFor(opts, deps);
14
+ // Flat limit/offset-paginated result set; each item carries a `type`
15
+ // (file/canvas/user/app). Pass res.data through raw — no listPaginationEcho.
16
+ const params = { instance_id: opts.instanceId, query: opts.query };
17
+ if (opts.includeExpired)
18
+ params.include_expired = true;
19
+ const res = await client.get('/api/v1/search/', { params });
20
+ return ok(res.data);
21
+ }
22
+ catch (e) {
23
+ return fail(toCliError(e));
24
+ }
25
+ }
26
+ export function createSearchCommand() {
27
+ return withAuthFlags(new Command('search').description('search content across an instance (files, canvases, users, apps)'))
28
+ .requiredOption('--instance-id <id>', 'Instance ID')
29
+ .requiredOption('--query <text>', 'Search query')
30
+ .option('--include-expired', 'Include expired content')
31
+ .action(async (options, command) => {
32
+ const globals = command.optsWithGlobals();
33
+ emitEnvelope(await runSearch({ ...options, quiet: globals.quiet }), globals);
34
+ });
35
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from 'commander';
2
+ import type { AxiosInstance } from 'axios';
3
+ import { type CommonOpts } from '../core/http.js';
4
+ import { type Envelope } from '../core/envelope.js';
5
+ export interface SharedLinksDeps {
6
+ client?: AxiosInstance;
7
+ }
8
+ export declare function runSharedLinksCreate(opts: CommonOpts & {
9
+ canvasId?: string;
10
+ fileId?: string;
11
+ }, deps?: SharedLinksDeps): Promise<Envelope<unknown>>;
12
+ export declare function runSharedLinksGet(opts: CommonOpts & {
13
+ canvasId?: string;
14
+ fileId?: string;
15
+ }, deps?: SharedLinksDeps): Promise<Envelope<unknown>>;
16
+ export declare function createSharedLinksCommand(): Command;
@@ -0,0 +1,61 @@
1
+ import { Command } from 'commander';
2
+ import { clientFor, toCliError } from '../core/http.js';
3
+ import { ok, fail } from '../core/envelope.js';
4
+ import { createCliError } from '../core/errors.js';
5
+ import { emitEnvelope } from '../core/output.js';
6
+ import { withAuthFlags } from './withAuth.js';
7
+ /** Enforce exactly one of --canvas-id / --file-id. Scope is derived from the target. */
8
+ function requireOneTarget(opts) {
9
+ if (opts.canvasId && opts.fileId)
10
+ throw createCliError('VALIDATION', 'pass exactly one of --canvas-id or --file-id');
11
+ if (opts.canvasId)
12
+ return 'canvas';
13
+ if (opts.fileId)
14
+ return 'file';
15
+ throw createCliError('VALIDATION', 'pass exactly one of --canvas-id or --file-id');
16
+ }
17
+ export async function runSharedLinksCreate(opts, deps = {}) {
18
+ try {
19
+ const target = requireOneTarget(opts);
20
+ const client = clientFor(opts, deps);
21
+ const body = target === 'canvas' ? { canvas_id: opts.canvasId } : { file_id: opts.fileId };
22
+ const res = await client.post('/api/v1/shared-links/', body);
23
+ return ok(res.data);
24
+ }
25
+ catch (e) {
26
+ return fail(toCliError(e));
27
+ }
28
+ }
29
+ export async function runSharedLinksGet(opts, deps = {}) {
30
+ try {
31
+ const target = requireOneTarget(opts);
32
+ const client = clientFor(opts, deps);
33
+ // encodeURIComponent so an id containing `/` or `..` can't retarget the request path
34
+ const path = target === 'canvas'
35
+ ? `/api/v1/shared-links/canvas/${encodeURIComponent(opts.canvasId)}`
36
+ : `/api/v1/shared-links/file/${encodeURIComponent(opts.fileId)}`;
37
+ const res = await client.get(path);
38
+ return ok(res.data);
39
+ }
40
+ catch (e) {
41
+ return fail(toCliError(e));
42
+ }
43
+ }
44
+ export function createSharedLinksCommand() {
45
+ const cmd = new Command('shared-links').description('create and read share links for canvases and files');
46
+ withAuthFlags(cmd.command('create'))
47
+ .option('--canvas-id <id>', 'Canvas to share')
48
+ .option('--file-id <id>', 'File to share')
49
+ .action(async (options, command) => {
50
+ const globals = command.optsWithGlobals();
51
+ emitEnvelope(await runSharedLinksCreate({ ...options, quiet: globals.quiet }), globals);
52
+ });
53
+ withAuthFlags(cmd.command('get'))
54
+ .option('--canvas-id <id>', 'Canvas whose link to fetch')
55
+ .option('--file-id <id>', 'File whose link to fetch')
56
+ .action(async (options, command) => {
57
+ const globals = command.optsWithGlobals();
58
+ emitEnvelope(await runSharedLinksGet({ ...options, quiet: globals.quiet }), globals);
59
+ });
60
+ return cmd;
61
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from 'commander';
2
+ import type { AxiosInstance } from 'axios';
3
+ import { type CommonOpts } from '../core/http.js';
4
+ import { type Envelope } from '../core/envelope.js';
5
+ export interface TagsDeps {
6
+ client?: AxiosInstance;
7
+ }
8
+ export declare function runTagsList(opts: CommonOpts & {
9
+ instanceId?: string;
10
+ search?: string;
11
+ type?: string;
12
+ canvasType?: string;
13
+ includeHidden?: boolean;
14
+ page?: number;
15
+ pageSize?: number;
16
+ }, deps?: TagsDeps): Promise<Envelope<{
17
+ results: unknown[];
18
+ pagination: Record<string, unknown>;
19
+ }>>;
20
+ export declare function createTagsCommand(): Command;
@@ -0,0 +1,52 @@
1
+ import { Command } from 'commander';
2
+ import { clientFor, toCliError } from '../core/http.js';
3
+ import { ok, fail } from '../core/envelope.js';
4
+ import { createCliError } from '../core/errors.js';
5
+ import { emitEnvelope } from '../core/output.js';
6
+ import { listPageParams, listPaginationEcho } from '../core/pagination.js';
7
+ import { assertIntFlags } from '../schema/content.js';
8
+ import { withAuthFlags } from './withAuth.js';
9
+ export async function runTagsList(opts, deps = {}) {
10
+ try {
11
+ if (!opts.instanceId)
12
+ throw createCliError('VALIDATION', '--instance-id is required for tags list');
13
+ if (!opts.search)
14
+ throw createCliError('VALIDATION', '--search is required for tags list');
15
+ assertIntFlags(opts);
16
+ const client = clientFor(opts, deps);
17
+ // Server enforces search min_length=3; we pass it through and let the
18
+ // server 400 rather than re-implementing field validation client-side.
19
+ const params = {
20
+ instance_id: opts.instanceId,
21
+ search: opts.search,
22
+ ...listPageParams(opts),
23
+ };
24
+ if (opts.type)
25
+ params.type = opts.type;
26
+ if (opts.canvasType)
27
+ params.canvas_type = opts.canvasType;
28
+ if (opts.includeHidden)
29
+ params.include_hidden = true;
30
+ const res = await client.get('/api/v1/tags/', { params });
31
+ return ok({ results: res.data.results, pagination: listPaginationEcho(opts, res.data) });
32
+ }
33
+ catch (e) {
34
+ return fail(toCliError(e));
35
+ }
36
+ }
37
+ export function createTagsCommand() {
38
+ const cmd = new Command('tags').description('list tags in an instance (search term of >=3 chars required)');
39
+ withAuthFlags(cmd.command('list'))
40
+ .requiredOption('--instance-id <id>', 'Instance ID')
41
+ .requiredOption('--search <text>', 'Search term (server requires at least 3 characters)')
42
+ .option('--type <type>', 'files | canvases')
43
+ .option('--canvas-type <type>', 'Canvas type filter')
44
+ .option('--include-hidden', 'Include hidden tags')
45
+ .option('--page <n>', 'Page number', (v) => parseInt(v, 10))
46
+ .option('--page-size <n>', 'Page size (max 500)', (v) => parseInt(v, 10))
47
+ .action(async (options, command) => {
48
+ const globals = command.optsWithGlobals();
49
+ emitEnvelope(await runTagsList({ ...options, quiet: globals.quiet }), globals);
50
+ });
51
+ return cmd;
52
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ /** Attach the shared auth flags (--api-key/--org/--url) to a (sub)command. */
3
+ export declare function withAuthFlags(c: Command): Command;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ /** Attach the shared --instance-id flag to a (sub)command. */
3
+ export declare function withInstanceFlag(c: Command): Command;
@@ -0,0 +1,4 @@
1
+ /** Attach the shared --instance-id flag to a (sub)command. */
2
+ export function withInstanceFlag(c) {
3
+ return c.requiredOption('--instance-id <id>', 'Instance ID');
4
+ }
@@ -5,6 +5,17 @@ export interface PageOpts {
5
5
  }
6
6
  /** Page query params to merge into a list request (clamped so sent === echoed). */
7
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>;
8
19
  /**
9
20
  * Pagination block echoed in the envelope. `count`/`next`/`previous` are
10
21
  * server-truth. `page_size` is echoed ONLY when the caller set it (the clamped
@@ -14,6 +14,20 @@ export function listPageParams(opts) {
14
14
  params.page_size = pageSize;
15
15
  return params;
16
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
+ }
17
31
  /**
18
32
  * Pagination block echoed in the envelope. `count`/`next`/`previous` are
19
33
  * server-truth. `page_size` is echoed ONLY when the caller set it (the clamped
@@ -0,0 +1,2 @@
1
+ export declare function readTextFile(filePath: string): string;
2
+ export declare function readJsonFile(filePath: string): unknown;
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
1
2
  export declare function parseJsonObject(raw: string, flagName: string): Record<string, unknown>;
2
3
  export declare function parseJsonArray(raw: string, flagName: string): unknown[];
3
4
  export declare function parseTags(raw: string): string[];
@@ -5,5 +6,7 @@ export declare function validateAccessType(v: string | undefined): string | unde
5
6
  export declare function assertIntFlags(opts: {
6
7
  page?: number;
7
8
  pageSize?: number;
9
+ limit?: number;
10
+ offset?: number;
8
11
  }): void;
9
12
  export declare function deepMerge(base: unknown, patch: Record<string, unknown>): Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  import { createCliError } from '../core/errors.js';
2
2
  const ACCESS_TYPES = ['public', 'restricted', 'personal'];
3
- function isPlainObject(v) {
3
+ export function isPlainObject(v) {
4
4
  return typeof v === 'object' && v !== null && !Array.isArray(v);
5
5
  }
6
6
  function parseJson(raw, flagName) {
@@ -24,7 +24,10 @@ export function parseJsonArray(raw, flagName) {
24
24
  return parsed;
25
25
  }
26
26
  export function parseTags(raw) {
27
- return raw.split(',').map((t) => t.trim()).filter(Boolean);
27
+ return raw
28
+ .split(',')
29
+ .map((t) => t.trim())
30
+ .filter(Boolean);
28
31
  }
29
32
  export function validateAccessType(v) {
30
33
  if (v === undefined)
@@ -37,13 +40,16 @@ export function validateAccessType(v) {
37
40
  // Guards against NaN reaching the request (commander's parseInt yields NaN on
38
41
  // non-numeric input, e.g. `--page abc`).
39
42
  export function assertIntFlags(opts) {
43
+ // page/page-size are 1-indexed (min 1); limit/offset are counts/skips (min 0).
40
44
  const checks = [
41
- ['page', opts.page],
42
- ['page-size', opts.pageSize],
45
+ ['page', opts.page, 1],
46
+ ['page-size', opts.pageSize, 1],
47
+ ['limit', opts.limit, 0],
48
+ ['offset', opts.offset, 0],
43
49
  ];
44
- for (const [flag, val] of checks) {
45
- if (val !== undefined && (!Number.isInteger(val) || val < 1)) {
46
- throw createCliError('VALIDATION', `--${flag} must be a positive integer`);
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`);
47
53
  }
48
54
  }
49
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitcher/cli-scripts",
3
- "version": "2.3.0",
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",