@pitcher/cli-scripts 2.20.1 → 3.0.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 (46) hide show
  1. package/README.md +45 -45
  2. package/dist/index.js +4 -0
  3. package/dist/src/commands/analytics.d.ts +3 -3
  4. package/dist/src/commands/analytics.js +8 -6
  5. package/dist/src/commands/announcements.js +24 -3
  6. package/dist/src/commands/devSync.d.ts +3 -1
  7. package/dist/src/commands/devSync.js +17 -14
  8. package/dist/src/commands/login.d.ts +83 -0
  9. package/dist/src/commands/login.js +348 -0
  10. package/dist/src/commands/mcp.js +4 -3
  11. package/dist/src/commands/publishApp.d.ts +2 -1
  12. package/dist/src/commands/publishApp.js +10 -7
  13. package/dist/src/commands/sharedLinks.d.ts +0 -2
  14. package/dist/src/commands/sharedLinks.js +10 -20
  15. package/dist/src/commands/withAuth.d.ts +5 -1
  16. package/dist/src/commands/withAuth.js +8 -3
  17. package/dist/src/core/credential.d.ts +4 -16
  18. package/dist/src/core/credential.js +1 -25
  19. package/dist/src/core/http.d.ts +15 -7
  20. package/dist/src/core/http.js +33 -21
  21. package/dist/src/core/kpiTypes.d.ts +9 -0
  22. package/dist/src/core/kpiTypes.js +23 -0
  23. package/dist/src/core/ld.d.ts +3 -2
  24. package/dist/src/core/ld.js +45 -24
  25. package/dist/src/core/pitcherAuth.d.ts +86 -0
  26. package/dist/src/core/pitcherAuth.js +143 -0
  27. package/dist/src/core/session.d.ts +90 -0
  28. package/dist/src/core/session.js +293 -0
  29. package/dist/src/core/target.d.ts +47 -0
  30. package/dist/src/core/target.js +114 -0
  31. package/dist/src/mcp/local.d.ts +6 -0
  32. package/dist/src/mcp/local.js +8 -0
  33. package/dist/src/mcp/serve.d.ts +19 -4
  34. package/dist/src/mcp/serve.js +38 -15
  35. package/dist/src/mcp/server.d.ts +3 -1
  36. package/dist/src/mcp/server.js +2 -6
  37. package/dist/src/mcp/tools/analytics.js +3 -2
  38. package/dist/src/mcp/tools/publishApp.js +3 -3
  39. package/dist/src/mcp/tools/sharedLinks.js +4 -6
  40. package/package.json +11 -6
  41. package/dist/src/core/auth.d.ts +0 -3
  42. package/dist/src/core/auth.js +0 -10
  43. package/dist/src/mcp/bundled.d.ts +0 -4
  44. package/dist/src/mcp/bundled.js +0 -25
  45. package/dist/src/mcp/env.d.ts +0 -7
  46. package/dist/src/mcp/env.js +0 -13
package/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  > Pitcher CLI Tools provide a suite of command-line utilities to streamline your workflow. For the best experience, we recommend installing the tools globally using `npm install -g @pitcher/cli-scripts`.
4
4
 
5
+ Releases are published to npm automatically: a merge to `main` that bumps `version` in `package.json` publishes that version, and a merge without a bump publishes nothing (`.github/workflows/cli-scripts-publish.yml`; release flow in `CLAUDE.md`).
6
+
7
+ > ⚠️ **Breaking changes in 3.0.0**
8
+ >
9
+ > 1. **Local `p mcp` now serves ONLY 3 tools** (`pitcher_dev_sync`, `pitcher_dev_sync_revert`, `pitcher_publish_app`), down from 124. Everything else is the hosted server `https://pitcher-mcp.fly.dev/mcp`. `p mcp` no longer reads `NG_API_KEY`/`NG_ORG`/`NG_URL`/`NG_ENV`; run `p login` once (`--domain` or `PITCHER_DOMAIN` when logged in to several orgs).
10
+ > 2. **Every other command uses your `p login` session.** `--api-key` (automation) still wins when passed; `NG_API_KEY` is used only when you have NO `p login` session, and is otherwise ignored with a notice. `--org` no longer defaults to `dev` on the session path: the session's org is the target. `--domain` cannot be combined with `--api-key` or `--org`.
11
+ > 3. **The Claude Desktop `.mcpb` extension is retired.** Claude Desktop adds the remote URL natively.
12
+ > 4. **`p analytics kpi` requires `--type`** (one of the 12 KPI types listed in `p analytics kpi --help`). The old default `all` always returned a 500.
13
+ > 5. **Node ≥ 22.12** (`engines`).
14
+
5
15
  ## Help
