@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,120 @@
|
|
|
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 { parseJsonArray, validateAccessType, assertIntFlags } from '../schema/content.js';
|
|
8
|
+
import { withAuthFlags } from './withAuth.js';
|
|
9
|
+
export async function runFoldersList(opts, deps = {}) {
|
|
10
|
+
try {
|
|
11
|
+
if (!opts.instanceId)
|
|
12
|
+
throw createCliError('VALIDATION', '--instance-id is required for folders list');
|
|
13
|
+
assertIntFlags(opts);
|
|
14
|
+
const client = clientFor(opts, deps);
|
|
15
|
+
const params = { instance_id: opts.instanceId, ...listPageParams(opts) };
|
|
16
|
+
const res = await client.get('/api/v1/folders/', { params });
|
|
17
|
+
return ok({ results: res.data.results, pagination: listPaginationEcho(opts, res.data) });
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return fail(toCliError(e));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function runFoldersGet(opts, deps = {}) {
|
|
24
|
+
try {
|
|
25
|
+
if (opts.root && opts.id)
|
|
26
|
+
throw createCliError('VALIDATION', 'pass either a folder id or --root, not both');
|
|
27
|
+
if (!opts.root && !opts.id)
|
|
28
|
+
throw createCliError('VALIDATION', 'a folder id or --root is required');
|
|
29
|
+
const client = clientFor(opts, deps);
|
|
30
|
+
if (opts.root) {
|
|
31
|
+
if (!opts.instanceId)
|
|
32
|
+
throw createCliError('VALIDATION', '--instance-id is required with --root');
|
|
33
|
+
const res = await client.get(`/api/v1/folders/root/${opts.instanceId}`);
|
|
34
|
+
return ok(res.data);
|
|
35
|
+
}
|
|
36
|
+
const res = await client.get(`/api/v1/folders/${opts.id}`);
|
|
37
|
+
return ok(res.data);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return fail(toCliError(e));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function runFoldersCreate(opts, deps = {}) {
|
|
44
|
+
try {
|
|
45
|
+
if (!opts.name)
|
|
46
|
+
throw createCliError('VALIDATION', '--name is required for folders create');
|
|
47
|
+
if (!opts.parentFolderId)
|
|
48
|
+
throw createCliError('VALIDATION', '--parent-folder-id is required for folders create');
|
|
49
|
+
const client = clientFor(opts, deps);
|
|
50
|
+
const body = { name: opts.name, parent_folder_id: opts.parentFolderId };
|
|
51
|
+
const accessType = validateAccessType(opts.accessType);
|
|
52
|
+
if (accessType)
|
|
53
|
+
body.access_type = accessType;
|
|
54
|
+
if (opts.rules)
|
|
55
|
+
body.rules = parseJsonArray(opts.rules, 'rules');
|
|
56
|
+
const res = await client.post('/api/v1/folders/', body);
|
|
57
|
+
return ok(res.data);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return fail(toCliError(e));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function runFoldersUpdate(opts, deps = {}) {
|
|
64
|
+
try {
|
|
65
|
+
if (!opts.id)
|
|
66
|
+
throw createCliError('VALIDATION', 'folder id is required');
|
|
67
|
+
const client = clientFor(opts, deps);
|
|
68
|
+
const body = {};
|
|
69
|
+
if (opts.name)
|
|
70
|
+
body.name = opts.name;
|
|
71
|
+
const accessType = validateAccessType(opts.accessType);
|
|
72
|
+
if (accessType)
|
|
73
|
+
body.access_type = accessType;
|
|
74
|
+
if (opts.rules)
|
|
75
|
+
body.rules = parseJsonArray(opts.rules, 'rules');
|
|
76
|
+
const res = await client.patch(`/api/v1/folders/${opts.id}`, body);
|
|
77
|
+
return ok(res.data);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return fail(toCliError(e));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function createFoldersCommand() {
|
|
84
|
+
const cmd = new Command('folders').description('manage Pitcher content folders');
|
|
85
|
+
withAuthFlags(cmd.command('list'))
|
|
86
|
+
.requiredOption('--instance-id <id>', 'Instance ID')
|
|
87
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
88
|
+
.option('--page-size <n>', 'Page size (max 500)', (v) => parseInt(v, 10))
|
|
89
|
+
.action(async (options, command) => {
|
|
90
|
+
const globals = command.optsWithGlobals();
|
|
91
|
+
emitEnvelope(await runFoldersList({ ...options, quiet: globals.quiet }), globals);
|
|
92
|
+
});
|
|
93
|
+
withAuthFlags(cmd.command('get'))
|
|
94
|
+
.argument('[id]', 'Folder ID (omit and use --root for the instance root)')
|
|
95
|
+
.option('--root', 'Fetch the instance root folder')
|
|
96
|
+
.option('--instance-id <id>', 'Instance ID (required with --root)')
|
|
97
|
+
.action(async (id, options, command) => {
|
|
98
|
+
const globals = command.optsWithGlobals();
|
|
99
|
+
emitEnvelope(await runFoldersGet({ ...options, id, quiet: globals.quiet }), globals);
|
|
100
|
+
});
|
|
101
|
+
withAuthFlags(cmd.command('create'))
|
|
102
|
+
.requiredOption('--name <name>', 'Folder name')
|
|
103
|
+
.requiredOption('--parent-folder-id <id>', 'Parent folder ID')
|
|
104
|
+
.option('--access-type <type>', 'public | restricted | personal')
|
|
105
|
+
.option('--rules <json>', 'Smart-folder rules JSON array')
|
|
106
|
+
.action(async (options, command) => {
|
|
107
|
+
const globals = command.optsWithGlobals();
|
|
108
|
+
emitEnvelope(await runFoldersCreate({ ...options, quiet: globals.quiet }), globals);
|
|
109
|
+
});
|
|
110
|
+
withAuthFlags(cmd.command('update'))
|
|
111
|
+
.argument('<id>', 'Folder ID')
|
|
112
|
+
.option('--name <name>', 'Folder name')
|
|
113
|
+
.option('--access-type <type>', 'public | restricted | personal')
|
|
114
|
+
.option('--rules <json>', 'Smart-folder rules JSON array (replaces existing)')
|
|
115
|
+
.action(async (id, options, command) => {
|
|
116
|
+
const globals = command.optsWithGlobals();
|
|
117
|
+
emitEnvelope(await runFoldersUpdate({ ...options, id, quiet: globals.quiet }), globals);
|
|
118
|
+
});
|
|
119
|
+
return cmd;
|
|
120
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 InstancesDeps {
|
|
6
|
+
client?: AxiosInstance;
|
|
7
|
+
}
|
|
8
|
+
export declare function runInstancesList(opts: CommonOpts & {
|
|
9
|
+
region?: string;
|
|
10
|
+
storageRegion?: string;
|
|
11
|
+
environment?: string;
|
|
12
|
+
page?: number;
|
|
13
|
+
pageSize?: number;
|
|
14
|
+
}, deps?: InstancesDeps): Promise<Envelope<{
|
|
15
|
+
results: unknown[];
|
|
16
|
+
pagination: Record<string, unknown>;
|
|
17
|
+
}>>;
|
|
18
|
+
export declare function runInstancesInfo(opts: CommonOpts & {
|
|
19
|
+
id?: string;
|
|
20
|
+
}, deps?: InstancesDeps): Promise<Envelope<unknown>>;
|
|
21
|
+
export declare function createInstancesCommand(): Command;
|
|
@@ -0,0 +1,58 @@
|
|
|
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 runInstancesList(opts, deps = {}) {
|
|
10
|
+
try {
|
|
11
|
+
assertIntFlags(opts);
|
|
12
|
+
const client = clientFor(opts, deps);
|
|
13
|
+
const params = { ...listPageParams(opts) };
|
|
14
|
+
if (opts.region)
|
|
15
|
+
params.region = opts.region;
|
|
16
|
+
if (opts.storageRegion)
|
|
17
|
+
params.storage_region = opts.storageRegion;
|
|
18
|
+
if (opts.environment)
|
|
19
|
+
params.environment = opts.environment;
|
|
20
|
+
const res = await client.get('/api/v1/instances/', { params });
|
|
21
|
+
return ok({ results: res.data.results, pagination: listPaginationEcho(opts, res.data) });
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
return fail(toCliError(e));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function runInstancesInfo(opts, deps = {}) {
|
|
28
|
+
try {
|
|
29
|
+
if (!opts.id)
|
|
30
|
+
throw createCliError('VALIDATION', 'instance id is required');
|
|
31
|
+
const client = clientFor(opts, deps);
|
|
32
|
+
const res = await client.get(`/api/v1/instances/${encodeURIComponent(opts.id)}`);
|
|
33
|
+
return ok(res.data);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return fail(toCliError(e));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function createInstancesCommand() {
|
|
40
|
+
const cmd = new Command('instances').description('discover Pitcher instances (resolve names to instance IDs)');
|
|
41
|
+
withAuthFlags(cmd.command('list'))
|
|
42
|
+
.option('--region <r>', 'Filter by region')
|
|
43
|
+
.option('--storage-region <r>', 'Filter by storage region')
|
|
44
|
+
.option('--environment <e>', 'Filter by environment')
|
|
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 runInstancesList({ ...options, quiet: globals.quiet }), globals);
|
|
50
|
+
});
|
|
51
|
+
withAuthFlags(cmd.command('info'))
|
|
52
|
+
.argument('<id>', 'Instance ID')
|
|
53
|
+
.action(async (id, options, command) => {
|
|
54
|
+
const globals = command.optsWithGlobals();
|
|
55
|
+
emitEnvelope(await runInstancesInfo({ ...options, id, quiet: globals.quiet }), globals);
|
|
56
|
+
});
|
|
57
|
+
return cmd;
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 OrgDeps {
|
|
6
|
+
client?: AxiosInstance;
|
|
7
|
+
}
|
|
8
|
+
export declare function runOrgGet(opts: CommonOpts, deps?: OrgDeps): Promise<Envelope<unknown>>;
|
|
9
|
+
export declare function createOrgCommand(): Command;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { clientFor, toCliError } from '../core/http.js';
|
|
3
|
+
import { ok, fail } from '../core/envelope.js';
|
|
4
|
+
import { emitEnvelope } from '../core/output.js';
|
|
5
|
+
import { withAuthFlags } from './withAuth.js';
|
|
6
|
+
export async function runOrgGet(opts, deps = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const client = clientFor(opts, deps);
|
|
9
|
+
// Returns raw settings/metadata (no server allow-list) — may include
|
|
10
|
+
// integration credentials; treat JSON output as sensitive.
|
|
11
|
+
const res = await client.get('/api/v1/organization/');
|
|
12
|
+
return ok(res.data);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
return fail(toCliError(e));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createOrgCommand() {
|
|
19
|
+
const cmd = new Command('org').description('read the organization this API key belongs to');
|
|
20
|
+
withAuthFlags(cmd.command('get'))
|
|
21
|
+
.action(async (options, command) => {
|
|
22
|
+
const globals = command.optsWithGlobals();
|
|
23
|
+
emitEnvelope(await runOrgGet({ ...options, quiet: globals.quiet }), globals);
|
|
24
|
+
});
|
|
25
|
+
return cmd;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Envelope } from '../core/envelope.js';
|
|
2
|
+
export declare function emitScaffold(content: string, out?: string): Envelope<unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Starter HTML email (inline CSS, table layout) for `p email-template scaffold`.
|
|
5
|
+
* Contains the common {{recipient.*}} tokens in context, each documented with
|
|
6
|
+
* an HTML comment. The emails app resolves tokens at send time; unknown
|
|
7
|
+
* tokens render blank (no server-side validation).
|
|
8
|
+
*/
|
|
9
|
+
export declare const EMAIL_SCAFFOLD_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sample Email Template</title>\n </head>\n <body style=\"margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color:#f4f4f4;padding:24px 0;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color:#ffffff;border-radius:8px;overflow:hidden;\">\n <tr>\n <td style=\"background-color:#29335C;padding:24px;text-align:center;\">\n <span style=\"color:#ffffff;font-size:20px;font-weight:bold;\">Pitcher</span>\n </td>\n </tr>\n <tr>\n <td style=\"padding:32px 24px;color:#212121;font-size:15px;line-height:1.5;\">\n <!-- token: recipient.firstName -> the recipient's first name -->\n <!-- token: recipient.lastName -> the recipient's last name -->\n <p style=\"margin:0 0 16px 0;\">Dear {{recipient.firstName}} {{recipient.lastName}},</p>\n <p style=\"margin:0 0 16px 0;\">\n Thanks for your time today. As promised, here is the material we discussed.\n Reach out any time if you have questions.\n </p>\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:24px 0;\">\n <tr>\n <td style=\"background-color:#29335C;border-radius:4px;\">\n <a href=\"https://example.com\" style=\"display:inline-block;padding:12px 24px;color:#ffffff;text-decoration:none;font-size:14px;font-weight:bold;\">\n View content\n </a>\n </td>\n </tr>\n </table>\n <!-- token: recipient.email -> the recipient's email address, e.g. for a footer mailto: link -->\n <p style=\"margin:0;color:#757575;font-size:12px;\">\n Sent to {{recipient.email}}. If you'd rather not receive these, let your rep know.\n </p>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>\n";
|
|
10
|
+
/**
|
|
11
|
+
* Starter DSR (Digital Sales Room) landing page HTML for `p dsr-landing scaffold`.
|
|
12
|
+
* Contains the required `data-pitcher-email-gate` marker, an editable-region
|
|
13
|
+
* example via `data-field-*` attributes, a themeable accent color via the
|
|
14
|
+
* `--p-primary` CSS variable, and the Handlebars tokens the platform resolves
|
|
15
|
+
* at render time, each documented with an HTML comment. No `<script>` or
|
|
16
|
+
* external `<link>` tags — the server strips both via DOMPurify.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DSR_SCAFFOLD_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sample DSR Landing Page</title>\n </head>\n <body style=\"margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif;color:#212121;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding:32px 0;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" width=\"640\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color:#ffffff;border-radius:8px;overflow:hidden;\">\n <tr>\n <td style=\"background-color:var(--p-primary, #29335C);padding:28px;text-align:center;\">\n <!-- token: organization.name -> the sharing org's display name -->\n <span style=\"color:#ffffff;font-size:20px;font-weight:bold;\">{{organization.name}}</span>\n </td>\n </tr>\n <tr>\n <td style=\"padding:32px;\">\n <!-- token: recipient.firstName -> the recipient's first name -->\n <!-- token: customer.name -> the recipient's company name -->\n <h1 style=\"margin:0 0 8px 0;font-size:22px;\">Welcome, {{recipient.firstName}}</h1>\n <p style=\"margin:0 0 24px 0;color:#757575;font-size:14px;\">Prepared for {{customer.name}}</p>\n\n <!--\n Editable region: admins can edit this block's text in the DSR\n landing editor without touching markup. data-field-char-limit\n enforces a max length in that editor.\n -->\n <div\n data-field-id=\"welcome-message\"\n data-field-editable=\"true\"\n data-field-char-limit=\"120\"\n 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;\"\n >\n Thanks for stopping by. Everything you need for this conversation is right here.\n </div>\n\n <!-- token: rep.name -> the sharing rep's display name -->\n <p style=\"margin:0 0 24px 0;font-size:14px;color:#757575;\">Shared by {{rep.name}}</p>\n\n <!--\n Required marker: the platform injects the email-gate form\n into this div at render time. It MUST be present verbatim\n (a literal data-pitcher-email-gate attribute) or the\n server rejects the template on create/update.\n -->\n <div data-pitcher-email-gate></div>\n </td>\n </tr>\n <tr>\n <td style=\"padding:16px 32px;border-top:1px solid #e0e2ed;text-align:center;color:#757575;font-size:12px;\">\n <!-- token: sharedLink.valid_until -> when this share expires, if the share sets an expiry -->\n This link is valid until {{sharedLink.valid_until}}.\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>\n";
|
|
19
|
+
export declare const ANNOUNCEMENT_SCAFFOLD_JSON: string;
|
|
@@ -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
|
+
}
|