@phnx-labs/agents-cli 1.22.44 → 1.22.46

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 (39) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +24 -0
  3. package/dist/bootstrap.js +1 -6
  4. package/dist/cli/command-registry.d.ts +0 -1
  5. package/dist/cli/command-registry.js +1 -3
  6. package/dist/commands/accounts.d.ts +1 -9
  7. package/dist/commands/accounts.js +12 -90
  8. package/dist/commands/auth.d.ts +0 -7
  9. package/dist/commands/auth.js +198 -83
  10. package/dist/commands/insights.js +82 -154
  11. package/dist/commands/view.js +1 -1
  12. package/dist/lib/accounting/usage.d.ts +22 -3
  13. package/dist/lib/accounting/usage.js +94 -12
  14. package/dist/lib/agent-spec/agents.js +6 -1
  15. package/dist/lib/cli-resources.js +17 -15
  16. package/dist/lib/devices/harness-inventory.js +20 -3
  17. package/dist/lib/exec.d.ts +20 -0
  18. package/dist/lib/exec.js +43 -6
  19. package/dist/lib/identity/client.d.ts +53 -0
  20. package/dist/lib/identity/client.js +106 -0
  21. package/dist/lib/identity/index.d.ts +115 -0
  22. package/dist/lib/identity/index.js +82 -0
  23. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  24. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
  25. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  26. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  27. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  28. package/dist/lib/probe.d.ts +8 -0
  29. package/dist/lib/probe.js +105 -0
  30. package/dist/lib/startup/command-registry.d.ts +3 -1
  31. package/dist/lib/startup/command-registry.js +5 -2
  32. package/dist/lib/view-types.d.ts +2 -2
  33. package/package.json +1 -1
  34. package/dist/commands/org.d.ts +0 -11
  35. package/dist/commands/org.js +0 -228
  36. package/dist/lib/entitlement.d.ts +0 -31
  37. package/dist/lib/entitlement.js +0 -137
  38. package/dist/lib/prix-account.d.ts +0 -159
  39. package/dist/lib/prix-account.js +0 -215
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Phoenix ID — the typed surface commands use. Every route the account backend
3
+ * exposes is a function here; no command builds a URL or reads a token itself.
4
+ */
5
+ import { type PhoenixSession } from './client.js';
6
+ export { PHOENIX_ID_BASE, PhoenixApiError, clearSession, readSession, sessionFilePath, writeSession, type PhoenixSession, } from './client.js';
7
+ export interface DeviceAuthorization {
8
+ device_code: string;
9
+ user_code: string;
10
+ verification_uri: string;
11
+ verification_uri_complete: string;
12
+ expires_in: number;
13
+ interval: number;
14
+ }
15
+ export interface WhoAmI {
16
+ userId: string;
17
+ email: string;
18
+ valid: true;
19
+ }
20
+ /**
21
+ * RFC 8628 poll outcomes. `pending` and `slow_down` are normal states of a
22
+ * login in progress, not failures — the server signals them through the error
23
+ * body, and this is where that wire detail stops.
24
+ */
25
+ export type DevicePoll = {
26
+ status: 'authorized';
27
+ access_token: string;
28
+ user: {
29
+ email: string;
30
+ id: string;
31
+ };
32
+ } | {
33
+ status: 'pending';
34
+ } | {
35
+ status: 'slow_down';
36
+ } | {
37
+ status: 'expired';
38
+ } | {
39
+ status: 'denied';
40
+ };
41
+ export declare function startDeviceAuthorization(): Promise<DeviceAuthorization>;
42
+ export declare function pollDeviceToken(deviceCode: string): Promise<DevicePoll>;
43
+ export declare function fetchWhoAmI(token?: string): Promise<WhoAmI>;
44
+ export interface SpaceSummary {
45
+ id: string;
46
+ slug: string;
47
+ name: string;
48
+ organization_id: string | null;
49
+ owner_user_id: string;
50
+ invite_code?: string;
51
+ user_role: 'owner' | 'admin' | 'member';
52
+ created_at: string;
53
+ }
54
+ export interface SpaceMember {
55
+ user_id: string;
56
+ email: string;
57
+ name?: string;
58
+ avatar_url?: string;
59
+ role: 'owner' | 'admin' | 'member';
60
+ joined_at: string;
61
+ }
62
+ export interface SpaceInvite {
63
+ id: string;
64
+ space_id: string;
65
+ email: string;
66
+ role: 'admin' | 'member';
67
+ invite_code: string;
68
+ created_at: string;
69
+ }
70
+ export type CreateInviteResult = {
71
+ invited: true;
72
+ email: string;
73
+ role: string;
74
+ member_added: true;
75
+ } | {
76
+ invited: true;
77
+ email: string;
78
+ role: string;
79
+ invite_code: string;
80
+ member_added: false;
81
+ };
82
+ export declare const listSpaces: () => Promise<SpaceSummary[]>;
83
+ export declare const createSpace: (input: {
84
+ name: string;
85
+ slug: string;
86
+ }) => Promise<SpaceSummary>;
87
+ export declare const getSpace: (id: string) => Promise<SpaceSummary>;
88
+ export declare const listSpaceMembers: (id: string) => Promise<SpaceMember[]>;
89
+ export declare const createSpaceInvite: (id: string, input: {
90
+ email: string;
91
+ role: "admin" | "member";
92
+ }) => Promise<CreateInviteResult>;
93
+ export declare const listSpaceInvites: (id: string) => Promise<SpaceInvite[]>;
94
+ export declare const revokeSpaceInvite: (id: string, inviteId: string) => Promise<{
95
+ revoked: true;
96
+ }>;
97
+ export declare const updateSpaceMemberRole: (id: string, userId: string, role: "admin" | "member") => Promise<{
98
+ user_id: string;
99
+ role: string;
100
+ updated: true;
101
+ }>;
102
+ export declare const removeSpaceMember: (id: string, userId: string) => Promise<void>;
103
+ export declare const deleteSpace: (id: string) => Promise<void>;
104
+ export interface Subscription {
105
+ tierName?: string;
106
+ [key: string]: unknown;
107
+ }
108
+ export declare const fetchSubscription: (agent?: string) => Promise<Subscription>;
109
+ /** `Design Team` → `design-team`; the slug a space gets when the user gives only a name. */
110
+ export declare function slugify(name: string): string;
111
+ /** Resolve a space by id, slug, or name from a list the caller already fetched. */
112
+ export declare function resolveSpaceFromList(spaces: SpaceSummary[], ref?: string): SpaceSummary | null;
113
+ /** Resolve a member by email or user id from a list the caller already fetched. */
114
+ export declare function resolveMemberFromList(members: SpaceMember[], ref: string): SpaceMember | null;
115
+ export type { PhoenixSession as Session };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Phoenix ID — the typed surface commands use. Every route the account backend
3
+ * exposes is a function here; no command builds a URL or reads a token itself.
4
+ */
5
+ import { phoenixRequest, PhoenixApiError } from './client.js';
6
+ export { PHOENIX_ID_BASE, PhoenixApiError, clearSession, readSession, sessionFilePath, writeSession, } from './client.js';
7
+ export function startDeviceAuthorization() {
8
+ return phoenixRequest('POST', '/api/v1/auth/device/authorization', {
9
+ auth: false,
10
+ body: {},
11
+ });
12
+ }
13
+ export async function pollDeviceToken(deviceCode) {
14
+ try {
15
+ return await phoenixRequest('POST', '/api/v1/auth/device/token', {
16
+ auth: false,
17
+ body: {
18
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
19
+ device_code: deviceCode,
20
+ },
21
+ });
22
+ }
23
+ catch (err) {
24
+ if (!(err instanceof PhoenixApiError))
25
+ throw err;
26
+ // The server encodes poll state in the error body (RFC 8628 §3.5).
27
+ if (err.message.includes('authorization_pending'))
28
+ return { status: 'pending' };
29
+ if (err.message.includes('slow_down'))
30
+ return { status: 'slow_down' };
31
+ if (err.message.includes('expired_token'))
32
+ return { status: 'expired' };
33
+ if (err.message.includes('access_denied'))
34
+ return { status: 'denied' };
35
+ throw err;
36
+ }
37
+ }
38
+ export function fetchWhoAmI(token) {
39
+ return phoenixRequest('GET', '/api/v1/auth/me', { token });
40
+ }
41
+ export const listSpaces = () => phoenixRequest('GET', '/api/v1/spaces');
42
+ export const createSpace = (input) => phoenixRequest('POST', '/api/v1/spaces', { body: input });
43
+ export const getSpace = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}`);
44
+ export const listSpaceMembers = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}/members`);
45
+ export const createSpaceInvite = (id, input) => phoenixRequest('POST', `/api/v1/spaces/${encodeURIComponent(id)}/invites`, {
46
+ body: input,
47
+ });
48
+ export const listSpaceInvites = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}/invites`);
49
+ export const revokeSpaceInvite = (id, inviteId) => phoenixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(id)}/invites/${encodeURIComponent(inviteId)}`);
50
+ export const updateSpaceMemberRole = (id, userId, role) => phoenixRequest('PATCH', `/api/v1/spaces/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`, {
51
+ body: { role },
52
+ });
53
+ export const removeSpaceMember = (id, userId) => phoenixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`);
54
+ export const deleteSpace = (id) => phoenixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(id)}`);
55
+ export const fetchSubscription = (agent = 'agents-cli') => phoenixRequest('GET', `/api/v1/billing/subscription?agent=${encodeURIComponent(agent)}`);
56
+ // ─── Helpers shared by the commands ──────────────────────────────────────────
57
+ /** `Design Team` → `design-team`; the slug a space gets when the user gives only a name. */
58
+ export function slugify(name) {
59
+ return name
60
+ .trim()
61
+ .toLowerCase()
62
+ .replace(/[^a-z0-9]+/g, '-')
63
+ .replace(/^-+|-+$/g, '')
64
+ .slice(0, 63);
65
+ }
66
+ /** Resolve a space by id, slug, or name from a list the caller already fetched. */
67
+ export function resolveSpaceFromList(spaces, ref) {
68
+ if (!ref)
69
+ return spaces.length === 1 ? spaces[0] : null;
70
+ const needle = ref.trim().toLowerCase();
71
+ return (spaces.find((s) => s.id === ref) ??
72
+ spaces.find((s) => s.slug.toLowerCase() === needle) ??
73
+ spaces.find((s) => s.name.toLowerCase() === needle) ??
74
+ null);
75
+ }
76
+ /** Resolve a member by email or user id from a list the caller already fetched. */
77
+ export function resolveMemberFromList(members, ref) {
78
+ const needle = ref.trim().toLowerCase();
79
+ return (members.find((m) => m.user_id === ref) ??
80
+ members.find((m) => m.email.toLowerCase() === needle) ??
81
+ null);
82
+ }
@@ -7,7 +7,11 @@
7
7
  <key>CFBundleIdentifier</key>