6
16
 
7
17
  ```bash
@@ -10,62 +20,55 @@ p --help
10
20
  p command --help
11
21
  ```
12
22
 
13
- ## MCP server
23
+ ## Sign in (`p login`)
14
24
 
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.
25
+ People sign in with their own Pitcher account; API keys are for automation (CI, deploy scripts).
16
26
 
17
27
  ```bash
18
- p mcp
19
- # or without a global install
20
- npx @pitcher/cli-scripts mcp
28
+ p login --domain acme.my.pitcher.com # opens the browser; --no-browser prints the URL instead
29
+ p whoami # every session: domain, environment, email, token expiry, status
30
+ p logout acme.my.pitcher.com # or: p logout --all
21
31
  ```
22
32
 
23
- ### Environment variables
24
-
25
- Auth and org scope are resolved once at boot from environment variables. They are never tool inputs, so the model cannot change org or credentials mid-session.
33
+ - **`p logout` is local only:** it deletes the session file and makes no Auth0 call. The refresh token is not revoked; it expires on its own. Your hosted MCP connectors (ChatGPT, Claude) are unaffected. A new `p login` over an existing session likewise just replaces the file. (Revoking is avoided on purpose: the CLI and the hosted server share one Auth0 client, and a revoke can delete the whole grant, signing every connector out.)
26
34
 
27
- - `NG_API_KEY` (required, secret): the API key the server authenticates as.
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). 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.
35
+ - **One session per org domain.** Commands pick it with `--domain`, else `PITCHER_DOMAIN`, else the only session. Logged in to several orgs and nothing explicit → an error naming them (never a silent pick).
36
+ - Sessions live in `${XDG_CONFIG_HOME:-~/.config}/pitcher/sessions/<domain>.json` (mode `0600`, directories `0700`). The access token refreshes itself; an expired session asks you to `p login` again.
37
+ - The sign-in listener uses `http://127.0.0.1:53682/callback` only. Over SSH, forward port 53682 and use `--no-browser`.
38
+ - **Precedence:** `--api-key` → your `p login` session → `NG_API_KEY`. With a session, `--org acme` (or `--url https://acme.my.pitcher.com`) selects the `acme` session; a `--url`/`--publish-url` on another host is refused rather than sent your token.
39
+ - CI is unaffected: it passes `--api-key` and has no session.
30
40
 
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
-
33
- ### Host config example (Claude Desktop / `.mcp.json`)
34
-
35
- ```jsonc
36
- "pitcher": {
37
- "command": "npx",
38
- "args": ["-y", "@pitcher/cli-scripts", "mcp"],
39
- "env": { "NG_API_KEY": "<key>", "NG_ORG": "dev" }
40
- }
41
- ```
42
-
43
- ### Tool annotations
41
+ ## MCP server
44
42
 
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 — 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.
43
+ Pitcher's MCP tools come from two places:
47
44
 
