pal-explorer-cli 0.4.12 → 0.4.13

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 (99) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +63 -2
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +203 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/help.js +357 -357
  98. package/lib/utils/torrent.js +1 -0
  99. package/package.json +4 -3
@@ -1,273 +1,273 @@
1
- import chalk from 'chalk';
2
-
3
- async function getAuthHeaders() {
4
- const { fetchFirst } = await import('../core/discoveryClient.js');
5
- const { getIdentity } = await import('../core/identity.js');
6
-
7
- const identity = await getIdentity();
8
- if (!identity?.publicKey) {
9
- console.error(chalk.red('No identity. Run pe init first.'));
10
- process.exitCode = 1;
11
- return null;
12
- }
13
-
14
- const keysRes = await fetchFirst(`/api/v1/keys/${encodeURIComponent(identity.publicKey)}`);
15
- const keys = keysRes ? await keysRes.json() : [];
16
- if (!keys?.length) {
17
- console.error(chalk.red('No API key. Create one: pe api-keys create'));
18
- process.exitCode = 1;
19
- return null;
20
- }
21
-
22
- return { 'Content-Type': 'application/json', 'x-api-key': keys[0].key };
23
- }
24
-
25
- async function apiRequest(method, path, body) {
26
- const { getPrimaryServer } = await import('../core/discoveryClient.js');
27
- const headers = await getAuthHeaders();
28
- if (!headers) return null;
29
-
30
- const baseUrl = getPrimaryServer();
31
- const opts = { method, headers, signal: AbortSignal.timeout(10000) };
32
- if (body) opts.body = JSON.stringify(body);
33
-
34
- const res = await fetch(`${baseUrl}${path}`, opts);
35
- return res;
36
- }
37
-
38
- export default function orgCommand(program) {
39
- const cmd = program
40
- .command('org')
41
- .description('manage organizations for enterprise extension management')
42
- .addHelpText('after', `
43
- Examples:
44
- $ pe org create "Acme Corp" Create an organization
45
- $ pe org list List your organizations
46
- $ pe org info abc123 Show org details and members
47
- $ pe org invite abc123 alice Add a member by handle
48
- $ pe org remove abc123 alice Remove a member
49
- $ pe org subscribe abc123 analytics Subscribe org to an extension
50
- $ pe org unsubscribe abc123 analytics Cancel extension subscription
51
- $ pe org billing abc123 Show billing summary
52
- `);
53
-
54
- cmd
55
- .command('create <name>')
56
- .description('create an organization')
57
- .action(async (name) => {
58
- try {
59
- const res = await apiRequest('POST', '/api/v1/orgs', { name });
60
- if (!res) return;
61
- const data = await res.json();
62
- if (res.ok && data?.id) {
63
- console.log(chalk.green(`\u2714 Created organization "${name}"`));
64
- console.log(` ID: ${chalk.white(data.id)}`);
65
- } else {
66
- console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
67
- process.exitCode = 1;
68
- }
69
- } catch (err) {
70
- console.error(chalk.red(`Create failed: ${err.message}`));
71
- process.exitCode = 1;
72
- }
73
- });
74
-
75
- cmd
76
- .command('list')
77
- .description('list your organizations')
78
- .action(async () => {
79
- try {
80
- const res = await apiRequest('GET', '/api/v1/orgs');
81
- if (!res) return;
82
- const data = await res.json();
83
- if (!data?.orgs?.length) {
84
- console.log(chalk.gray('No organizations.'));
85
- console.log(chalk.gray(' pe org create <name>'));
86
- return;
87
- }
88
- console.log('');
89
- console.log(chalk.cyan('Organizations:'));
90
- console.log(chalk.gray('\u2500'.repeat(50)));
91
- for (const org of data.orgs) {
92
- const role = org.role === 'owner' ? chalk.yellow('owner') : chalk.gray(org.role);
93
- console.log(` ${chalk.white(org.name)} ${chalk.gray(`(${org.id})`)}`);
94
- console.log(` Role: ${role} Members: ${chalk.cyan(org.memberCount || 0)} Subscriptions: ${chalk.cyan(org.subscriptionCount || 0)}`);
95
- }
96
- console.log('');
97
- } catch (err) {
98
- console.error(chalk.red(`List failed: ${err.message}`));
99
- process.exitCode = 1;
100
- }
101
- });
102
-
103
- cmd
104
- .command('info <id>')
105
- .description('show org details, members, and subscriptions')
106
- .action(async (id) => {
107
- try {
108
- const res = await apiRequest('GET', `/api/v1/orgs/${encodeURIComponent(id)}`);
109
- if (!res) return;
110
- if (!res.ok) {
111
- const err = await res.json().catch(() => ({}));
112
- console.error(chalk.red(`Not found: ${err?.error || 'Organization not found'}`));
113
- process.exitCode = 1;
114
- return;
115
- }
116
- const data = await res.json();
117
- console.log('');
118
- console.log(chalk.cyan.bold(data.name));
119
- console.log(chalk.gray('\u2500'.repeat(50)));
120
- console.log(` ID: ${chalk.white(data.id)}`);
121
- console.log(` Created: ${chalk.gray(new Date(data.createdAt).toLocaleDateString())}`);
122
- console.log(` Plan: ${chalk.yellow(data.plan || 'free')}`);
123
-
124
- if (data.members?.length) {
125
- console.log('');
126
- console.log(chalk.cyan(' Members:'));
127
- for (const m of data.members) {
128
- const roleColor = m.role === 'owner' ? chalk.yellow : m.role === 'admin' ? chalk.cyan : chalk.gray;
129
- console.log(` ${chalk.white('@' + m.handle)} ${roleColor(m.role)}`);
130
- }
131
- }
132
-
133
- if (data.subscriptions?.length) {
134
- console.log('');
135
- console.log(chalk.cyan(' Subscriptions:'));
136
- for (const sub of data.subscriptions) {
137
- const status = sub.active ? chalk.green('active') : chalk.red('cancelled');
138
- console.log(` ${chalk.white(sub.extName)} ${status} ${chalk.gray('$' + (sub.price || 0).toFixed(2) + '/mo')}`);
139
- }
140
- }
141
- console.log('');
142
- } catch (err) {
143
- console.error(chalk.red(`Info failed: ${err.message}`));
144
- process.exitCode = 1;
145
- }
146
- });
147
-
148
- cmd
149
- .command('invite <id> <handle>')
150
- .description('add a member to an organization')
151
- .action(async (id, handle) => {
152
- try {
153
- const res = await apiRequest('POST', `/api/v1/orgs/${encodeURIComponent(id)}/members`, { handle });
154
- if (!res) return;
155
- const data = await res.json();
156
- if (res.ok && data?.success) {
157
- console.log(chalk.green(`\u2714 Invited @${handle} to organization`));
158
- } else {
159
- console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
160
- process.exitCode = 1;
161
- }
162
- } catch (err) {
163
- console.error(chalk.red(`Invite failed: ${err.message}`));
164
- process.exitCode = 1;
165
- }
166
- });
167
-
168
- cmd
169
- .command('remove <id> <handle>')
170
- .description('remove a member from an organization')
171
- .action(async (id, handle) => {
172
- try {
173
- const res = await apiRequest('DELETE', `/api/v1/orgs/${encodeURIComponent(id)}/members/${encodeURIComponent(handle)}`);
174
- if (!res) return;
175
- const data = await res.json();
176
- if (res.ok && data?.success) {
177
- console.log(chalk.green(`\u2714 Removed @${handle} from organization`));
178
- } else {
179
- console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
180
- process.exitCode = 1;
181
- }
182
- } catch (err) {
183
- console.error(chalk.red(`Remove failed: ${err.message}`));
184
- process.exitCode = 1;
185
- }
186
- });
187
-
188
- cmd
189
- .command('subscribe <id> <ext-name>')
190
- .description('subscribe organization to an enterprise extension')
191
- .action(async (id, extName) => {
192
- try {
193
- const res = await apiRequest('POST', `/api/v1/orgs/${encodeURIComponent(id)}/subscriptions`, { extName });
194
- if (!res) return;
195
- const data = await res.json();
196
- if (res.ok && data?.success) {
197
- console.log(chalk.green(`\u2714 Subscribed to "${extName}"`));
198
- if (data.price) console.log(` Price: ${chalk.yellow('$' + data.price.toFixed(2) + '/mo')}`);
199
- } else {
200
- console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
201
- process.exitCode = 1;
202
- }
203
- } catch (err) {
204
- console.error(chalk.red(`Subscribe failed: ${err.message}`));
205
- process.exitCode = 1;
206
- }
207
- });
208
-
209
- cmd
210
- .command('unsubscribe <id> <ext-name>')
211
- .description('cancel an extension subscription')
212
- .action(async (id, extName) => {
213
- try {
214
- const res = await apiRequest('DELETE', `/api/v1/orgs/${encodeURIComponent(id)}/subscriptions/${encodeURIComponent(extName)}`);
215
- if (!res) return;
216
- const data = await res.json();
217
- if (res.ok && data?.success) {
218
- console.log(chalk.green(`\u2714 Unsubscribed from "${extName}"`));
219
- } else {
220
- console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
221
- process.exitCode = 1;
222
- }
223
- } catch (err) {
224
- console.error(chalk.red(`Unsubscribe failed: ${err.message}`));
225
- process.exitCode = 1;
226
- }
227
- });
228
-
229
- cmd
230
- .command('billing <id>')
231
- .description('show billing summary for an organization')
232
- .action(async (id) => {
233
- try {
234
- const res = await apiRequest('GET', `/api/v1/orgs/${encodeURIComponent(id)}/billing`);
235
- if (!res) return;
236
- if (!res.ok) {
237
- const err = await res.json().catch(() => ({}));
238
- console.error(chalk.red(`Failed: ${err?.error || 'Unknown error'}`));
239
- process.exitCode = 1;
240
- return;
241
- }
242
- const data = await res.json();
243
- console.log('');
244
- console.log(chalk.cyan.bold('Organization Billing'));
245
- console.log(chalk.gray('\u2500'.repeat(40)));
246
- console.log(` Plan: ${chalk.yellow(data.plan || 'free')}`);
247
- console.log(` Monthly Total: ${chalk.white('$' + (data.monthlyTotal || 0).toFixed(2))}`);
248
- console.log(` Active Seats: ${chalk.cyan(data.activeSeats || 0)}`);
249
- console.log(` Subscriptions: ${chalk.cyan(data.subscriptionCount || 0)}`);
250
-
251
- if (data.subscriptions?.length) {
252
- console.log('');
253
- console.log(chalk.cyan(' Active Subscriptions:'));
254
- for (const sub of data.subscriptions) {
255
- console.log(` ${chalk.white(sub.extName)} ${chalk.gray('$' + (sub.price || 0).toFixed(2) + '/mo')} ${chalk.gray('since ' + new Date(sub.startDate).toLocaleDateString())}`);
256
- }
257
- }
258
-
259
- if (data.invoices?.length) {
260
- console.log('');
261
- console.log(chalk.cyan(' Recent Invoices:'));
262
- for (const inv of data.invoices) {
263
- const statusColor = inv.status === 'paid' ? chalk.green : inv.status === 'pending' ? chalk.yellow : chalk.red;
264
- console.log(` ${chalk.gray(new Date(inv.date).toLocaleDateString())} $${inv.amount.toFixed(2)} ${statusColor(inv.status)}`);
265
- }
266
- }
267
- console.log('');
268
- } catch (err) {
269
- console.error(chalk.red(`Billing failed: ${err.message}`));
270
- process.exitCode = 1;
271
- }
272
- });
273
- }
1
+ import chalk from 'chalk';
2
+
3
+ async function getAuthHeaders() {
4
+ const { fetchFirst } = await import('../core/discoveryClient.js');
5
+ const { getIdentity } = await import('../core/identity.js');
6
+
7
+ const identity = await getIdentity();
8
+ if (!identity?.publicKey) {
9
+ console.error(chalk.red('No identity. Run pal init first.'));
10
+ process.exitCode = 1;
11
+ return null;
12
+ }
13
+
14
+ const keysRes = await fetchFirst(`/api/v1/keys/${encodeURIComponent(identity.publicKey)}`);
15
+ const keys = keysRes ? await keysRes.json() : [];
16
+ if (!keys?.length) {
17
+ console.error(chalk.red('No API key. Create one: pal api-keys create'));
18
+ process.exitCode = 1;
19
+ return null;
20
+ }
21
+
22
+ return { 'Content-Type': 'application/json', 'x-api-key': keys[0].key };
23
+ }
24
+
25
+ async function apiRequest(method, path, body) {
26
+ const { getPrimaryServer } = await import('../core/discoveryClient.js');
27
+ const headers = await getAuthHeaders();
28
+ if (!headers) return null;
29
+
30
+ const baseUrl = getPrimaryServer();
31
+ const opts = { method, headers, signal: AbortSignal.timeout(10000) };
32
+ if (body) opts.body = JSON.stringify(body);
33
+
34
+ const res = await fetch(`${baseUrl}${path}`, opts);
35
+ return res;
36
+ }
37
+
38
+ export default function orgCommand(program) {
39
+ const cmd = program
40
+ .command('org')
41
+ .description('manage organizations for enterprise extension management')
42
+ .addHelpText('after', `
43
+ Examples:
44
+ $ pal org create "Acme Corp" Create an organization
45
+ $ pal org list List your organizations
46
+ $ pal org info abc123 Show org details and members
47
+ $ pal org invite abc123 alice Add a member by handle
48
+ $ pal org remove abc123 alice Remove a member
49
+ $ pal org subscribe abc123 analytics Subscribe org to an extension
50
+ $ pal org unsubscribe abc123 analytics Cancel extension subscription
51
+ $ pal org billing abc123 Show billing summary
52
+ `);
53
+
54
+ cmd
55
+ .command('create <name>')
56
+ .description('create an organization')
57
+ .action(async (name) => {
58
+ try {
59
+ const res = await apiRequest('POST', '/api/v1/orgs', { name });
60
+ if (!res) return;
61
+ const data = await res.json();
62
+ if (res.ok && data?.id) {
63
+ console.log(chalk.green(`\u2714 Created organization "${name}"`));
64
+ console.log(` ID: ${chalk.white(data.id)}`);
65
+ } else {
66
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
67
+ process.exitCode = 1;
68
+ }
69
+ } catch (err) {
70
+ console.error(chalk.red(`Create failed: ${err.message}`));
71
+ process.exitCode = 1;
72
+ }
73
+ });
74
+
75
+ cmd
76
+ .command('list')
77
+ .description('list your organizations')
78
+ .action(async () => {
79
+ try {
80
+ const res = await apiRequest('GET', '/api/v1/orgs');
81
+ if (!res) return;
82
+ const data = await res.json();
83
+ if (!data?.orgs?.length) {
84
+ console.log(chalk.gray('No organizations.'));
85
+ console.log(chalk.gray(' pal org create <name>'));
86
+ return;
87
+ }
88
+ console.log('');
89
+ console.log(chalk.cyan('Organizations:'));
90
+ console.log(chalk.gray('\u2500'.repeat(50)));
91
+ for (const org of data.orgs) {
92
+ const role = org.role === 'owner' ? chalk.yellow('owner') : chalk.gray(org.role);
93
+ console.log(` ${chalk.white(org.name)} ${chalk.gray(`(${org.id})`)}`);
94
+ console.log(` Role: ${role} Members: ${chalk.cyan(org.memberCount || 0)} Subscriptions: ${chalk.cyan(org.subscriptionCount || 0)}`);
95
+ }
96
+ console.log('');
97
+ } catch (err) {
98
+ console.error(chalk.red(`List failed: ${err.message}`));
99
+ process.exitCode = 1;
100
+ }
101
+ });
102
+
103
+ cmd
104
+ .command('info <id>')
105
+ .description('show org details, members, and subscriptions')
106
+ .action(async (id) => {
107
+ try {
108
+ const res = await apiRequest('GET', `/api/v1/orgs/${encodeURIComponent(id)}`);
109
+ if (!res) return;
110
+ if (!res.ok) {
111
+ const err = await res.json().catch(() => ({}));
112
+ console.error(chalk.red(`Not found: ${err?.error || 'Organization not found'}`));
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ const data = await res.json();
117
+ console.log('');
118
+ console.log(chalk.cyan.bold(data.name));
119
+ console.log(chalk.gray('\u2500'.repeat(50)));
120
+ console.log(` ID: ${chalk.white(data.id)}`);
121
+ console.log(` Created: ${chalk.gray(new Date(data.createdAt).toLocaleDateString())}`);
122
+ console.log(` Plan: ${chalk.yellow(data.plan || 'free')}`);
123
+
124
+ if (data.members?.length) {
125
+ console.log('');
126
+ console.log(chalk.cyan(' Members:'));
127
+ for (const m of data.members) {
128
+ const roleColor = m.role === 'owner' ? chalk.yellow : m.role === 'admin' ? chalk.cyan : chalk.gray;
129
+ console.log(` ${chalk.white('@' + m.handle)} ${roleColor(m.role)}`);
130
+ }
131
+ }
132
+
133
+ if (data.subscriptions?.length) {
134
+ console.log('');
135
+ console.log(chalk.cyan(' Subscriptions:'));
136
+ for (const sub of data.subscriptions) {
137
+ const status = sub.active ? chalk.green('active') : chalk.red('cancelled');
138
+ console.log(` ${chalk.white(sub.extName)} ${status} ${chalk.gray('$' + (sub.price || 0).toFixed(2) + '/mo')}`);
139
+ }
140
+ }
141
+ console.log('');
142
+ } catch (err) {
143
+ console.error(chalk.red(`Info failed: ${err.message}`));
144
+ process.exitCode = 1;
145
+ }
146
+ });
147
+
148
+ cmd
149
+ .command('invite <id> <handle>')
150
+ .description('add a member to an organization')
151
+ .action(async (id, handle) => {
152
+ try {
153
+ const res = await apiRequest('POST', `/api/v1/orgs/${encodeURIComponent(id)}/members`, { handle });
154
+ if (!res) return;
155
+ const data = await res.json();
156
+ if (res.ok && data?.success) {
157
+ console.log(chalk.green(`\u2714 Invited @${handle} to organization`));
158
+ } else {
159
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
160
+ process.exitCode = 1;
161
+ }
162
+ } catch (err) {
163
+ console.error(chalk.red(`Invite failed: ${err.message}`));
164
+ process.exitCode = 1;
165
+ }
166
+ });
167
+
168
+ cmd
169
+ .command('remove <id> <handle>')
170
+ .description('remove a member from an organization')
171
+ .action(async (id, handle) => {
172
+ try {
173
+ const res = await apiRequest('DELETE', `/api/v1/orgs/${encodeURIComponent(id)}/members/${encodeURIComponent(handle)}`);
174
+ if (!res) return;
175
+ const data = await res.json();
176
+ if (res.ok && data?.success) {
177
+ console.log(chalk.green(`\u2714 Removed @${handle} from organization`));
178
+ } else {
179
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
180
+ process.exitCode = 1;
181
+ }
182
+ } catch (err) {
183
+ console.error(chalk.red(`Remove failed: ${err.message}`));
184
+ process.exitCode = 1;
185
+ }
186
+ });
187
+
188
+ cmd
189
+ .command('subscribe <id> <ext-name>')
190
+ .description('subscribe organization to an enterprise extension')
191
+ .action(async (id, extName) => {
192
+ try {
193
+ const res = await apiRequest('POST', `/api/v1/orgs/${encodeURIComponent(id)}/subscriptions`, { extName });
194
+ if (!res) return;
195
+ const data = await res.json();
196
+ if (res.ok && data?.success) {
197
+ console.log(chalk.green(`\u2714 Subscribed to "${extName}"`));
198
+ if (data.price) console.log(` Price: ${chalk.yellow('$' + data.price.toFixed(2) + '/mo')}`);
199
+ } else {
200
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
201
+ process.exitCode = 1;
202
+ }
203
+ } catch (err) {
204
+ console.error(chalk.red(`Subscribe failed: ${err.message}`));
205
+ process.exitCode = 1;
206
+ }
207
+ });
208
+
209
+ cmd
210
+ .command('unsubscribe <id> <ext-name>')
211
+ .description('cancel an extension subscription')
212
+ .action(async (id, extName) => {
213
+ try {
214
+ const res = await apiRequest('DELETE', `/api/v1/orgs/${encodeURIComponent(id)}/subscriptions/${encodeURIComponent(extName)}`);
215
+ if (!res) return;
216
+ const data = await res.json();
217
+ if (res.ok && data?.success) {
218
+ console.log(chalk.green(`\u2714 Unsubscribed from "${extName}"`));
219
+ } else {
220
+ console.error(chalk.red(`Failed: ${data?.error || 'Unknown error'}`));
221
+ process.exitCode = 1;
222
+ }
223
+ } catch (err) {
224
+ console.error(chalk.red(`Unsubscribe failed: ${err.message}`));
225
+ process.exitCode = 1;
226
+ }
227
+ });
228
+
229
+ cmd
230
+ .command('billing <id>')
231
+ .description('show billing summary for an organization')
232
+ .action(async (id) => {
233
+ try {
234
+ const res = await apiRequest('GET', `/api/v1/orgs/${encodeURIComponent(id)}/billing`);
235
+ if (!res) return;
236
+ if (!res.ok) {
237
+ const err = await res.json().catch(() => ({}));
238
+ console.error(chalk.red(`Failed: ${err?.error || 'Unknown error'}`));
239
+ process.exitCode = 1;
240
+ return;
241
+ }
242
+ const data = await res.json();
243
+ console.log('');
244
+ console.log(chalk.cyan.bold('Organization Billing'));
245
+ console.log(chalk.gray('\u2500'.repeat(40)));
246
+ console.log(` Plan: ${chalk.yellow(data.plan || 'free')}`);
247
+ console.log(` Monthly Total: ${chalk.white('$' + (data.monthlyTotal || 0).toFixed(2))}`);
248
+ console.log(` Active Seats: ${chalk.cyan(data.activeSeats || 0)}`);
249
+ console.log(` Subscriptions: ${chalk.cyan(data.subscriptionCount || 0)}`);
250
+
251
+ if (data.subscriptions?.length) {
252
+ console.log('');
253
+ console.log(chalk.cyan(' Active Subscriptions:'));
254
+ for (const sub of data.subscriptions) {
255
+ console.log(` ${chalk.white(sub.extName)} ${chalk.gray('$' + (sub.price || 0).toFixed(2) + '/mo')} ${chalk.gray('since ' + new Date(sub.startDate).toLocaleDateString())}`);
256
+ }
257
+ }
258
+
259
+ if (data.invoices?.length) {
260
+ console.log('');
261
+ console.log(chalk.cyan(' Recent Invoices:'));
262
+ for (const inv of data.invoices) {
263
+ const statusColor = inv.status === 'paid' ? chalk.green : inv.status === 'pending' ? chalk.yellow : chalk.red;
264
+ console.log(` ${chalk.gray(new Date(inv.date).toLocaleDateString())} $${inv.amount.toFixed(2)} ${statusColor(inv.status)}`);
265
+ }
266
+ }
267
+ console.log('');
268
+ } catch (err) {
269
+ console.error(chalk.red(`Billing failed: ${err.message}`));
270
+ process.exitCode = 1;
271
+ }
272
+ });
273
+ }