8
8
  <string>com.phnx-labs.agents-menubar</string>
9
9
  <key>CFBundleName</key>
10
- <string>Agents Menu Bar</string>
10
+ <string>AGI Menu</string>
11
+ <key>CFBundleDisplayName</key>
12
+ <string>AGI Menu</string>
13
+ <key>CFBundleIconFile</key>
14
+ <string>AppIcon</string>
11
15
  <key>CFBundlePackageType</key>
12
16
  <string>APPL</string>
13
17
  <key>CFBundleShortVersionString</key>
@@ -3,9 +3,22 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>files</key>
6
- <dict/>
6
+ <dict>
7
+ <key>Resources/AppIcon.icns</key>
8
+ <data>
9
+ jOjZVimcFRHoP2VPzgn8uM8mjkA=
10
+ </data>
11
+ </dict>
7
12
  <key>files2</key>
8
- <dict/>
13
+ <dict>
14
+ <key>Resources/AppIcon.icns</key>
15
+ <dict>
16
+ <key>hash2</key>
17
+ <data>
18
+ GFvSLeNYJ3ASxW3OzcuiB4aID0gZUBkpozmWZkbTFNw=
19
+ </data>
20
+ </dict>
21
+ </dict>
9
22
  <key>rules</key>
10
23
  <dict>