48
- ### Desktop Extension (`.mcpb`)
45
+ | Server | Tools | Add it with |
46
+ |---|---|---|
47
+ | **Hosted** (OAuth; Claude Code, Claude Desktop, claude.ai, ChatGPT) | everything: content, canvases, DSRs, analytics, L&D, users, … (121 tools, prompts, resources) | `claude mcp add --transport http pitcher https://pitcher-mcp.fly.dev/mcp` — Claude Desktop: Settings → Connectors → Add custom connector, same URL |
48
+ | **Local** `p mcp` (stdio) | only `pitcher_dev_sync`, `pitcher_dev_sync_revert`, `pitcher_publish_app` (they zip a folder on YOUR machine) | `claude mcp add pitcher-local -- p mcp` |
49
49
 
50
- For one-click install in Claude Desktop (name + icon, an install-time API-key/org form, no hand-edited config, no Node prerequisite), build a `.mcpb` bundle:
50
+ Claude Code users add both. The local server runs on your `p login` session (no API key, no env block); the domain is fixed when it starts. Signed in to several orgs? Add one entry per org:
51
51
 
52
- ```bash
53
- yarn mcpb:pack # → pitcher.mcpb (validated + packed)
52
+ ```jsonc
53
+ "pitcher-local-acme": { "command": "p", "args": ["mcp", "--domain", "acme.my.pitcher.com"] }
54
54
  ```
55
55
 
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
-
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`.
56
+ - Auth and org are pinned when the server starts and are never tool inputs. The session token is refreshed before each tool call; after `p logout` the tools answer AUTH until you `p login` and restart the server.
57
+ - `pitcher_publish_app` is always marked `destructiveHint`. `pitcher_dev_sync`/`_revert` get it unless the session's org is a test org (an unknown environment counts as production).
58
+ - `dev-sync` is a one-shot sync as a tool; the continuous `watch` mode is CLI-only.
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
 
62
62
  ## To run the `publish-app` command:
63
63
 
64
64
  ```bash
65
- # Basic usage
65
+ # Signed in with p login: publishes to your session's org
66
+ p publish-app <folder>
67
+
68
+ # Automation (CI): an API key; --org picks the org
66
69
  p publish-app <folder> --org pitcher --api-key myapikey
67
70
 
68
- # Publish to current folder and default org (dev)
71
+ # With an API key and no --org, the org is dev
69
72
  p publish-app --api-key myapikey
70
73
 
71
74
  # Publish to a specific organization subdomain (e.g., pitcher.my.pitcher.com)
@@ -103,11 +106,11 @@ p publish-app --org pitcher --increment-version --install-to-env-name Production
103
106
  Sync a marketplace app directly to S3 for live development testing without publishing. This is useful for testing app changes in a pre-production environment.
104
107
 
105
108
  ```bash
106
- # One-time sync from dist folder
107
- p dev-sync sync --file-id <pitcher_file_id> --api-key myapikey
109
+ # One-time sync from dist folder (on your p login session)
110
+ p dev-sync sync --file-id <pitcher_file_id>
108
111
 
109
- # Sync to a specific organization (default: dev)
110
- p dev-sync sync --file-id <pitcher_file_id> --org pitcher
112
+ # Signed in to several orgs: pick one
113
+ p dev-sync sync --file-id <pitcher_file_id> --domain pitcher.my.pitcher.com
111
114
 
112
115
  # Watch mode - auto-sync on file changes (reverts on Ctrl+C)
113
116
  p dev-sync watch --file-id <pitcher_file_id> --api-key myapikey
@@ -223,15 +226,12 @@ p canvases get 01CANVASID... --api-key myapikey
223
226
 
224
227
  ## To run the `shared-links` command:
225
228
 
226
- Create and read share links for canvases or files (returns the `short_url`).
229
+ Create and read share links for canvases (returns the `short_url`).
227
230
 
228
231
  ```bash
229
232
  # Create a share link for a canvas
230
233
  p shared-links create --canvas-id 01CANVASID... --api-key myapikey
231
234
 
232
- # Create a share link for a file
233
- p shared-links create --file-id 01FILEID... --api-key myapikey
234
-
235
235
  # Fetch the existing share link for a canvas
236
236
  p shared-links get --canvas-id 01CANVASID... --api-key myapikey
237
237
  ```
