@pitcher/cli-scripts 2.17.1 → 2.19.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 +69 -10
- package/dist/index.js +0 -0
- package/dist/src/commands/devSync.d.ts +2 -0
- package/dist/src/commands/devSync.js +5 -5
- package/dist/src/commands/dsr.d.ts +5 -0
- package/dist/src/commands/dsr.js +177 -21
- package/dist/src/commands/publishApp.d.ts +2 -0
- package/dist/src/commands/publishApp.js +3 -3
- package/dist/src/commands/scaffolds.js +11 -3
- package/dist/src/core/archive.js +3 -3
- package/dist/src/core/credential.d.ts +25 -0
- package/dist/src/core/credential.js +25 -0
- package/dist/src/core/http.d.ts +8 -2
- package/dist/src/core/http.js +168 -8
- package/dist/src/core/ld.d.ts +38 -2
- package/dist/src/core/ld.js +162 -6
- package/dist/src/core/readInput.js +8 -1
- package/dist/src/mcp/bundled.js +1 -1
- package/dist/src/mcp/hosted.d.ts +21 -0
- package/dist/src/mcp/hosted.js +82 -0
- package/dist/src/mcp/index.d.ts +7 -0
- package/dist/src/mcp/index.js +6 -0
- package/dist/src/mcp/prompts/dsr.js +4 -0
- package/dist/src/mcp/serve.js +1 -1
- package/dist/src/mcp/server.d.ts +19 -2
- package/dist/src/mcp/server.js +4 -4
- package/dist/src/mcp/tools/devSync.js +2 -2
- package/dist/src/mcp/tools/dsr.js +37 -8
- package/dist/src/mcp/tools/publishApp.js +2 -1
- package/dist/src/mcp/tools/types.d.ts +1 -1
- package/package.json +17 -6
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ p command --help
|
|
|
12
12
|
|
|
13
13
|
## MCP server
|
|
14
14
|
|
|
15
|
-
Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps
|
|
15
|
+
Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps one tool per CLI `run*` function: every command below (including `dsr create`/`get`/`update`/`delete`/`bind-landing` and the full `ld` surface) except the five local `scaffold` subcommands (which just print/write a starter file and make no request). `dev-sync` is exposed as a one-shot sync only; the continuous `watch` mode is not available as a tool.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
p mcp
|
|
@@ -26,7 +26,7 @@ Auth and org scope are resolved once at boot from environment variables. They ar
|
|
|
26
26
|
|
|
27
27
|
- `NG_API_KEY` (required, secret): the API key the server authenticates as.
|
|
28
28
|
- `NG_ORG` (default `dev`): organization subdomain, or set `NG_URL` directly for a full origin.
|
|
29
|
-
- `NG_ENV` (`prod` or `test`): controls the destructive-hint guard on write tools (see below).
|
|
29
|
+
- `NG_ENV` (`prod` or `test`): controls the destructive-hint guard on write tools (see below). When unset, anything other than the `dev` org (or, if `NG_URL` is set, anything other than `https://dev.my.pitcher.com`) is treated as production.
|
|
30
30
|
|
|
31
31
|
One config entry = one org, because the API key is org-specific. To work against multiple orgs, add multiple entries (e.g. `pitcher-dev`, `pitcher-prod`) with different env blocks; do not try to pass org per tool call.
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@ One config entry = one org, because the API key is org-specific. To work against
|
|
|
43
43
|
### Tool annotations
|
|
44
44
|
|
|
45
45
|
- Read commands are marked `readOnlyHint`. `dev-sync` counts as a write (it mutates remote dev-override state).
|
|
46
|
-
- `publish-app` is always marked `destructiveHint` (it creates a live, org-visible release). Other write tools get `destructiveHint` only when the resolved environment is production (`NG_ENV=prod`, or `
|
|
46
|
+
- `publish-app` is always marked `destructiveHint` (it creates a live, org-visible release). Other write tools get `destructiveHint` only when the resolved environment is production (`NG_ENV=prod`, or — with `NG_ENV` unset — a target other than `dev` / `dev.my.pitcher.com`). This steers hosts toward requiring extra confirmation before mutating a production org.
|
|
47
47
|
|
|
48
48
|
### Desktop Extension (`.mcpb`)
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ yarn mcpb:pack # → pitcher.mcpb (validated + packed)
|
|
|
55
55
|
|
|
56
56
|
Install by double-clicking `pitcher.mcpb` (or Settings → Extensions → Install Extension). Claude Desktop prompts for the API key (stored in the OS keychain) and organization; both are editable afterward, so switching orgs needs no reinstall.
|
|
57
57
|
|
|
58
|
-
The bundle reuses the same server as `p mcp` but registers a **content-tool subset (
|
|
58
|
+
The bundle reuses the same server as `p mcp` but registers a **content-tool subset (121)** — it omits the developer-only `publish-app` and `dev-sync`/`dev-sync-revert`. The `organization` field accepts a subdomain (`acme`), a bare hostname (`acme.my.pitcher.com`), or a full `https://` URL. Build details (esbuild single-file, version injection) live in `mcpb/` and `CLAUDE.md`.
|
|
59
59
|
|
|
60
60
|
This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the `pitcher-apps` skill, not this README.
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ This section is a reference only. For app-development conventions (modules, sett
|
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
# Basic usage
|
|
66
|
-
p publish-app <folder> --
|
|
66
|
+
p publish-app <folder> --org pitcher --api-key myapikey
|
|
67
67
|
|
|
68
68
|
# Publish to current folder and default org (dev)
|
|
69
69
|
p publish-app --api-key myapikey
|
|
@@ -506,7 +506,7 @@ p metadata-template upsert --instance-id 01... --sections-file ./schema.json --a
|
|
|
506
506
|
|
|
507
507
|
## To run the `dsr` command:
|
|
508
508
|
|
|
509
|
-
Create and share a Digital Sales Room (a canvas share link) and get a link back in one command.
|
|
509
|
+
Create and share a Digital Sales Room (a canvas share link) and get a link back in one command. Six subcommands: `create` (build + share, with optional landing-page binding), `get` (read an existing share back), `update` (change or clear the access settings of an existing share), `delete` (revoke a share), `bind-landing` (bind/unbind a landing template on an existing share), and `scaffold` (print a starter `{source,security,landing}` spec for `--config-file`).
|
|
510
510
|
|
|
511
511
|
The create endpoint is a **get-or-create**: sharing an already-shared canvas returns the existing link and updates only the fields you pass. That is deliberate (it mirrors Admin's "Create link once / Copy link after"), but it means `create` with individual flags can never *remove* a value — omitting a flag leaves the stored value alone. Use `update` for that, and `--clear <field>` to remove one.
|
|
512
512
|
|
|
@@ -519,13 +519,21 @@ p dsr create --instance-id 01... --canvas-id 01... --api-key myapikey
|
|
|
519
519
|
p dsr create --instance-id 01... --canvas-id 01... --auth-strategy none
|
|
520
520
|
# ShareBox + security + branded landing
|
|
521
521
|
p dsr create --instance-id 01... --sharebox-file-ids 01A,01B --account-id 01... \
|
|
522
|
-
--auth-strategy specified_domains --allowed-domains acme.com --valid-until 2026-
|
|
522
|
+
--auth-strategy specified_domains --allowed-domains acme.com --valid-until 2026-12-01T00:00:00Z \
|
|
523
523
|
--landing-template-id 01... --landing-context '{"rep":"Sam"}'
|
|
524
524
|
# bind/unbind a landing template on an existing share
|
|
525
|
-
p dsr bind-landing --id 01... --landing-template-id 01...
|
|
526
|
-
p dsr bind-landing --id 01... --landing-template-id none
|
|
525
|
+
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01...
|
|
526
|
+
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id none
|
|
527
|
+
# merge fields from a FILE instead of inline JSON (the only spelling that works in PowerShell)
|
|
528
|
+
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
|
|
529
|
+
p dsr create --instance-id 01... --canvas-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
|
|
530
|
+
# read an existing share back
|
|
531
|
+
p dsr get --id 01...
|
|
527
532
|
# refuse to reuse an existing share instead of silently returning it
|
|
528
533
|
p dsr create --canvas-id 01... --if-exists error
|
|
534
|
+
# send the download preference explicitly — either direction
|
|
535
|
+
p dsr create --canvas-id 01... --allow-download
|
|
536
|
+
p dsr create --canvas-id 01... --no-allow-download
|
|
529
537
|
# change an existing share — only the flags you pass are sent
|
|
530
538
|
p dsr update --id 01... --valid-until 2026-12-01T00:00:00Z
|
|
531
539
|
p dsr update --id 01... --auth-strategy specified_domains --allowed-domains acme.com
|
|
@@ -538,8 +546,33 @@ p dsr delete --id 01...
|
|
|
538
546
|
p dsr scaffold --out share.json
|
|
539
547
|
```
|
|
540
548
|
|
|
549
|
+
### The `--config-file` spec
|
|
550
|
+
|
|
551
|
+
`p dsr scaffold` prints the starter. The spec has three blocks — `source`, `security`, `landing` — and the CLI sends `source` and `security` **spread verbatim into the create POST body**, so their keys are the wire (snake_case) names, not flag names. `landing` is not part of that body: it becomes a follow-up PUT to `/{id}/landing-page` after the share exists.
|
|
552
|
+
|
|
553
|
+
The `source` block picks what is shared. The scaffold emits `canvas_id` (an example value) **and** an empty `sharebox_items: []`, so both source keys are visible in the printed JSON; an empty array asks for nothing, so the untouched scaffold shares `canvas_id`. To share files instead, fill `sharebox_items`, delete the `canvas_id` line, and add `instance_id` (or pass `--instance-id`). `p dsr create --help` lists the same field names.
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
"source": { "instance_id": "01...", "sharebox_items": [{ "file_id": "01..." }, { "file_id": "01...", "page_index": 2 }] }
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
| Key | Type | Meaning |
|
|
560
|
+
|-----|------|---------|
|
|
561
|
+
| `canvas_id` | ULID string | Share an **existing** canvas. Exactly one of `canvas_id` / `sharebox_items`. |
|
|
562
|
+
| `sharebox_items` | `[{ file_id, page_index? }]` | Build a **new** ShareBox canvas from these files and share that. `file_id` is a ULID; an integer `page_index` shares that single page of the file instead of the whole file (`--sharebox-file-ids` can only send whole files, so a per-page share needs the spec). Exactly one of `canvas_id` / `sharebox_items`. |
|
|
563
|
+
| `canvas_name` | string | Name of the ShareBox-built canvas. Server default is `ShareBox <timestamp>`. |
|
|
564
|
+
| `account_id` | ULID string | Account attached to the ShareBox-built canvas. **Required** when the instance is configured to require an account on canvas creation. |
|
|
565
|
+
| `template_id` | ULID string | Canvas template the ShareBox-built canvas is built from. Omitted, the server looks for the instance's `share`-tagged template. |
|
|
566
|
+
| `instance_id` | ULID string | The instance to create in. `--instance-id` overrides it when both are given, and it is also what the landing-page PUT is scoped to. |
|
|
567
|
+
|
|
568
|
+
`canvas_name`, `account_id`, `template_id` and `security.tags` name or shape the canvas the server **mints**, so they only mean anything for a `sharebox_items` source — with `canvas_id` the canvas already exists and there is nothing to name. `tags` is the one the CLI enforces: a non-empty `tags` alongside `canvas_id` is rejected rather than sent to be dropped. The scaffold's empty `tags: []` is accepted, since it asks for nothing.
|
|
569
|
+
|
|
570
|
+
`security` holds the access settings (`auth_strategy`, `allowed_emails`, `allowed_domains`, `valid_until`, `allow_forward`, `allow_download`, `tags`) and every key in it is spread into the POST body **exactly as written**. The CLI checks three of them and no more: `auth_strategy` against the allowed values, the `allowed_emails`/`allowed_domains` that strategy requires, and `tags` (above). The rest — `allow_download` included — it forwards untouched and makes no claim about, so read the created share back if you need to confirm a value took. The scaffold deliberately **omits** `allow_download`: it is stored on the canvas (every link on that canvas reads it), and the server writes it only when the key is present — omitted, an existing canvas keeps its current value and a new ShareBox canvas carries none, which the viewer treats as **downloads allowed**. Add `"allow_download": false` to the spec to turn downloads off. Sending the block as written also matters against an already-shared canvas — see the `--if-exists` note above. `landing` holds `landing_page_template_id` (discover ids with `p dsr-landing list`) and `landing_page_context`; left at the scaffold's `null` / `{}` no landing page is bound at all. Binding one — from the spec or from `--landing-template-id` — needs an instance: `--instance-id` or `source.instance_id`. Without it the CLI skips the bind and returns the share with a warning naming the flag.
|
|
571
|
+
|
|
541
572
|
`update` PATCHes `/core/api/protected/shared-links/{id}`, which distinguishes an **absent** key from an **explicit null** — so omitting `--valid-until` leaves the stored expiry exactly as it is, and `--clear valid-until` is what resets it. `--clear` is repeatable and accepts `valid-until`, `allowed-emails`, `allowed-domains` (the CLI sends `null` for the first and `[]` for the lists, matching what each field's server schema accepts). An empty `--allowed-emails`/`--allowed-domains` is rejected on both `create` and `update`, because `""` from an unset shell variable would otherwise be sent as `[]` and wipe the stored list. `--clear` is a separate flag rather than an in-value sentinel such as `--valid-until ""` because an empty string is something a script can produce by accident from an unset variable, and `none` is already a real `--auth-strategy` value. `tags` and `--allow-download` are **create-time only** — the PATCH surface does not accept them, and `--clear` says so rather than dropping them silently.
|
|
542
573
|
|
|
574
|
+
`--allow-download` has a `--no-allow-download` twin, so **both** directions are sendable (PIT-7521). Without it commander could only ever produce `true` or nothing: passing the flag said “allow”, and there was no way to say “do not allow” — the OFF state could not be expressed at all, only left out. Declaration order is load-bearing: the positive flag of each pair is declared first, because commander takes a boolean's default from whichever it sees first. ⚠️ The value records the sharer's **intent** and drives the viewer's UI; it is not a server-side download block — the file URL is already in the payload the browser receives.
|
|
575
|
+
|
|
543
576
|
**Clearing an allow-list is refused while a strategy that needs it is in force**, because an empty allow-list denies every recipient rather than admitting them — the clear would brick the link, not open it. `specified_emails`/`verified_emails` need `allowed-emails`; `specified_domains`/`verified_domains` need `allowed-domains`. The check covers a strategy passed in the same call **and** the one already stored on the share (read from the public `GET /core/api/shared-links/{id}/config`, since the PATCH surface has no GET). If that read fails the update still goes through, with a warning saying the check did not run. Two ways out:
|
|
544
577
|
|
|
545
578
|
```bash
|
|
@@ -549,9 +582,35 @@ p dsr update --id 01... --auth-strategy any_email --clear allowed-emails
|
|
|
549
582
|
p dsr update --id 01... --allowed-emails new@acme.com
|
|
550
583
|
```
|
|
551
584
|
|
|
585
|
+
### `--landing-context` from a file
|
|
586
|
+
|
|
587
|
+
`--landing-context` takes a JSON object as a single shell argument, which **PowerShell cannot express**: it strips the double quotes a JSON object cannot do without, so on Windows the flag has no usable spelling at all. `--landing-context-file <path>` is the same field read off disk, and it is available on **both** `dsr create` and `dsr bind-landing`:
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
echo '{"rep":"Sam","account":"Acme"}' > ctx.json
|
|
591
|
+
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
Exactly one of the two may be given: passing **both** fails `VALIDATION` (exit 3) before any request, naming the pair. The file must hold a JSON **object** — valid JSON that is an array or a scalar is rejected with the same code, as is malformed JSON. `--landing-context-file` is a landing flag like the rest, so it is mutually exclusive with `--config-file` (whose `landing.landing_page_context` block is that spec's own way of saying the same thing).
|
|
595
|
+
|
|
596
|
+
### `p dsr get` — read a share back
|
|
597
|
+
|
|
598
|
+
`p dsr get --id <id>` returns the share's current settings. The protected shared-links surface has **no GET**, so this wraps the public pre-auth config the sharing page itself loads (`GET /core/api/shared-links/{id}/config`) and returns that body **unchanged**. Nothing is reshaped, so the field names are the wire names.
|
|
599
|
+
|
|
600
|
+
That body is the share's **access config**, and nothing else: `auth_strategy`, `allowed_domains`, `allowed_emails`, `valid_until`, `allow_forward` (plus `allow_forward_raw`, the rep's own toggle before the admin `allow_dsr_forwarding` setting is applied), the landing-page binding as `landing_page_template_id` / `landing_page_template_snapshot` / `landing_page_context`, and `created_at` / `modified_at`. **There is no `canvas` or `file` field** — the endpoint selects neither, so hold on to the `canvas` that `create` returned if you need it.
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
p dsr get --id 01...
|
|
604
|
+
# confirm what a create actually stored
|
|
605
|
+
p dsr create --instance-id 01... --canvas-id 01... --auth-strategy specified_domains --allowed-domains acme.com
|
|
606
|
+
p dsr get --id 01...
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Use it to confirm what `create` or `update` stored (`create` is a get-or-create, so what you passed is not always what the share ends up with) and to check a link before changing it. There is **one** id failure mode: any id that matches no share answers `404` → `NOT_FOUND` (exit 5). The endpoint does not validate the id's format — it matches `pitcher_sharedlink.id`, a plain varchar — so a malformed id is a miss like any other and returns `NOT_FOUND` too, not `VALIDATION`.
|
|
610
|
+
|
|
552
611
|
`create` returns the share as `{ id, type, file, canvas, instance, organization, status, url, short_url }` — **`url` is the full link; there is no `full_url` field.**
|
|
553
612
|
|
|
554
|
-
Creating, updating and deleting need a share-capable key (`canvas.share` on the link's own instance when `enable_granular_permissions` is on); **binding a landing page
|
|
613
|
+
`get` reads a public, pre-auth endpoint, so it needs no special permission beyond a valid key. Creating, updating and deleting need a share-capable key (`canvas.share` on the link's own instance when `enable_granular_permissions` is on); **binding a landing page** needs `--instance-id`, and an admin or editor key to bind any share in the org — a member key can bind only the shares that member created (PIT-7525). On `create`, a denied bind returns the link with a warning. Sending `--allow-download` / `--no-allow-download` (or `allow_download` in a spec) with an existing `--canvas-id` needs you to own that canvas or hold `canvas.update` on it — the setting is stored on the canvas — otherwise the create is refused as `AUTH` and nothing is written; a ShareBox source is always yours, so it never hits this. Discover landing template IDs via `p dsr-landing list`.
|
|
555
614
|
|
|
556
615
|
## To run the `ld` command:
|
|
557
616
|
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { AxiosInstance } from 'axios';
|
|
3
|
+
import { type Credential } from '../core/credential.js';
|
|
3
4
|
import { type Envelope } from '../core/envelope.js';
|
|
4
5
|
interface AppJson {
|
|
5
6
|
name: string;
|
|
@@ -17,6 +18,7 @@ interface SyncResponse {
|
|
|
17
18
|
interface DevSyncOptions {
|
|
18
19
|
fileId: string;
|
|
19
20
|
apiKey?: string;
|
|
21
|
+
credential?: Credential;
|
|
20
22
|
org?: string;
|
|
21
23
|
quiet?: boolean;
|
|
22
24
|
output?: string;
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import FormData from 'form-data';
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
|
-
import {
|
|
6
|
+
import { credentialOf } from '../core/credential.js';
|
|
7
7
|
import { resolveBaseUrl } from '../core/baseUrl.js';
|
|
8
8
|
import { makeClient, toCliError } from '../core/http.js';
|
|
9
9
|
import { zipDirectoryToBuffer } from '../core/archive.js';
|
|
@@ -32,11 +32,11 @@ export function readAppJson(distPath) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function runDevSync(opts, deps = {}) {
|
|
34
34
|
try {
|
|
35
|
-
const
|
|
35
|
+
const cred = credentialOf(opts);
|
|
36
36
|
const baseUrl = resolveBaseUrl(opts);
|
|
37
37
|
const distPath = findDistPath(deps.cwd);
|
|
38
38
|
const appJson = readAppJson(distPath);
|
|
39
|
-
const client = deps.client ?? makeClient(baseUrl,
|
|
39
|
+
const client = deps.client ?? makeClient(baseUrl, cred);
|
|
40
40
|
log(`Syncing ${appJson.name} from ${distPath}...`, { quiet: opts.quiet });
|
|
41
41
|
const zip = await zipDirectoryToBuffer(distPath);
|
|
42
42
|
log(`Zip created: ${(zip.length / 1024).toFixed(2)} KB`, { quiet: opts.quiet });
|
|
@@ -57,9 +57,9 @@ export async function runDevSync(opts, deps = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
export async function runRevert(opts, deps = {}) {
|
|
59
59
|
try {
|
|
60
|
-
const
|
|
60
|
+
const cred = credentialOf(opts);
|
|
61
61
|
const baseUrl = resolveBaseUrl(opts);
|
|
62
|
-
const client = deps.client ?? makeClient(baseUrl,
|
|
62
|
+
const client = deps.client ?? makeClient(baseUrl, cred);
|
|
63
63
|
const res = await client.delete(SYNC_ENDPOINT, {
|
|
64
64
|
headers: { 'Content-Type': 'application/json' },
|
|
65
65
|
data: { file_id: opts.fileId },
|
|
@@ -12,6 +12,7 @@ export declare function runDsrBindLanding(opts: CommonOpts & {
|
|
|
12
12
|
id?: string;
|
|
13
13
|
landingTemplateId?: string;
|
|
14
14
|
landingContext?: string;
|
|
15
|
+
landingContextFile?: string;
|
|
15
16
|
instanceId?: string;
|
|
16
17
|
}, deps?: DsrDeps): Promise<Envelope<unknown>>;
|
|
17
18
|
export interface DsrCreateOpts extends CommonOpts {
|
|
@@ -30,6 +31,7 @@ export interface DsrCreateOpts extends CommonOpts {
|
|
|
30
31
|
tags?: string[];
|
|
31
32
|
landingTemplateId?: string;
|
|
32
33
|
landingContext?: string;
|
|
34
|
+
landingContextFile?: string;
|
|
33
35
|
configFile?: string;
|
|
34
36
|
ifExists?: string;
|
|
35
37
|
}
|
|
@@ -48,6 +50,9 @@ export interface DsrUpdateOpts extends CommonOpts {
|
|
|
48
50
|
allowForward?: boolean;
|
|
49
51
|
clear?: string[];
|
|
50
52
|
}
|
|
53
|
+
export declare function runDsrGet(opts: CommonOpts & {
|
|
54
|
+
id?: string;
|
|
55
|
+
}, deps?: DsrDeps): Promise<Envelope<unknown>>;
|
|
51
56
|
export declare function assembleUpdateBody(opts: DsrUpdateOpts): Record<string, unknown>;
|
|
52
57
|
export declare function runDsrUpdate(opts: DsrUpdateOpts, deps?: DsrDeps): Promise<Envelope<unknown>>;
|
|
53
58
|
export declare function runDsrDelete(opts: CommonOpts & {
|
package/dist/src/commands/dsr.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ok, fail } from '../core/envelope.js';
|
|
|
4
4
|
import { createCliError } from '../core/errors.js';
|
|
5
5
|
import { emitEnvelope } from '../core/output.js';
|
|
6
6
|
import { parseJsonObject, parseTags, isPlainObject } from '../schema/content.js';
|
|
7
|
-
import { readTextFile } from '../core/readInput.js';
|
|
7
|
+
import { readTextFile, parseJsonInput } from '../core/readInput.js';
|
|
8
8
|
import { withAuthFlags } from './withAuth.js';
|
|
9
9
|
import { emitScaffold, DSR_SHARE_SCAFFOLD_JSON } from './scaffolds.js';
|
|
10
10
|
const ENDPOINT = '/core/api/protected/shared-links';
|
|
@@ -19,9 +19,60 @@ function resolveTemplateId(raw) {
|
|
|
19
19
|
return null;
|
|
20
20
|
return raw;
|
|
21
21
|
}
|
|
22
|
+
// ── landing context: inline OR file (PIT-7521 QA Q2) ────────────────────────
|
|
23
|
+
// `--landing-context` is a JSON object passed as a shell argument, which PowerShell mangles — it
|
|
24
|
+
// eats the double quotes a JSON object cannot do without, so on Windows the flag has no usable
|
|
25
|
+
// spelling at all. `--landing-context-file` is the way in: same field, read off disk, no quoting.
|
|
26
|
+
//
|
|
27
|
+
// The exclusion check is PRESENCE-based (`!== undefined`), not truthiness, and it runs BEFORE
|
|
28
|
+
// `parseJsonInput`: that helper's own XOR (`resolveTextInput`) is truthy-based, so it would let
|
|
29
|
+
// `--landing-context '' --landing-context-file ctx.json` through and silently read the file. Same
|
|
30
|
+
// reasoning as `metadata-template`'s `--sections` / `--sections-file` pair.
|
|
31
|
+
function resolveLandingContext(inline, file) {
|
|
32
|
+
if (inline !== undefined && file !== undefined)
|
|
33
|
+
throw createCliError('VALIDATION', 'pass exactly one of --landing-context or --landing-context-file');
|
|
34
|
+
if (inline === undefined && file === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
// Exactly one flag is present from here on. An EMPTY string for either flag still short-circuits
|
|
37
|
+
// inside `resolveTextInput`, whose XOR is truthy-based, so `parseJsonInput` would answer `provide
|
|
38
|
+
// --landing-context (inline JSON) or --landing-context-file` -- naming the flag the caller just
|
|
39
|
+
// passed. That is the wrong advice for the accident this command family documents: `--flag "$MAYBE_UNSET"`
|
|
40
|
+
// expanding to an empty string is why `--clear` exists. Name the real problem instead. The
|
|
41
|
+
// genuinely-neither case is returned above and never reaches here, so it keeps that message.
|
|
42
|
+
if (inline === '')
|
|
43
|
+
throw createCliError('VALIDATION', '--landing-context must be valid JSON');
|
|
44
|
+
if (file === '')
|
|
45
|
+
throw createCliError('VALIDATION', '--landing-context-file requires a file path');
|
|
46
|
+
const parsed = parseJsonInput(inline, file, 'landing-context');
|
|
47
|
+
if (!isPlainObject(parsed))
|
|
48
|
+
throw createCliError('VALIDATION', '--landing-context must be a JSON object');
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
// The protected shared-links surface has NO GET — the only readback the platform offers is the
|
|
52
|
+
// PUBLIC pre-auth config the sharing page itself reads. Both callers go through this one function
|
|
53
|
+
// so the URL, the encoding and the error mapping have a single definition: `dsr get`, which returns
|
|
54
|
+
// the body as-is, and `dsr update`'s stored-strategy lookup, which swallows the failure.
|
|
55
|
+
function shareConfigUrl(id) {
|
|
56
|
+
return `/core/api/shared-links/${encodeURIComponent(id)}/config`;
|
|
57
|
+
}
|
|
58
|
+
async function fetchShareConfig(client, id) {
|
|
59
|
+
const res = await client.get(shareConfigUrl(id));
|
|
60
|
+
return res.data;
|
|
61
|
+
}
|
|
62
|
+
// The landing-page PUT is guarded by next-core's `withAtLeastInstanceAdmin`, which resolves the
|
|
63
|
+
// instance from the URL path, then the request body, then the `instance_id` query param. This route
|
|
64
|
+
// has no instance in its path and the body the CLI sends carries only the landing fields, so the
|
|
65
|
+
// query param is the ONLY source there is: without it the server answers 400 "Instance ID is
|
|
66
|
+
// required" before it ever looks at the share. Catching it here names the missing flag instead of
|
|
67
|
+
// handing back that opaque server error.
|
|
68
|
+
const LANDING_INSTANCE_REQUIRED = '--instance-id is required to bind a landing page: the landing-page endpoint resolves the instance from that query param and refuses the call without it';
|
|
69
|
+
// `instanceId` is REQUIRED, not optional-with-a-ternary: both call sites now refuse an empty one
|
|
70
|
+
// before they get here (`runDsrBindLanding` throws, `runDsrCreate` returns the share with a warning),
|
|
71
|
+
// so a params-less PUT is unreachable — and it was never anything but the guaranteed 400 above.
|
|
72
|
+
// Typing it away is what stops a future caller from quietly reintroducing that call.
|
|
22
73
|
async function bindLandingRequest(client, id, body, instanceId) {
|
|
23
74
|
const url = `${ENDPOINT}/${encodeURIComponent(id)}/landing-page`;
|
|
24
|
-
const res = await client.put(url, body,
|
|
75
|
+
const res = await client.put(url, body, { params: { instance_id: instanceId } });
|
|
25
76
|
return res.data;
|
|
26
77
|
}
|
|
27
78
|
export async function runDsrBindLanding(opts, deps = {}) {
|
|
@@ -31,9 +82,12 @@ export async function runDsrBindLanding(opts, deps = {}) {
|
|
|
31
82
|
const templateId = resolveTemplateId(opts.landingTemplateId);
|
|
32
83
|
if (templateId === undefined)
|
|
33
84
|
throw createCliError('VALIDATION', '--landing-template-id is required (use "none" to unbind)');
|
|
85
|
+
if (!opts.instanceId)
|
|
86
|
+
throw createCliError('VALIDATION', LANDING_INSTANCE_REQUIRED);
|
|
34
87
|
const body = { landing_page_template_id: templateId };
|
|
35
|
-
|
|
36
|
-
|
|
88
|
+
const context = resolveLandingContext(opts.landingContext, opts.landingContextFile);
|
|
89
|
+
if (context !== undefined)
|
|
90
|
+
body.landing_page_context = context;
|
|
37
91
|
const client = clientFor(opts, deps);
|
|
38
92
|
const data = await bindLandingRequest(client, opts.id, body, opts.instanceId);
|
|
39
93
|
return ok(data);
|
|
@@ -48,6 +102,7 @@ const AUTH_STRATEGIES = [
|
|
|
48
102
|
const SPEC_FLAG_KEYS = [
|
|
49
103
|
'canvasId', 'shareboxFileIds', 'canvasName', 'accountId', 'templateId', 'authStrategy', 'allowedEmails',
|
|
50
104
|
'allowedDomains', 'validUntil', 'allowForward', 'allowDownload', 'tags', 'landingTemplateId', 'landingContext',
|
|
105
|
+
'landingContextFile',
|
|
51
106
|
];
|
|
52
107
|
// config-file blocks must be JSON objects — a bare cast would silently spread a string/array into the request body
|
|
53
108
|
function requirePlainObjectField(v, field) {
|
|
@@ -105,9 +160,28 @@ function assertCreateBody(body, instanceId) {
|
|
|
105
160
|
const items = body.sharebox_items;
|
|
106
161
|
const hasSharebox = Array.isArray(items) && items.length > 0;
|
|
107
162
|
if (hasCanvas === hasSharebox)
|
|
108
|
-
throw createCliError('VALIDATION', 'pass exactly one source: --canvas-id or --sharebox-file-ids');
|
|
163
|
+
throw createCliError('VALIDATION', 'pass exactly one source: --canvas-id or --sharebox-file-ids (in a --config-file spec: source.canvas_id or a non-empty source.sharebox_items)');
|
|
109
164
|
if (hasSharebox && !instanceId && body.instance_id === undefined)
|
|
110
|
-
throw createCliError('VALIDATION', '--instance-id is required for --sharebox-file-ids');
|
|
165
|
+
throw createCliError('VALIDATION', '--instance-id is required for --sharebox-file-ids (in a --config-file spec: --instance-id or source.instance_id)');
|
|
166
|
+
const tags = body.tags;
|
|
167
|
+
// SHAPE first: a hand-written spec can put anything under `security.tags`. The server would refuse a
|
|
168
|
+
// malformed value too (its ZodError catch answers `400 Invalid request data`), so this is not closing
|
|
169
|
+
// a hole — it fails before any request, with a message that names the field. This guard already owns
|
|
170
|
+
// the key, so it owns the shape too — and shape means the ELEMENTS as well: the server field is
|
|
171
|
+
// `z.array(z.string())`, so `tags: [1]` clears `Array.isArray` and still has to be refused. An EMPTY
|
|
172
|
+
// array passes both checks, by design.
|
|
173
|
+
if (tags !== undefined && (!Array.isArray(tags) || tags.some((t) => typeof t !== 'string')))
|
|
174
|
+
throw createCliError('VALIDATION', 'tags must be an array of strings');
|
|
175
|
+
// SOURCE second: `tags` is read by exactly one branch of the server route — the ShareBox branch,
|
|
176
|
+
// where it names the canvas the server MINTS from sharebox_items. With an existing canvas there is
|
|
177
|
+
// nothing to name: the POST schema still accepts the key and then drops it on the floor, so the
|
|
178
|
+
// tags vanish with no error at all. Refuse the combination instead of shipping a silent no-op.
|
|
179
|
+
// An EMPTY array is NOT a request to tag anything: `dsr scaffold` emits `tags: []` in its starter
|
|
180
|
+
// security block, so rejecting that would break every untouched `--config-file` create.
|
|
181
|
+
if (hasCanvas && Array.isArray(tags) && tags.length > 0)
|
|
182
|
+
throw createCliError('VALIDATION', 'tags apply only to a ShareBox-built canvas — they name the canvas the server creates from ' +
|
|
183
|
+
'--sharebox-file-ids, and with --canvas-id the server ignores them. Drop the tags, or share the ' +
|
|
184
|
+
'files with --sharebox-file-ids.');
|
|
111
185
|
const strat = body.auth_strategy;
|
|
112
186
|
if (strat !== undefined) {
|
|
113
187
|
if (!AUTH_STRATEGIES.includes(strat))
|
|
@@ -149,11 +223,14 @@ function resolveLandingBody(opts, cfgLanding) {
|
|
|
149
223
|
}
|
|
150
224
|
return b;
|
|
151
225
|
}
|
|
152
|
-
|
|
226
|
+
// resolved FIRST so `--landing-context X --landing-context-file Y` names the real mistake
|
|
227
|
+
// rather than being masked by the missing-template-id error below
|
|
228
|
+
const context = resolveLandingContext(opts.landingContext, opts.landingContextFile);
|
|
229
|
+
if (opts.landingTemplateId === undefined && context === undefined)
|
|
153
230
|
return null;
|
|
154
231
|
const b = { landing_page_template_id: requireCreateTemplateId(opts.landingTemplateId) };
|
|
155
|
-
if (
|
|
156
|
-
b.landing_page_context =
|
|
232
|
+
if (context !== undefined)
|
|
233
|
+
b.landing_page_context = context;
|
|
157
234
|
return b;
|
|
158
235
|
}
|
|
159
236
|
// ── create collision policy (PIT-7521) ──────────────────────────────────────
|
|
@@ -223,6 +300,16 @@ export async function runDsrCreate(opts, deps = {}) {
|
|
|
223
300
|
const shareId = share?.id;
|
|
224
301
|
if (!shareId)
|
|
225
302
|
return ok({ ...share, landing: null }, ['landing not attached: no share id in create response']);
|
|
303
|
+
// Same missing-instance failure as `dsr bind-landing`, reached from the other call site. It stays
|
|
304
|
+
// a WARNING rather than a hard failure because the share itself was created successfully and is
|
|
305
|
+
// the caller's primary output — dropping it would strand a live link with nothing referencing it.
|
|
306
|
+
// What changes is that the PUT is not attempted at all (it cannot succeed) and the warning names
|
|
307
|
+
// the flag plus the exact follow-up command, instead of echoing an opaque 400 from the server.
|
|
308
|
+
if (!effectiveInstanceId)
|
|
309
|
+
return ok({ ...share, landing: null }, [
|
|
310
|
+
`landing not attached: ${LANDING_INSTANCE_REQUIRED}. The share itself was created — bind the ` +
|
|
311
|
+
`landing page with \`p dsr bind-landing --id ${shareId} --landing-template-id <template-id> --instance-id <instance-id>\``,
|
|
312
|
+
]);
|
|
226
313
|
try {
|
|
227
314
|
const landing = await bindLandingRequest(client, shareId, landingBody, effectiveInstanceId);
|
|
228
315
|
return ok({ ...share, landing });
|
|
@@ -327,14 +414,37 @@ function clearedListFields(body) {
|
|
|
327
414
|
// that is a warning here and a hard failure in the --if-exists preflight.
|
|
328
415
|
async function readStoredAuthStrategy(client, id) {
|
|
329
416
|
try {
|
|
330
|
-
const
|
|
331
|
-
const strategy =
|
|
417
|
+
const data = (await fetchShareConfig(client, id));
|
|
418
|
+
const strategy = data?.auth_strategy;
|
|
332
419
|
return typeof strategy === 'string' ? strategy : null;
|
|
333
420
|
}
|
|
334
421
|
catch {
|
|
335
422
|
return null;
|
|
336
423
|
}
|
|
337
424
|
}
|
|
425
|
+
// ── read a share back (PIT-7521 QA Q5) ──────────────────────────────────────
|
|
426
|
+
// The only readback the platform offers. The body is returned verbatim — the CLI reshapes nothing,
|
|
427
|
+
// so what the caller sees is exactly what the sharing page is served: the ACCESS CONFIG only —
|
|
428
|
+
// auth_strategy, allowed_emails / allowed_domains, valid_until, allow_forward (+ allow_forward_raw,
|
|
429
|
+
// the rep's own toggle before the admin setting is applied), the landing binding
|
|
430
|
+
// (landing_page_template_id / _snapshot / landing_page_context) and created_at / modified_at.
|
|
431
|
+
// There is NO canvas or file field: the route selects none (see next-core
|
|
432
|
+
// `src/app/api/shared-links/[id]/config/route.ts`), so use the id from `dsr create` for those.
|
|
433
|
+
//
|
|
434
|
+
// ONE id failure mode, not two. The route has no id validation and no 400 path: it matches
|
|
435
|
+
// `pitcher_sharedlink.id` (a varchar) and answers 404 on zero rows, so ANY id that does not match
|
|
436
|
+
// a share — malformed or merely unknown — comes back 404 → NOT_FOUND via `toCliError`.
|
|
437
|
+
export async function runDsrGet(opts, deps = {}) {
|
|
438
|
+
try {
|
|
439
|
+
if (!opts.id)
|
|
440
|
+
throw createCliError('VALIDATION', '--id is required for dsr get');
|
|
441
|
+
const client = clientFor(opts, deps);
|
|
442
|
+
return ok(await fetchShareConfig(client, opts.id));
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
return fail(toCliError(e));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
338
448
|
export function assembleUpdateBody(opts) {
|
|
339
449
|
const cleared = normalizeClearFields(opts.clear);
|
|
340
450
|
assertNoEmptyLists(opts);
|
|
@@ -407,8 +517,40 @@ export async function runDsrDelete(opts, deps = {}) {
|
|
|
407
517
|
return fail(toCliError(e));
|
|
408
518
|
}
|
|
409
519
|
}
|
|
520
|
+
// PIT-7521 QA Q8: the spec's field names were documented only in the README, so a user reading
|
|
521
|
+
// `--help` had to guess the ShareBox source keys. The keys are the wire (snake_case) names.
|
|
522
|
+
const CREATE_HELP = `
|
|
523
|
+
--config-file spec (start from \`p dsr scaffold\`). Keys are the wire (snake_case)
|
|
524
|
+
names; source and security are spread into the create body as written.
|
|
525
|
+
source exactly one of:
|
|
526
|
+
canvas_id existing canvas ULID
|
|
527
|
+
sharebox_items [{ "file_id": "<ULID>", "page_index"?: 0 }] — builds a
|
|
528
|
+
new ShareBox canvas; page_index is optional (omit it
|
|
529
|
+
to share the whole file)
|
|
530
|
+
ShareBox-only extras: instance_id (required unless --instance-id),
|
|
531
|
+
canvas_name, account_id, template_id
|
|
532
|
+
security auth_strategy, allowed_emails [], allowed_domains [], valid_until,
|
|
533
|
+
allow_forward, allow_download, tags [] (tags: ShareBox only)
|
|
534
|
+
allow_download is stored on the CANVAS: omit it (the scaffold does) to
|
|
535
|
+
leave the canvas unchanged; unset, the viewer offers downloads
|
|
536
|
+
landing landing_page_template_id, landing_page_context {}
|
|
537
|
+
Example ShareBox source:
|
|
538
|
+
"source": { "instance_id": "01...", "sharebox_items": [{ "file_id": "01..." }] }`;
|
|
539
|
+
const GET_HELP = `
|
|
540
|
+
Reads the share back through the same public pre-auth config the sharing page
|
|
541
|
+
itself loads (the protected shared-links surface has no GET). The body is
|
|
542
|
+
returned unchanged, and it is the ACCESS CONFIG only: auth_strategy,
|
|
543
|
+
allowed_emails / allowed_domains, valid_until, allow_forward (plus
|
|
544
|
+
allow_forward_raw, the rep's own toggle), the landing-page binding
|
|
545
|
+
(landing_page_template_id, landing_page_template_snapshot,
|
|
546
|
+
landing_page_context) and created_at / modified_at. The endpoint returns NO
|
|
547
|
+
canvas or file — keep the id \`dsr create\` handed back for those.
|
|
548
|
+
--id the shared link ID (required)
|
|
549
|
+
Any id that matches no share answers 404 -> NOT_FOUND (exit 5). That is the
|
|
550
|
+
only id failure mode: the route does not validate the id's format, so a
|
|
551
|
+
malformed id is a miss like any other, not a 400.`;
|
|
410
552
|
export function createDsrCommand() {
|
|
411
|
-
const cmd = new Command('dsr').description('create and share a Digital Sales Room (a canvas share link)');
|
|
553
|
+
const cmd = new Command('dsr').description('create, read and share a Digital Sales Room (a canvas share link)');
|
|
412
554
|
withAuthFlags(cmd.command('scaffold'))
|
|
413
555
|
.description('print a {source,security,landing} share spec to fill in and pass to `dsr create --config-file`')
|
|
414
556
|
.option('--out <path>', 'Write the scaffold to a file instead of stdout')
|
|
@@ -418,7 +560,7 @@ export function createDsrCommand() {
|
|
|
418
560
|
});
|
|
419
561
|
withAuthFlags(cmd.command('create'))
|
|
420
562
|
.description('create + share a DSR (canvas share link) and return the link (id, url, short_url, canvas)')
|
|
421
|
-
.option('--instance-id <id>', 'Instance ID (required for --sharebox-file-ids)')
|
|
563
|
+
.option('--instance-id <id>', 'Instance ID (required for --sharebox-file-ids, and to bind a landing page)')
|
|
422
564
|
.option('--canvas-id <id>', 'Existing canvas to share (one source required)')
|
|
423
565
|
.option('--sharebox-file-ids <csv>', 'Build a canvas from these file IDs (one source required)', parseTags)
|
|
424
566
|
.option('--canvas-name <name>', 'Name for the ShareBox-built canvas')
|
|
@@ -430,16 +572,20 @@ export function createDsrCommand() {
|
|
|
430
572
|
.option('--valid-until <iso>', 'Expiry (ISO datetime)')
|
|
431
573
|
.option('--allow-forward', 'Allow recipients to forward')
|
|
432
574
|
.option('--no-allow-forward', 'Disallow forwarding')
|
|
433
|
-
.option('--allow-download', 'Allow download')
|
|
434
|
-
.option('--
|
|
575
|
+
.option('--allow-download', 'Allow download (with --canvas-id you must own the canvas or hold canvas.update on it, else AUTH)')
|
|
576
|
+
.option('--no-allow-download', 'Disallow download (same ownership rule as --allow-download)')
|
|
577
|
+
.option('--tags <csv>', 'Tags for the ShareBox-built canvas (rejected with --canvas-id, which ignores them)', parseTags)
|
|
435
578
|
.option('--landing-template-id <id>', 'Landing template to bind (optional)')
|
|
436
579
|
.option('--landing-context <json>', 'Landing personalization merge fields (JSON object)')
|
|
437
|
-
.option('--
|
|
580
|
+
.option('--landing-context-file <path>', 'Path to a JSON file with the landing merge fields — the Windows-safe alternative to --landing-context (exactly one of the two)')
|
|
581
|
+
.option('--config-file <path>', 'Full {source,security,landing} spec JSON — source is {canvas_id} or {sharebox_items: [{file_id, page_index?}], ' +
|
|
582
|
+
'instance_id}; field list below (mutually exclusive with the flags above)')
|
|
438
583
|
.option('--if-exists <mode>', 'return|error. What to do when the canvas is already shared: `return` hands back the existing link (default ' +
|
|
439
584
|
'with individual flags), `error` refuses and points at `dsr update` (default with --config-file, whose ' +
|
|
440
585
|
'security block is sent as-is and would overwrite the existing share). The check is a preflight lookup, so ' +
|
|
441
586
|
'it is advisory rather than atomic — a link created between the check and the share is still returned. ' +
|
|
442
587
|
'Never applies to a ShareBox source, which always mints a new canvas.')
|
|
588
|
+
.addHelpText('after', CREATE_HELP)
|
|
443
589
|
.action(async (options, command) => {
|
|
444
590
|
const globals = command.optsWithGlobals();
|
|
445
591
|
emitEnvelope(await runDsrCreate({ ...options, quiet: globals.quiet }), globals);
|
|
@@ -449,15 +595,25 @@ export function createDsrCommand() {
|
|
|
449
595
|
.option('--id <id>', 'Shared link ID (required)')
|
|
450
596
|
.option('--landing-template-id <id>', "Landing template ID, or 'none' to unbind (required)")
|
|
451
597
|
.option('--landing-context <json>', 'Personalization merge fields (JSON object)')
|
|
452
|
-
.option('--
|
|
598
|
+
.option('--landing-context-file <path>', 'Path to a JSON file with the merge fields — the Windows-safe alternative to --landing-context (exactly one of the two)')
|
|
599
|
+
.option('--instance-id <id>', 'Instance ID (required — the endpoint resolves the instance from it)')
|
|
453
600
|
.action(async (options, command) => {
|
|
454
601
|
const globals = command.optsWithGlobals();
|
|
455
602
|
emitEnvelope(await runDsrBindLanding({ ...options, quiet: globals.quiet }), globals);
|
|
456
603
|
});
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
604
|
+
withAuthFlags(cmd.command('get'))
|
|
605
|
+
.description('read an existing share back: gating, allow-lists, expiry, forwarding and the bound landing page')
|
|
606
|
+
.option('--id <id>', 'Shared link ID (required)')
|
|
607
|
+
.addHelpText('after', GET_HELP)
|
|
608
|
+
.action(async (options, command) => {
|
|
609
|
+
const globals = command.optsWithGlobals();
|
|
610
|
+
emitEnvelope(await runDsrGet({ ...options, quiet: globals.quiet }), globals);
|
|
611
|
+
});
|
|
612
|
+
// Every --x is declared BEFORE its --no-x on purpose: commander takes a boolean's default
|
|
613
|
+
// from whichever of the pair it sees first, so the reverse order would make the option
|
|
614
|
+
// default to `true` and every call would ship it. For allow_forward that clobbers a rep's
|
|
615
|
+
// opt-out; for allow_download it would turn an unpassed flag into an active grant.
|
|
616
|
+
// `create` above relies on the same ordering for BOTH pairs.
|
|
461
617
|
withAuthFlags(cmd.command('update'))
|
|
462
618
|
.description('update an existing share: only the flags you pass are sent; --clear removes a value')
|
|
463
619
|
.option('--id <id>', 'Shared link ID (required)')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { AxiosInstance } from 'axios';
|
|
3
|
+
import { type Credential } from '../core/credential.js';
|
|
3
4
|
import { type Envelope } from '../core/envelope.js';
|
|
4
5
|
interface PublishResult {
|
|
5
6
|
app?: {
|
|
@@ -14,6 +15,7 @@ interface PublishResult {
|
|
|
14
15
|
interface PublishOptions {
|
|
15
16
|
folder?: string;
|
|
16
17
|
apiKey?: string;
|
|
18
|
+
credential?: Credential;
|
|
17
19
|
org?: string;
|
|
18
20
|
url?: string;
|
|
19
21
|
incrementVersion?: boolean;
|
|
@@ -4,7 +4,7 @@ import os from 'os';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import FormData from 'form-data';
|
|
7
|
-
import {
|
|
7
|
+
import { credentialOf } from '../core/credential.js';
|
|
8
8
|
import { resolveBaseUrl } from '../core/baseUrl.js';
|
|
9
9
|
import { makeClient, toCliError } from '../core/http.js';
|
|
10
10
|
import { zipDirectoryToFile } from '../core/archive.js';
|
|
@@ -92,7 +92,7 @@ async function installToInstances(client, appName, instances, quiet) {
|
|
|
92
92
|
}
|
|
93
93
|
export async function runPublishApp(opts, deps = {}) {
|
|
94
94
|
try {
|
|
95
|
-
const
|
|
95
|
+
const cred = credentialOf(opts);
|
|
96
96
|
const baseUrl = resolveBaseUrl({ org: opts.org, url: opts.url });
|
|
97
97
|
const folderPath = path.resolve(opts.folder ?? '.');
|
|
98
98
|
const appJsonPath = path.join(folderPath, 'app.json');
|
|
@@ -124,7 +124,7 @@ export async function runPublishApp(opts, deps = {}) {
|
|
|
124
124
|
fs.writeFileSync(appJsonPath, JSON.stringify(existingAppJson, null, 2));
|
|
125
125
|
log(`Updated app.json version to ${nextVersion}`, { quiet: opts.quiet });
|
|
126
126
|
}
|
|
127
|
-
const client = deps.client ?? makeClient(baseUrl,
|
|
127
|
+
const client = deps.client ?? makeClient(baseUrl, cred);
|
|
128
128
|
const zipPath = path.join(deps.tmpDir ?? os.tmpdir(), `cli-publish-${process.pid}-${randomUUID()}.zip`);
|
|
129
129
|
try {
|
|
130
130
|
log(`Zipping ${folderPath}...`, { quiet: opts.quiet });
|
|
@@ -272,9 +272,14 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
|
|
|
272
272
|
export const METADATA_TEMPLATE_SCAFFOLD_JSON = JSON.stringify(METADATA_TEMPLATE_SCAFFOLD_DATA, null, 2);
|
|
273
273
|
const DSR_SHARE_SCAFFOLD_DATA = {
|
|
274
274
|
source: {
|
|
275
|
-
// provide ONE
|
|
275
|
+
// provide ONE source: canvas_id (share an existing canvas) OR a non-empty sharebox_items (build a
|
|
276
|
+
// new ShareBox canvas from files) — delete the canvas_id line when you fill sharebox_items.
|
|
277
|
+
// Emitted as a real key, not a comment, because comments never reach the printed JSON (PIT-7521 QA Q8);
|
|
278
|
+
// an EMPTY array asks for nothing, so the untouched scaffold still shares canvas_id.
|
|
276
279
|
canvas_id: '01EXAMPLECANVASID000000000',
|
|
277
|
-
|
|
280
|
+
sharebox_items: [], // [{ "file_id": "<file ULID>", "page_index"?: 0 }] — page_index optional (one page, not the whole file)
|
|
281
|
+
// ShareBox-only, and NOT emitted because the server refuses null for them — add them when needed:
|
|
282
|
+
// instance_id: '01EXAMPLEINSTANCEID0000000', (required for sharebox_items unless --instance-id is passed)
|
|
278
283
|
// canvas_name: 'Acme QBR',
|
|
279
284
|
// account_id: '01EXAMPLEACCOUNTID00000000',
|
|
280
285
|
// template_id: '01EXAMPLECANVASTEMPLATE0000',
|
|
@@ -285,7 +290,10 @@ const DSR_SHARE_SCAFFOLD_DATA = {
|
|
|
285
290
|
allowed_domains: [],
|
|
286
291
|
valid_until: null,
|
|
287
292
|
allow_forward: true,
|
|
288
|
-
allow_download: false,
|
|
293
|
+
// allow_download: false, — NOT emitted. It is stored on the CANVAS (read by every link on it), and
|
|
294
|
+
// the server writes it only when present: omitted, an existing canvas keeps its value and a new
|
|
295
|
+
// ShareBox canvas carries none, which the viewer treats as downloads allowed. Emitting a default
|
|
296
|
+
// here would silently flip an existing canvas's setting on every untouched run (PIT-7521 review).
|
|
289
297
|
tags: [],
|
|
290
298
|
},
|
|
291
299
|
landing: {
|