11
24
  <key>^Resources/</key>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Async probe capturing stdout. Rejects on spawn error, non-zero exit, or
3
+ * timeout — matching the `execFileAsync` contract the version probe had — and
4
+ * reaps the probe's whole process group on every settle path.
5
+ */
6
+ export declare function probeCapture(cmd: string, args: string[], timeoutMs: number): Promise<{
7
+ stdout: string;
8
+ }>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Spawn helpers for CAPABILITY PROBES — short-lived invocations of third-party
3
+ * binaries (`copilot --version`, a manifest's `check:` command) whose only job
4
+ * is an exit status or a line of stdout.
5
+ *
6
+ * A probed binary may fork children of its own: the copilot npm wrapper forks
7
+ * a platform-binary downloader into `~/Library/Caches/copilot` on first run
8
+ * under a fresh HOME. Node's `timeout:` option kills only the DIRECT child, so
9
+ * such grandchildren outlive the probe and keep writing — under a test's temp
10
+ * HOME that race is the ENOTEMPTY teardown class (RUSH-3028; residual after
11
+ * RUSH-3021 gated the daemon autostart). Every probe here therefore runs in
12
+ * its OWN process group (`detached`), and the whole group is reaped once the
13
+ * probe settles, so nothing a probe spawned can outlive it.
14
+ *
15
+ * Group semantics are POSIX only: on win32 `detached` means a new console and
16
+ * negative-pid group kills are unsupported, so there the DIRECT child is
17
+ * killed on settle instead — the same guarantee `execFileAsync`'s `timeout:`
18
+ * gave (the grandchild leak class is a darwin/linux temp-HOME teardown race).
19
+ *
20
+ * The parent dying mid-probe is covered too: a detached probe leaves the
21
+ * terminal's foreground group, so a Ctrl-C that hard-exits the CLI
22
+ * (`process.exit(130)` in index.ts) would strand it. Live probe groups are
23
+ * tracked in LIVE_GROUPS and a `process.on('exit')` hook — which runs on
24
+ * every `process.exit` path, including that SIGINT handler — reaps them
25
+ * synchronously.
26
+ */
27
+ import { spawn } from 'child_process';
28
+ const GROUP_REAP = process.platform !== 'win32';
29
+ const LIVE_GROUPS = new Set();
30
+ let exitHookInstalled = false;
31
+ function ensureExitHook() {
32
+ if (exitHookInstalled)
33
+ return;
34
+ exitHookInstalled = true;
35
+ process.on('exit', () => {
36
+ for (const pid of LIVE_GROUPS) {
37
+ try {
38
+ process.kill(-pid, 'SIGKILL');
39
+ }
40
+ catch {
41
+ /* group already fully exited */
42
+ }
43
+ }
44
+ });
45
+ }
46
+ function reapGroup(pid) {
47
+ if (!GROUP_REAP || !pid)
48
+ return;
49
+ LIVE_GROUPS.delete(pid);
50
+ try {
51
+ process.kill(-pid, 'SIGKILL');
52
+ }
53
+ catch {
54
+ /* group already fully exited */
55
+ }
56
+ }
57
+ /**
58
+ * Async probe capturing stdout. Rejects on spawn error, non-zero exit, or
59
+ * timeout — matching the `execFileAsync` contract the version probe had — and
60
+ * reaps the probe's whole process group on every settle path.
61
+ */
62
+ export function probeCapture(cmd, args, timeoutMs) {
63
+ return new Promise((resolve, reject) => {
64
+ const child = spawn(cmd, args, {
65
+ detached: GROUP_REAP,
66
+ stdio: ['ignore', 'pipe', 'ignore'],
67
+ windowsHide: true,
68
+ });
69
+ if (GROUP_REAP && child.pid) {
70
+ LIVE_GROUPS.add(child.pid);
71
+ ensureExitHook();
72
+ }
73
+ let out = '';
74
+ let settled = false;
75
+ const settle = (err) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ reapGroup(child.pid);
81
+ // win32 has no group to reap: kill the direct child so a timed-out
82
+ // probe still dies, matching execFile's `timeout:` behavior. No-op
83
+ // after a clean exit.
84
+ if (!GROUP_REAP)
85
+ child.kill('SIGKILL');
86
+ if (err)
87
+ reject(err);
88
+ else
89
+ resolve({ stdout: out });
90
+ };
91
+ const timer = setTimeout(() => settle(new Error(`probe timed out after ${timeoutMs}ms: ${cmd} ${args.join(' ')}`)), timeoutMs);
92
+ child.stdout?.setEncoding('utf8');
93
+ child.stdout?.on('data', (d) => {
94
+ out += d;
95
+ });
96
+ child.on('error', (e) => settle(e));
97
+ // 'exit', not 'close': a forked grandchild inherits the stdout pipe, and
98
+ // 'close' waits for EVERY holder of that pipe to exit — exactly the
99
+ // process this helper exists to reap. Settle when the probed binary
100
+ // itself exits; one tick's grace lets its final stdout chunks land.
101
+ child.on('exit', (code) => {
102
+ setImmediate(() => settle(code === 0 ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
103
+ });
104
+ });
105
+ }
@@ -30,7 +30,9 @@ export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
30
30
  * read-only local web companion + `--control` anchor) was removed with the
31
31
  * unshipped iOS Fleet Cockpit it existed for (RUSH-3001). `apply` nested under
32
32
  * `agents fleet apply` / `agents devices apply`. `beta` nested under
33
- * `agents setup beta` (RUSH-2981).
33
+ * `agents setup beta` (RUSH-2981). `org` (the Prix-coupled account layer) was
34
+ * removed; `agents auth` returned against Phoenix ID with `auth space` as the
35
+ * team surface (RUSH-2581).
34
36
  */
35
37
  export declare const RETIRED_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
36
38
  export declare function isKnownTopLevelCommand(name: string): boolean;
@@ -1,5 +1,5 @@
1
1
  const LOADED_COMMAND_NAMES = [
2
- 'accounts', 'auth', 'org', 'view', 'inspect', 'feedback', 'commands', 'hooks', 'skills', 'rules', 'memory',
2
+ 'accounts', 'auth', 'view', 'inspect', 'feedback', 'commands', 'hooks', 'skills', 'rules', 'memory',
3
3
  'permissions', 'mcp', 'clis', 'subagents', 'plugins', 'workflows', 'add', 'use', 'list',
4
4
  'remove', 'rm', 'purge', 'update', 'prune', 'import', 'registry', 'search', 'install',
5
5
  'routines', 'monitors', 'projects', 'run', 'open', 'reconnect', 'fork', 'config',
@@ -50,10 +50,13 @@ export const KNOWN_TOP_LEVEL_COMMANDS = new Set([
50
50
  * read-only local web companion + `--control` anchor) was removed with the
51
51
  * unshipped iOS Fleet Cockpit it existed for (RUSH-3001). `apply` nested under
52
52
  * `agents fleet apply` / `agents devices apply`. `beta` nested under
53
- * `agents setup beta` (RUSH-2981).
53
+ * `agents setup beta` (RUSH-2981). `org` (the Prix-coupled account layer) was
54
+ * removed; `agents auth` returned against Phoenix ID with `auth space` as the
55
+ * team surface (RUSH-2581).
54
56
  */
55
57
  export const RETIRED_TOP_LEVEL_COMMANDS = new Set([
56
58
  'webhook',
59
+ 'org',
57
60
  'serve',
58
61
  'login',
59
62
  'logout',
@@ -28,8 +28,8 @@ export interface ViewJsonVersion {
28
28
  resetsAt: string | null;
29
29
  }>;
30
30
  unavailable?: {
31
- reason: 'session_limit';
32
- resetsAt: string;
31
+ reason: 'session_limit' | 'out_of_credits';
32
+ resetsAt?: string;
33
33
  };
34
34
  lastActive: string | null;
35
35
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.44",
3
+ "version": "1.22.46",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,11 +0,0 @@
1
- import type { Command } from 'commander';
2
- /** `--role` defaults to member when omitted; anything present must be a real role — never silently coerced. */
3
- export declare function resolveInviteRole(raw: string | undefined): 'admin' | 'member';
4
- /** Validate a role argument before any network call — same rule the backend enforces server-side. */
5
- export declare function parseRole(raw: string): 'admin' | 'member';
6
- /** One-line deprecation for the leftover top-level `agents org` spelling. */
7
- export declare function printOrgDeprecation(): void;
8
- /** Canonical home: `agents auth space …`. */
9
- export declare function registerAuthSpaceCommand(auth: Command): void;
10
- /** Leftover top-level spelling — same tree, deprecation on every path. */
11
- export declare function registerOrgCommand(program: Command): void;
@@ -1,228 +0,0 @@
1
- import chalk from 'chalk';
2
- import { setHelpSections } from '../lib/help.js';
3
- import { runOrDie } from '../lib/format.js';
4
- import { createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, removeSpaceMember, resolveMemberFromList, resolvePrixToken, resolveSpaceFromList, slugify, updateSpaceMemberRole, } from '../lib/prix-account.js';
5
- /** Every `agents org` subcommand needs a token up front — one place to say so. */
6
- function requireToken() {
7
- if (!resolvePrixToken())
8
- throw new Error("Not signed in. Run 'agents auth login' first.");
9
- }
10
- async function resolveSpace(explicit) {
11
- const spaces = await listSpaces();
12
- return resolveSpaceFromList(spaces, explicit);
13
- }
14
- function printSpace(space) {
15
- console.log(`${chalk.bold(space.name)} ${chalk.gray(space.slug)} ${chalk.gray(space.id)}`);
16
- console.log(` role: ${space.user_role}${space.organization_id ? ` org: ${space.organization_id}` : ''}`);
17
- }
18
- function printMembers(members) {
19
- if (!members.length) {
20
- console.log(chalk.gray(' No members.'));
21
- return;
22
- }
23
- for (const m of members)
24
- console.log(` ${chalk.cyan(m.email)} ${m.role} ${chalk.gray(m.user_id)}`);
25
- }
26
- async function runCreate(name, o) {
27
- requireToken();
28
- const space = await createSpace({ name, slug: o.slug ?? slugify(name), description: o.description });
29
- if (o.json) {
30
- console.log(JSON.stringify(space, null, 2));
31
- return;
32
- }
33
- console.log(chalk.green(`Created space '${space.name}' (${space.slug}).`));
34
- printSpace(space);
35
- }
36
- async function runList(o) {
37
- requireToken();
38
- const spaces = await listSpaces();
39
- if (o.json) {
40
- console.log(JSON.stringify(spaces, null, 2));
41
- return;
42
- }
43
- if (!spaces.length) {
44
- console.log(chalk.gray("No spaces. Create one with 'agents auth space create <name>'."));
45
- return;
46
- }
47
- for (const space of spaces)
48
- printSpace(space);
49
- }
50
- async function runView(spaceArg, o) {
51
- requireToken();
52
- const space = await resolveSpace(spaceArg);
53
- if (o.json) {
54
- console.log(JSON.stringify(space, null, 2));
55
- return;
56
- }
57
- printSpace(space);
58
- }
59
- /** `--role` defaults to member when omitted; anything present must be a real role — never silently coerced. */
60
- export function resolveInviteRole(raw) {
61
- return raw === undefined ? 'member' : parseRole(raw);
62
- }
63
- async function runInvite(email, o) {
64
- requireToken();
65
- const role = resolveInviteRole(o.role);
66
- const space = await resolveSpace(o.space);
67
- const result = await createSpaceInvite(space.id, email, role);
68
- if (o.json) {
69
- console.log(JSON.stringify(result, null, 2));
70
- return;
71
- }
72
- if (result.member_added)
73
- console.log(chalk.green(`Added ${email} to '${space.name}' as ${role}.`));
74
- else
75
- console.log(chalk.green(`Invited ${email} to '${space.name}' as ${role}. They'll get an email.`));
76
- }
77
- async function runMembers(spaceArg, o) {
78
- requireToken();
79
- const space = await resolveSpace(spaceArg);
80
- const members = await listSpaceMembers(space.id);
81
- if (o.json) {
82
- console.log(JSON.stringify(members, null, 2));
83
- return;
84
- }
85
- console.log(chalk.bold(`${space.name} (${members.length} member${members.length === 1 ? '' : 's'})`));
86
- printMembers(members);
87
- }
88
- /** Validate a role argument before any network call — same rule the backend enforces server-side. */
89
- export function parseRole(raw) {
90
- if (raw === 'admin' || raw === 'member')
91
- return raw;
92
- throw new Error(`role must be 'admin' or 'member', got '${raw}'.`);
93
- }
94
- async function runRole(email, roleRaw, o) {
95
- requireToken();
96
- const role = parseRole(roleRaw);
97
- const space = await resolveSpace(o.space);
98
- const members = await listSpaceMembers(space.id);
99
- const member = resolveMemberFromList(members, email);
100
- const result = await updateSpaceMemberRole(space.id, member.user_id, role);
101
- if (o.json) {
102
- console.log(JSON.stringify(result, null, 2));
103
- return;
104
- }
105
- console.log(chalk.green(`${email} is now ${role} in '${space.name}'.`));
106
- }
107
- async function runRemove(email, o) {
108
- requireToken();
109
- const space = await resolveSpace(o.space);
110
- const members = await listSpaceMembers(space.id);
111
- const member = resolveMemberFromList(members, email);
112
- await removeSpaceMember(space.id, member.user_id);
113
- if (o.json) {
114
- console.log(JSON.stringify({ removed: email, space: space.slug }, null, 2));
115
- return;
116
- }
117
- console.log(chalk.green(`Removed ${email} from '${space.name}'.`));
118
- }
119
- async function runLeave(spaceArg, o) {
120
- requireToken();
121
- const resolved = resolvePrixToken();
122
- if (!resolved)
123
- throw new Error("Not signed in. Run 'agents auth login' first.");
124
- const who = await fetchWhoAmI(resolved.token);
125
- const space = await resolveSpace(spaceArg);
126
- if (space.owner_user_id === who.userId) {
127
- throw new Error(`You own '${space.name}'. Transfer ownership or delete the space instead of leaving it.`);
128
- }
129
- await removeSpaceMember(space.id, who.userId);
130
- if (o.json) {
131
- console.log(JSON.stringify({ left: space.slug }, null, 2));
132
- return;
133
- }
134
- console.log(chalk.green(`Left '${space.name}'.`));
135
- }
136
- /** One-line deprecation for the leftover top-level `agents org` spelling. */
137
- export function printOrgDeprecation() {
138
- console.error(chalk.yellow('agents org is deprecated — use `agents auth space`.'));
139
- }
140
- /** Shared space CRUD tree — canonical under `auth space`, leftover under `org`. */
141
- function attachSpaceCommands(parent, opts) {
142
- const before = () => {
143
- if (opts.deprecated)
144
- printOrgDeprecation();
145
- };
146
- const jsonFlag = (o, command) => !!(o.json || command.optsWithGlobals().json);
147
- parent.command('create <name>').description('Create a space (free tier: 1 owned space)')
148
- .option('--slug <slug>', 'Override the derived slug')
149
- .option('--description <text>', 'Optional description')
150
- .option('--json', 'Machine-readable output')
151
- .action((name, o, command) => {
152
- before();
153
- const json = jsonFlag(o, command);
154
- return runOrDie(() => runCreate(name, { ...o, json }), { json });
155
- });
156
- parent.command('list').description('List spaces you own or belong to').option('--json', 'Machine-readable output')
157
- .action((o, command) => {
158
- before();
159
- const json = jsonFlag(o, command);
160
- return runOrDie(() => runList({ json }), { json });
161
- });
162
- parent.command('view [space]').description('Show one space (defaults to your only space)').option('--json', 'Machine-readable output')
163
- .action((space, o, command) => {
164
- before();
165
- const json = jsonFlag(o, command);
166
- return runOrDie(() => runView(space, { json }), { json });
167
- });
168
- parent.command('invite <email>').description('Invite (or directly add) a member')
169
- .option('--role <role>', 'admin | member', 'member')
170
- .option('--space <id-or-slug>', 'Space to invite into (defaults to your only space)')
171
- .option('--json', 'Machine-readable output')
172
- .action((email, o, command) => {
173
- before();
174
- const json = jsonFlag(o, command);
175
- return runOrDie(() => runInvite(email, { ...o, json }), { json });
176
- });
177
- parent.command('members [space]').description("List a space's members").option('--json', 'Machine-readable output')
178
- .action((space, o, command) => {
179
- before();
180
- const json = jsonFlag(o, command);
181
- return runOrDie(() => runMembers(space, { json }), { json });
182
- });
183
- parent.command('role <email> <role>').description("Change a member's role (owner-only for admin)")
184
- .option('--space <id-or-slug>', 'Space to change (defaults to your only space)')
185
- .option('--json', 'Machine-readable output')
186
- .action((email, role, o, command) => {
187
- before();
188
- const json = jsonFlag(o, command);
189
- return runOrDie(() => runRole(email, role, { ...o, json }), { json });
190
- });
191
- parent.command('remove <email>').description('Remove a member from a space')
192
- .option('--space <id-or-slug>', 'Space to remove from (defaults to your only space)')
193
- .option('--json', 'Machine-readable output')
194
- .action((email, o, command) => {
195
- before();
196
- const json = jsonFlag(o, command);
197
- return runOrDie(() => runRemove(email, { ...o, json }), { json });
198
- });
199
- parent.command('leave [space]').description('Leave a space you do not own').option('--json', 'Machine-readable output')
200
- .action((space, o, command) => {
201
- before();
202
- const json = jsonFlag(o, command);
203
- return runOrDie(() => runLeave(space, { json }), { json });
204
- });
205
- setHelpSections(parent, {
206
- examples: `${opts.prefix} create acme-team
207
- ${opts.prefix} list
208
- ${opts.prefix} view acme-team
209
- ${opts.prefix} invite dev@example.com --role admin
210
- ${opts.prefix} members
211
- ${opts.prefix} role dev@example.com admin
212
- ${opts.prefix} remove dev@example.com
213
- ${opts.prefix} leave acme-team`,
214
- notes: opts.deprecated
215
- ? 'Deprecated alias of `agents auth space`. Maps to the Rush backend\'s /api/v1/spaces (the free-tier team primitive), not /api/v1/orgs. Free tier: 1 owned space, 3 members per space. Every command needs `agents auth login` (or a `rush login` session) first. `--space` is only needed once you belong to more than one space.'
216
- : 'Maps to the Rush backend\'s /api/v1/spaces (the free-tier team primitive), not /api/v1/orgs. Free tier: 1 owned space, 3 members per space. Every command needs `agents auth login` (or a `rush login` session) first. `--space` is only needed once you belong to more than one space. The leftover `agents org` spelling still works and prints a deprecation line.',
217
- });
218
- }
219
- /** Canonical home: `agents auth space …`. */
220
- export function registerAuthSpaceCommand(auth) {
221
- const space = auth.command('space').description('Create and manage a Prix space (invite collaborators)');
222
- attachSpaceCommands(space, { prefix: 'agents auth space' });
223
- }
224
- /** Leftover top-level spelling — same tree, deprecation on every path. */
225
- export function registerOrgCommand(program) {
226
- const org = program.command('org').description('Deprecated alias of `agents auth space` — create and manage a Prix space');
227
- attachSpaceCommands(org, { prefix: 'agents org', deprecated: true });
228
- }