@@ -352,7 +352,7 @@ p org get --api-key myapikey
352
352
 
353
353
  ## To run the `analytics` command:
354
354
 
355
- Read rollup analytics totals (`org`/`instance`), KPI metrics, and content/user engagement analytics. `org`/`instance` are Django rollups; `kpi` and the 7 verbs below hit the **clickhouse-api** service (`clickhouse.pitcher.com`) via the same env resolution as everything else — `--org`/`--url` pick the environment (routed via `x-original-domain`). Access **follows the key user's real role**: `popularity`, `users`, `files`, and `canvases` need an **org-admin** key; `engagement`, `viewers`, and `sessions` work with a member key. `kpi` and `sessions` return the raw payload; the other row-heavy verbs return a shaped envelope (`summary`, `rows`, `total_count`, `returned`, `has_more`, and sometimes `truncated`/`warnings`) capped by `--limit` (default 20, max 100). Date-range verbs default to a **30-day window** when `--start-date`/`--end-date` are omitted. `viewers` and `sessions` require `--canvas-id`; `sessions` also requires `--user`.
355
+ Read rollup analytics totals (`org`/`instance`), KPI metrics, and content/user engagement analytics. `org`/`instance` are Django rollups; `kpi` and the 7 verbs below hit the **clickhouse-api** service (`clickhouse.pitcher.com`) via the same env resolution as everything else — `--org`/`--url` pick the environment (routed via `x-original-domain`). Access **follows the signed-in user's own role** (or the key user's, with an API key): `popularity`, `users`, `files`, and `canvases` need an **org-admin** key; `engagement`, `viewers`, and `sessions` work with a member key. `kpi` and `sessions` return the raw payload; the other row-heavy verbs return a shaped envelope (`summary`, `rows`, `total_count`, `returned`, `has_more`, and sometimes `truncated`/`warnings`) capped by `--limit` (default 20, max 100). Date-range verbs default to a **30-day window** when `--start-date`/`--end-date` are omitted. `viewers` and `sessions` require `--canvas-id`; `sessions` also requires `--user`.
356
356
 
