@pitcher/cli-scripts 2.21.0 → 3.0.1

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 (45) hide show
  1. package/README.md +42 -41
  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 +26 -15
  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/withAuth.d.ts +5 -1
  14. package/dist/src/commands/withAuth.js +8 -3
  15. package/dist/src/core/archive.d.ts +2 -1
  16. package/dist/src/core/archive.js +3 -2
  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/package.json +11 -6
  40. package/dist/src/core/auth.d.ts +0 -3
  41. package/dist/src/core/auth.js +0 -10
  42. package/dist/src/mcp/bundled.d.ts +0 -4
  43. package/dist/src/mcp/bundled.js +0 -25
  44. package/dist/src/mcp/env.d.ts +0 -7
  45. package/dist/src/mcp/env.js +0 -13
package/README.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
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
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
+
7
15
  ## Help
8
16
 
9
17
  ```bash
@@ -12,62 +20,55 @@ p --help
12
20
  p command --help
13
21
  ```
14
22
 
15
- ## MCP server
23
+ ## Sign in (`p login`)
16
24
 
17
- 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).
18
26
 
19
27
  ```bash
20
- p mcp
21
- # or without a global install
22
- 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
23
31
  ```
24
32
 
25
- ### Environment variables
26
-
27
- 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.
28
-
29
- - `NG_API_KEY` (required, secret): the API key the server authenticates as.
30
- - `NG_ORG` (default `dev`): organization subdomain, or set `NG_URL` directly for a full origin.
31
- - `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.
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.)
32
34
 
33
- 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.
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.
34
40
 
35
- ### Host config example (Claude Desktop / `.mcp.json`)
36
-
37
- ```jsonc
38
- "pitcher": {
39
- "command": "npx",
40
- "args": ["-y", "@pitcher/cli-scripts", "mcp"],
41
- "env": { "NG_API_KEY": "<key>", "NG_ORG": "dev" }
42
- }
43
- ```
44
-
45
- ### Tool annotations
41
+ ## MCP server
46
42
 
47
- - Read commands are marked `readOnlyHint`. `dev-sync` counts as a write (it mutates remote dev-override state).
48
- - `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:
49
44
 
50
- ### 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` |
51
49
 
52
- 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:
53
51
 
54
- ```bash
55
- yarn mcpb:pack # → pitcher.mcpb (validated + packed)
52
+ ```jsonc
53
+ "pitcher-local-acme": { "command": "p", "args": ["mcp", "--domain", "acme.my.pitcher.com"] }
56
54
  ```
57
55
 
58
- 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.
59
-
60
- 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.
61
59
 
62
60
  This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the `pitcher-apps` skill, not this README.
63
61
 
64
62
  ## To run the `publish-app` command:
65
63
 
66
64
  ```bash
67
- # 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
68
69
  p publish-app <folder> --org pitcher --api-key myapikey
69
70
 
70
- # Publish to current folder and default org (dev)
71
+ # With an API key and no --org, the org is dev
71
72
  p publish-app --api-key myapikey
72
73
 
73
74
  # Publish to a specific organization subdomain (e.g., pitcher.my.pitcher.com)
@@ -105,11 +106,11 @@ p publish-app --org pitcher --increment-version --install-to-env-name Production
105
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.
106
107
 
107
108
  ```bash
108
- # One-time sync from dist folder
109
- 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>
110
111
 
111
- # Sync to a specific organization (default: dev)
112
- 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
113
114
 
114
115
  # Watch mode - auto-sync on file changes (reverts on Ctrl+C)
115
116
  p dev-sync watch --file-id <pitcher_file_id> --api-key myapikey
@@ -351,7 +352,7 @@ p org get --api-key myapikey
351
352
 
352
353
  ## To run the `analytics` command:
353
354
 
354
- 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`.
355
356
 
