@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.
- package/README.md +45 -45
- package/dist/index.js +4 -0
- package/dist/src/commands/analytics.d.ts +3 -3
- package/dist/src/commands/analytics.js +8 -6
- package/dist/src/commands/announcements.js +24 -3
- package/dist/src/commands/devSync.d.ts +3 -1
- package/dist/src/commands/devSync.js +17 -14
- package/dist/src/commands/login.d.ts +83 -0
- package/dist/src/commands/login.js +348 -0
- package/dist/src/commands/mcp.js +4 -3
- package/dist/src/commands/publishApp.d.ts +2 -1
- package/dist/src/commands/publishApp.js +10 -7
- package/dist/src/commands/sharedLinks.d.ts +0 -2
- package/dist/src/commands/sharedLinks.js +10 -20
- package/dist/src/commands/withAuth.d.ts +5 -1
- package/dist/src/commands/withAuth.js +8 -3
- package/dist/src/core/credential.d.ts +4 -16
- package/dist/src/core/credential.js +1 -25
- package/dist/src/core/http.d.ts +15 -7
- package/dist/src/core/http.js +33 -21
- package/dist/src/core/kpiTypes.d.ts +9 -0
- package/dist/src/core/kpiTypes.js +23 -0
- package/dist/src/core/ld.d.ts +3 -2
- package/dist/src/core/ld.js +45 -24
- package/dist/src/core/pitcherAuth.d.ts +86 -0
- package/dist/src/core/pitcherAuth.js +143 -0
- package/dist/src/core/session.d.ts +90 -0
- package/dist/src/core/session.js +293 -0
- package/dist/src/core/target.d.ts +47 -0
- package/dist/src/core/target.js +114 -0
- package/dist/src/mcp/local.d.ts +6 -0
- package/dist/src/mcp/local.js +8 -0
- package/dist/src/mcp/serve.d.ts +19 -4
- package/dist/src/mcp/serve.js +38 -15
- package/dist/src/mcp/server.d.ts +3 -1
- package/dist/src/mcp/server.js +2 -6
- package/dist/src/mcp/tools/analytics.js +3 -2
- package/dist/src/mcp/tools/publishApp.js +3 -3
- package/dist/src/mcp/tools/sharedLinks.js +4 -6
- package/package.json +11 -6
- package/dist/src/core/auth.d.ts +0 -3
- package/dist/src/core/auth.js +0 -10
- package/dist/src/mcp/bundled.d.ts +0 -4
- package/dist/src/mcp/bundled.js +0 -25
- package/dist/src/mcp/env.d.ts +0 -7
- 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
|
-
##
|
|
23
|
+
## Sign in (`p login`)
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
People sign in with their own Pitcher account; API keys are for automation (CI, deploy scripts).
|
|
16
26
|
|
|
17
27
|
```bash
|
|
18
|
-
p
|
|
19
|
-
#
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
28
|
-
- `
|
|
29
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
53
|
-
|
|
52
|
+
```jsonc
|
|
53
|
+
"pitcher-local-acme": { "command": "p", "args": ["mcp", "--domain", "acme.my.pitcher.com"] }
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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>
|
|
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
|
-
#
|
|
110
|
-
p dev-sync sync --file-id <pitcher_file_id> --
|
|
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
|
|
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
|
|
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
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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',
|
|
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>',
|
|
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
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
61
|
-
const
|
|
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
|
|
146
|
-
.option('--
|
|
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
|
|
162
|
-
.option('--
|
|
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
|
|
179
|
-
.option('--
|
|
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;
|