357
357
  ```bash
358
358
  # Organization-wide rollups (org-admin key required)
@@ -362,7 +362,7 @@ p analytics org --api-key myapikey
362
362
  p analytics instance --instance-id 01... --api-key myapikey
363
363
 
364
364
  # KPI metrics from the clickhouse analytics service (PIT-7647).
365
- # Access follows the key user's real role on the instance; --org/--url pick the
365
+ # Access follows the signed-in user's own role on the instance; --org/--url pick the
366
366
  # env exactly as for other commands (they route via x-original-domain).
367
367
  # Activity metrics need a date window; inventory counts (total-members, ...) do not.
368
368
  p analytics kpi --instance-id 01... --type total-members --api-key myapikey
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import { createMetadataTemplateCommand } from './src/commands/metadataTemplate.j
27
27
  import { createDsrCommand } from './src/commands/dsr.js';
28
28
  import { createLdCommand } from './src/commands/ld.js';
29
29
  import { createMcpCommand } from './src/commands/mcp.js';
30
+ import { createLoginCommand, createLogoutCommand, createWhoamiCommand } from './src/commands/login.js';
30
31
  const __filename = fileURLToPath(import.meta.url);
31
32
  const __dirname = dirname(__filename);
32
33
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -61,4 +62,7 @@ program.addCommand(createLdCommand());
61
62
  program.addCommand(createOrgCommand());
62
63
  program.addCommand(createAnalyticsCommand());
63
64
  program.addCommand(createMcpCommand());
65
+ program.addCommand(createLoginCommand());
66
+ program.addCommand(createLogoutCommand());
67
+ program.addCommand(createWhoamiCommand());
64
68
  program.parseAsync(process.argv);
@@ -22,9 +22,9 @@ export interface AnalyticsKpiOpts extends CommonOpts {
22
22
  * org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
23
23
  * org api-key; the caller inherits the key user's real role. Dates are required
24
24
  * by the server for activity-metric types (inventory counts like
25
- * `total-members` need none) — that validation stays server-side, as does the
26
- * set of valid `type` values (a bad one comes back as a 400 listing them). There
27
- * is no aggregate `all` type, so `--type` is required.
25
+ * `total-members` need none) — that validation stays server-side. `--type` is
26
+ * required and checked against `KPI_TYPES` before any request (there is no
27
+ * aggregate `all` type; clickhouse-api also 400s a missing or unknown one).
28
28
  */
29
29
  export declare function runAnalyticsKpi(opts: AnalyticsKpiOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
30
30
  export interface AnalyticsEngagementOpts extends CommonOpts {
@@ -4,6 +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 { withAuthFlags } from './withAuth.js';
7
+ import { isKpiType, KPI_TYPES } from '../core/kpiTypes.js';
7
8
  import { shapeAnalytics, pageSizeFor, withDefaultWindow, todayIso } from '../core/analytics-shape.js';
8
9
  export async function runAnalyticsOrg(opts, deps = {}) {
9
10
  try {
@@ -32,16 +33,17 @@ export async function runAnalyticsInstance(opts, deps = {}) {
32
33
  * org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
33
34
  * org api-key; the caller inherits the key user's real role. Dates are required
34
35
  * by the server for activity-metric types (inventory counts like
35
- * `total-members` need none) — that validation stays server-side, as does the
36
- * set of valid `type` values (a bad one comes back as a 400 listing them). There
37
- * is no aggregate `all` type, so `--type` is required.
36
+ * `total-members` need none) — that validation stays server-side. `--type` is
37
+ * required and checked against `KPI_TYPES` before any request (there is no
38
+ * aggregate `all` type; clickhouse-api also 400s a missing or unknown one).
38
39
  */
39
40
  export async function runAnalyticsKpi(opts, deps = {}) {
40
41
  try {
41
42
  if (!opts.instanceId)
42
43
  throw createCliError('VALIDATION', '--instance-id is required for analytics kpi');
43
- if (!opts.type)
44
- throw createCliError('VALIDATION', '--type is required for analytics kpi');
44
+ if (!opts.type || !isKpiType(opts.type)) {
45
+ throw createCliError('VALIDATION', `--type is required for analytics kpi, one of: ${KPI_TYPES.join(', ')}`);
46
+ }
45
47
  const client = clickhouseClientFor(opts, deps);
46
48
  const params = {
47
49
  instance_id: opts.instanceId,
@@ -380,7 +382,7 @@ export function createAnalyticsCommand() {
380
382
  });
381
383
  withAuthFlags(cmd.command('kpi'))
382
384
  .option('--instance-id <id>', 'Instance ID (required)')
383
- .option('--type <type>', 'KPI type, e.g. total-members, total-active-users (required)')
385
+ .option('--type <type>', `KPI type (required), one of: ${KPI_TYPES.join(', ')}`)
384
386
  .option('--start-date <iso>', 'Window start (ISO date; required by the server for activity metrics)')
385
387
  .option('--end-date <iso>', 'Window end (ISO date; required by the server for activity metrics)')
386
388
  .option('--group-ids <csv>', 'Comma-separated distribution-group IDs to scope by')
@@ -70,9 +70,30 @@ export async function runAnnouncementsCreate(opts, deps = {}) {
70
70
  throw createCliError('VALIDATION', 'announcement requires non-empty string title and body (via --from-file or --title/--body)');
71
71
  }
72
72
  const client = clientFor(opts, deps);
73
- // Deliberately omit `user_id`/`no_user_id`: omitting user_id yields the
74
- // desired instance-shared row (see design §4.3); there is no --owned flag.
75
- const res = await client.post(APPSDB_ENDPOINT, { type: ANNOUNCEMENT_TYPE, instance_id: opts.instanceId, data }, { params: { instance_id: opts.instanceId }, headers: { 'x-instance-id': opts.instanceId } });
73
+ const scope = { params: { instance_id: opts.instanceId }, headers: { 'x-instance-id': opts.instanceId } };
74
+ // An announcement must be an instance-SHARED row (user_id null), or reps cannot read it. An API key gets
75
+ // that by omitting user_id, but a user's JWT stamps the author unless an instance admin sends
76
+ // `no_user_id: true` (next-core appsdb/ownership.ts). The flag is harmless on the key path.
77
+ const res = await client.post(APPSDB_ENDPOINT, { type: ANNOUNCEMENT_TYPE, instance_id: opts.instanceId, data, no_user_id: true }, scope);
78
+ // A non-admin caller still gets the row created, user-owned (the server only logs a warning): remove it
79
+ // best-effort and fail, because it would be visible only to its author.
80
+ // The route always RETURNs user_id (appsdb/route.ts), so anything but null is a user-owned row.
81
+ const row = (res.data ?? {});
82
+ if (row.user_id !== null) {
83
+ const id = String(row.id);
84
+ let removed = false;
85
+ try {
86
+ await client.delete(`${APPSDB_ENDPOINT}/${encodeURIComponent(id)}`, scope);
87
+ removed = true;
88
+ }
89
+ catch {
90
+ removed = false;
91
+ }
92
+ throw createCliError('AUTH', 'Only instance admins can publish announcements (the row would have been visible only to you).' +
93
+ (removed
94
+ ? ''
95
+ : ` An unshared announcement row ${id} was created and could not be removed; delete it in appsDB by hand.`));
96
+ }
76
97
  return ok(res.data);
77
98
  }
78
99
  catch (e) {
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import type { AxiosInstance } from 'axios';
3
- import { type Credential } from '../core/credential.js';
3
+ import type { Credential } from '../core/credential.js';
4
4
  import { type Envelope } from '../core/envelope.js';
5
5
  interface AppJson {
6
6
  name: string;
@@ -19,7 +19,9 @@ interface DevSyncOptions {
19
19
  fileId: string;
20
20
  apiKey?: string;
21
21
  credential?: Credential;
22
+ domain?: string;
22
23
  org?: string;
24
+ url?: string;
23
25
  quiet?: boolean;
24
26
  output?: string;
25
27
  }
@@ -3,8 +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 { credentialOf } from '../core/credential.js';
7
- import { resolveBaseUrl } from '../core/baseUrl.js';
6
+ import { resolveRequestTarget } from '../core/target.js';
8
7
  import { makeClient, toCliError } from '../core/http.js';
9
8
  import { zipDirectoryToBuffer } from '../core/archive.js';
10
9
  import { ok, fail } from '../core/envelope.js';
@@ -32,11 +31,10 @@ export function readAppJson(distPath) {
32
31
  }
33
32
  export async function runDevSync(opts, deps = {}) {
34
33
  try {
35
- const cred = credentialOf(opts);
36
- const baseUrl = resolveBaseUrl(opts);
34
+ const target = resolveRequestTarget(opts);
37
35
  const distPath = findDistPath(deps.cwd);
38
36
  const appJson = readAppJson(distPath);
39
- const client = deps.client ?? makeClient(baseUrl, cred);
37
+ const client = deps.client ?? makeClient(target.baseUrl, target.source);
40
38
  log(`Syncing ${appJson.name} from ${distPath}...`, { quiet: opts.quiet });
41
39
  const zip = await zipDirectoryToBuffer(distPath);
42
40
  log(`Zip created: ${(zip.length / 1024).toFixed(2)} KB`, { quiet: opts.quiet });
@@ -57,9 +55,8 @@ export async function runDevSync(opts, deps = {}) {
57
55
  }
58
56
  export async function runRevert(opts, deps = {}) {
59
57
  try {
60
- const cred = credentialOf(opts);
61
- const baseUrl = resolveBaseUrl(opts);
62
- const client = deps.client ?? makeClient(baseUrl, cred);
58
+ const target = resolveRequestTarget(opts);
59
+ const client = deps.client ?? makeClient(target.baseUrl, target.source);
63
60
  const res = await client.delete(SYNC_ENDPOINT, {
64
61
  headers: { 'Content-Type': 'application/json' },
65
62
  data: { file_id: opts.fileId },
@@ -142,13 +139,15 @@ export function createDevSyncCommand() {
142
139
  .command('sync')
143
140
  .description('One-time sync of the app to S3')
144
141
  .requiredOption('--file-id <id>', 'File ID of the installed app (pitcher_file.id)')
145
- .option('--api-key <apiKey>', 'API Key for authentication')
146
- .option('--org <organization>', 'Organization subdomain', 'dev')
142
+ .option('--api-key <apiKey>', 'API key (CI/automation only; people use `p login`)')
143
+ .option('--domain <domain>', 'p login session to use, e.g. acme.my.pitcher.com (see p whoami)')
144
+ .option('--org <organization>', 'Organization subdomain (with --api-key: default dev; with a session: picks it)')
147
145
  .action(async (options, command) => {
148
146
  const globals = command.optsWithGlobals();
149
147
  const env = await runDevSync({
150
148
  fileId: options.fileId,
151
149
  apiKey: options.apiKey,
150
+ domain: options.domain,
152
151
  org: options.org,
153
152
  quiet: globals.quiet,
154
153
  });
@@ -158,14 +157,16 @@ export function createDevSyncCommand() {
158
157
  .command('watch')
159
158
  .description('Watch the dist folder and sync on changes')
160
159
  .requiredOption('--file-id <id>', 'File ID of the installed app (pitcher_file.id)')
161
- .option('--api-key <apiKey>', 'API Key for authentication')
162
- .option('--org <organization>', 'Organization subdomain', 'dev')
160
+ .option('--api-key <apiKey>', 'API key (CI/automation only; people use `p login`)')
161
+ .option('--domain <domain>', 'p login session to use, e.g. acme.my.pitcher.com (see p whoami)')
162
+ .option('--org <organization>', 'Organization subdomain (with --api-key: default dev; with a session: picks it)')
163
163
  .option('--debounce <ms>', 'Debounce time in milliseconds', '2500')
164
164
  .action(async (options, command) => {
165
165
  const globals = command.optsWithGlobals();
166
166
  await watchDevSync({
167
167
  fileId: options.fileId,
168
168
  apiKey: options.apiKey,
169
+ domain: options.domain,
169
170
  org: options.org,
170
171
  quiet: globals.quiet,
171
172
  output: globals.output,
@@ -175,13 +176,15 @@ export function createDevSyncCommand() {
175
176
  .command('revert')
176
177
  .description('Revert the dev override and restore the original version')
177
178
  .requiredOption('--file-id <id>', 'File ID of the installed app (pitcher_file.id)')
178
- .option('--api-key <apiKey>', 'API Key for authentication')
179
- .option('--org <organization>', 'Organization subdomain', 'dev')
179
+ .option('--api-key <apiKey>', 'API key (CI/automation only; people use `p login`)')
180
+ .option('--domain <domain>', 'p login session to use, e.g. acme.my.pitcher.com (see p whoami)')
181
+ .option('--org <organization>', 'Organization subdomain (with --api-key: default dev; with a session: picks it)')
180
182
  .action(async (options, command) => {
181
183
  const globals = command.optsWithGlobals();
182
184
  const env = await runRevert({
183
185
  fileId: options.fileId,
184
186
  apiKey: options.apiKey,
187
+ domain: options.domain,
185
188
  org: options.org,
186
189
  quiet: globals.quiet,
187
190
  });
@@ -0,0 +1,83 @@
1
+ import type { AddressInfo } from 'node:net';
2
+ import { Command } from 'commander';
3
+ import type { Credential } from '../core/credential.js';
4
+ import { type Envelope } from '../core/envelope.js';
5
+ import { type FetchFn } from '../core/pitcherAuth.js';
6
+ import { type SessionCredentialOpts } from '../core/session.js';
7
+ /** The loopback redirect registered on both Auth0 Native apps (`http://127.0.0.1:53682/callback`). */
8
+ export declare const LOGIN_PORT = 53682;
9
+ export interface LoginDeps {
10
+ fetch?: FetchFn;
11
+ /** Open the authorize URL. Default: `browserCommand` (`open` / `xdg-open` / `rundll32 url.dll`). */
12
+ openBrowser?: (url: string) => void;
13
+ /** Ask for the domain on a TTY. Default: readline on stderr. */
14
+ prompt?: (question: string) => Promise<string>;
15
+ isTty?: boolean;
16
+ /** Test seam: 0 = an ephemeral port (the redirect URI follows the bound port). */
17
+ port?: number;
18
+ timeoutMs?: number;
19
+ now?: () => number;
20
+ env?: NodeJS.ProcessEnv;
21
+ /** Test seam: observe the bound address. */
22
+ onListen?: (address: AddressInfo) => void;
23
+ quiet?: boolean;
24
+ }
25
+ export interface LoginResult {
26
+ domain: string;
27
+ environment?: string;
28
+ email?: string;
29
+ sub: string;
30
+ expiresAt: number;
31
+ }
32
+ /**
33
+ * The URL-opening command per platform. Windows uses the URL handler directly: through `cmd /c start`,
34
+ * cmd.exe would split the unquoted URL at its first `&` and drop client_id, state and the PKCE challenge.
35
+ */
36
+ export declare function browserCommand(url: string, platform: NodeJS.Platform): [string, string[]];
37
+ /** `GET /api/v1/users/me/` with the bearer: proves the token works on Django and yields the email. */
38
+ export declare function fetchMe(domain: string, cred: Credential, doFetch: FetchFn): Promise<{
39
+ email?: string;
40
+ }>;
41
+ export declare function runLogin(opts: {
42
+ domain?: string;
43
+ browser?: boolean;
44
+ }, deps?: LoginDeps): Promise<Envelope<LoginResult>>;
45
+ export interface LogoutDeps {
46
+ fetch?: FetchFn;
47
+ env?: NodeJS.ProcessEnv;
48
+ quiet?: boolean;
49
+ }
50
+ export declare function runLogout(opts: {
51
+ domain?: string;
52
+ all?: boolean;
53
+ }, deps?: LogoutDeps): Promise<Envelope<{
54
+ loggedOut: string[];
55
+ }>>;
56
+ export interface WhoamiRow {
57
+ domain: string;
58
+ environment?: string;
59
+ email?: string;
60
+ /** access-token expiry, epoch seconds */
61
+ expiresAt?: number;
62
+ status: string;
63
+ }
64
+ export interface WhoamiDeps {
65
+ fetch?: FetchFn;
66
+ env?: NodeJS.ProcessEnv;
67
+ session?: Omit<SessionCredentialOpts, 'env'>;
68
+ }
69
+ /**
70
+ * One whoami row's status, matched on the specific failure (never "any AUTH"): only a session Auth0 or Django
71
+ * no longer accepts is `expired`; a held lock is `busy`; a file we refuse to read is `invalid session file`;
72
+ * anything else (network, 5xx) is `unreachable`.
73
+ */
74
+ export declare function whoamiStatus(domain: string, e: unknown): string;
75
+ /** Never reads `--api-key`/`NG_API_KEY`; never returns a token. One bad session never fails the listing. */
76
+ export declare function runWhoami(opts: {
77
+ domain?: string;
78
+ }, deps?: WhoamiDeps): Promise<Envelope<{
79
+ sessions: WhoamiRow[];
80
+ }>>;
81
+ export declare function createLoginCommand(): Command;
82
+ export declare function createLogoutCommand(): Command;
83
+ export declare function createWhoamiCommand(): Command;