356
357
  ```bash
357
358
  # Organization-wide rollups (org-admin key required)
@@ -361,7 +362,7 @@ p analytics org --api-key myapikey
361
362
  p analytics instance --instance-id 01... --api-key myapikey
362
363
 
363
364
  # KPI metrics from the clickhouse analytics service (PIT-7647).
364
- # 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
365
366
  # env exactly as for other commands (they route via x-original-domain).
366
367
  # Activity metrics need a date window; inventory counts (total-members, ...) do not.
367
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,13 +31,20 @@ 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
- const zip = await zipDirectoryToBuffer(distPath);
39
+ // node_modules is never part of an app, and zipping it means uploading tens of thousands of files.
40
+ let hasNodeModules = false;
41
+ const zip = await zipDirectoryToBuffer(distPath, (name) => {
42
+ const isInNodeModules = name.split(/[\\/]/).includes('node_modules');
43
+ hasNodeModules || (hasNodeModules = isInNodeModules);
44
+ return isInNodeModules;
45
+ });
46
+ if (hasNodeModules)
47
+ log('Left node_modules out of the zip', { quiet: opts.quiet });
42
48
  log(`Zip created: ${(zip.length / 1024).toFixed(2)} KB`, { quiet: opts.quiet });
43
49
  const form = new FormData();
44
50
  form.append('app_name', appJson.name);
@@ -57,9 +63,8 @@ export async function runDevSync(opts, deps = {}) {
57
63
  }
58
64
  export async function runRevert(opts, deps = {}) {
59
65
  try {
60
- const cred = credentialOf(opts);
61
- const baseUrl = resolveBaseUrl(opts);
62
- const client = deps.client ?? makeClient(baseUrl, cred);
66
+ const target = resolveRequestTarget(opts);
67
+ const client = deps.client ?? makeClient(target.baseUrl, target.source);
63
68
  const res = await client.delete(SYNC_ENDPOINT, {
64
69
  headers: { 'Content-Type': 'application/json' },
65
70
  data: { file_id: opts.fileId },
@@ -142,13 +147,15 @@ export function createDevSyncCommand() {
142
147
  .command('sync')
143
148
  .description('One-time sync of the app to S3')
144
149
  .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')
150
+ .option('--api-key <apiKey>', 'API key (CI/automation only; people use `p login`)')
151
+ .option('--domain <domain>', 'p login session to use, e.g. acme.my.pitcher.com (see p whoami)')
152
+ .option('--org <organization>', 'Organization subdomain (with --api-key: default dev; with a session: picks it)')
147
153
  .action(async (options, command) => {
148
154
  const globals = command.optsWithGlobals();
149
155
  const env = await runDevSync({
150
156
  fileId: options.fileId,
151
157
  apiKey: options.apiKey,
158
+ domain: options.domain,
152
159
  org: options.org,
153
160
  quiet: globals.quiet,
154
161
  });
@@ -158,14 +165,16 @@ export function createDevSyncCommand() {
158
165
  .command('watch')
159
166
  .description('Watch the dist folder and sync on changes')
160
167
  .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')
168
+ .option('--api-key <apiKey>', 'API key (CI/automation only; people use `p login`)')
169
+ .option('--domain <domain>', 'p login session to use, e.g. acme.my.pitcher.com (see p whoami)')
170
+ .option('--org <organization>', 'Organization subdomain (with --api-key: default dev; with a session: picks it)')
163
171
  .option('--debounce <ms>', 'Debounce time in milliseconds', '2500')
164
172
  .action(async (options, command) => {
165
173
  const globals = command.optsWithGlobals();
166
174
  await watchDevSync({
167
175
  fileId: options.fileId,
168
176
  apiKey: options.apiKey,
177
+ domain: options.domain,
169
178
  org: options.org,
170
179
  quiet: globals.quiet,
171
180
  output: globals.output,
@@ -175,13 +184,15 @@ export function createDevSyncCommand() {
175
184
  .command('revert')
176
185
  .description('Revert the dev override and restore the original version')
177
186
  .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')
187
+ .option('--api-key <apiKey>', 'API key (CI/automation only; people use `p login`)')
188
+ .option('--domain <domain>', 'p login session to use, e.g. acme.my.pitcher.com (see p whoami)')
189
+ .option('--org <organization>', 'Organization subdomain (with --api-key: default dev; with a session: picks it)')
180
190
  .action(async (options, command) => {
181
191
  const globals = command.optsWithGlobals();
182
192
  const env = await runRevert({
183
193
  fileId: options.fileId,
184
194
  apiKey: options.apiKey,
195
+ domain: options.domain,
185
196
  org: options.org,
186
197
  quiet: globals.quiet,
187
198
  });
@@ -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;