@apex-actions/cli 0.100.0 → 0.101.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/dist/cli/grant/main.js
CHANGED
|
@@ -5,7 +5,8 @@ import { createGrantBody, createdLine, grantLine, revokedLine, updateGrantBody }
|
|
|
5
5
|
import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
|
|
6
6
|
/**
|
|
7
7
|
* `apex grant`: complimentary access from the terminal (api ADR-0056 as amended by api ADR-0079,
|
|
8
|
-
* TASK-0199). The admin API's own shapes, unchanged.
|
|
8
|
+
* TASK-0199). The admin API's own shapes, unchanged. A grant belongs to an Account and covers every Organization in it
|
|
9
|
+
* but the operator's (decisions-log O66, O73), so `create` names an Account, never an installation. Every write needs a person: the operator token
|
|
9
10
|
* `apex login --console` saved, whose operator the grant row and the audit log name. The machine token
|
|
10
11
|
* (`APEX_ADMIN_TOKEN`) can list and is refused every write, so a leaked one cannot make a free tenant.
|
|
11
12
|
*/
|
|
@@ -57,15 +58,15 @@ const listCommand = new Command('list')
|
|
|
57
58
|
console.log(` ${grantLine(g)}`);
|
|
58
59
|
});
|
|
59
60
|
const createCommand = new Command('create')
|
|
60
|
-
.description(
|
|
61
|
-
.
|
|
61
|
+
.description("Grant an Account complimentary access, replacing any open grant it holds — its Organizations share the minutes; recorded against you")
|
|
62
|
+
.requiredOption('--account <id>', 'the Account the grant belongs to (see: apex tenant installations — connect a fresh installation to an Account first)')
|
|
62
63
|
.requiredOption('--minutes <n>', 'standard minutes per calendar month, 1 to 80,000')
|
|
63
64
|
.requiredOption('--concurrency <n>', 'jobs at once, 1 to 50')
|
|
64
65
|
.requiredOption('--reason <text>', 'who this is for and why they run free, up to 500 characters — stored on the grant')
|
|
65
66
|
.option('--until <YYYY-MM-DD>', 'the grant ends at the end of that UTC day (default: open-ended)')
|
|
66
67
|
.option('--json', 'machine-readable output')
|
|
67
|
-
.action(async (
|
|
68
|
-
const body = createGrantBody(
|
|
68
|
+
.action(async (opts) => {
|
|
69
|
+
const body = createGrantBody(opts.account, opts, new Date());
|
|
69
70
|
if (!body.ok) {
|
|
70
71
|
usage(body.problem);
|
|
71
72
|
return;
|
|
@@ -108,13 +109,13 @@ const updateCommand = new Command('update')
|
|
|
108
109
|
console.log(JSON.stringify(grant, null, 2));
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
|
-
console.log(`${chalk.green('✔')} Changed the grant for ${grant.
|
|
112
|
+
console.log(`${chalk.green('✔')} Changed the grant for ${grant.accountName}.`);
|
|
112
113
|
console.log(` ${grantLine(grant)}`);
|
|
113
114
|
});
|
|
114
115
|
const revokeCommand = new Command('revoke')
|
|
115
116
|
.description('End a grant now: new runs are refused, queued free work is cancelled with a reason, and running work finishes')
|
|
116
117
|
.argument('<grantId>', 'the grant (see: apex grant list)')
|
|
117
|
-
.option('--yes', 'confirm — revoking cannot be undone; the
|
|
118
|
+
.option('--yes', 'confirm — revoking cannot be undone; the Account’s Organizations do not fall back to the pre-trial minutes')
|
|
118
119
|
.option('--json', 'machine-readable output')
|
|
119
120
|
.action(async (id, opts) => {
|
|
120
121
|
if (opts.yes !== true) {
|
package/dist/cli/tenant/main.js
CHANGED
|
@@ -223,7 +223,7 @@ const connectCommand = new Command('connect')
|
|
|
223
223
|
console.log(`${chalk.green('✔')} ${connectLine(outcome.value)}`);
|
|
224
224
|
});
|
|
225
225
|
const installationsCommand = new Command('installations')
|
|
226
|
-
.description("Every installation of the App as GitHub lists it, with what Apex knows of each — --fresh for those the tenants list never shows
|
|
226
|
+
.description("Every installation of the App as GitHub lists it, with what Apex knows of each and the Account it is in — --fresh for those the tenants list never shows")
|
|
227
227
|
.option('--fresh', 'only installations that never ran, paid or were granted')
|
|
228
228
|
.option('--json', 'machine-readable output')
|
|
229
229
|
.action(async (opts) => {
|
|
@@ -253,7 +253,7 @@ const installationsCommand = new Command('installations')
|
|
|
253
253
|
}
|
|
254
254
|
for (const i of outcome.value)
|
|
255
255
|
console.log(` ${installationLine(i)}`);
|
|
256
|
-
console.log(chalk.dim('\
|
|
256
|
+
console.log(chalk.dim('\nA grant belongs to an Account: connect an installation with apex tenant connect, then grant with: apex grant create --account <accountId> --minutes <n> --concurrency <n> --reason "<why>"'));
|
|
257
257
|
});
|
|
258
258
|
export default new Command('tenant')
|
|
259
259
|
.description('Tenants: every installation of the App, connecting new ones to Accounts, who left and deleting their data on request, and the one-time move into Accounts')
|
|
@@ -10,7 +10,7 @@ const attempt = async (fn) => {
|
|
|
10
10
|
/** Every grant ever made, newest first. The control plane decides who may read them. */
|
|
11
11
|
export const listGrants = async (api) => await attempt(() => api.listGrants());
|
|
12
12
|
/**
|
|
13
|
-
* Grant an
|
|
13
|
+
* Grant an Account complimentary access, replacing any open grant it holds (O73). Only a person may: the
|
|
14
14
|
* control plane records the operator the token belongs to, and refuses the machine token.
|
|
15
15
|
*/
|
|
16
16
|
export const createGrant = async (api, body) => await attempt(() => api.createGrant(body));
|
|
@@ -25,7 +25,7 @@ export const backfillLines = (r) => {
|
|
|
25
25
|
if (r.subscriptions.length > 0)
|
|
26
26
|
lines.push('Subscriptions:', ...r.subscriptions.map((s) => ` ${s.owner} (${s.status}): ${SUB_VERB[s.action][t]}`));
|
|
27
27
|
if (r.grants.length > 0) {
|
|
28
|
-
lines.push('
|
|
28
|
+
lines.push('The Account’s open grant — unchanged here; it covers every Organization in the Account:', ...r.grants.map((g) => ` ${g.id}: ${g.minutesPerMonth} minutes a month, ${g.concurrency} concurrent${g.expiresAt === undefined ? '' : `, until ${g.expiresAt.slice(0, 10)}`}`));
|
|
29
29
|
}
|
|
30
30
|
if (r.notes.length > 0)
|
|
31
31
|
lines.push('Notes:', ...r.notes.map((n) => ` ${n}`));
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Complimentary access from the terminal (api ADR-0056 as amended by
|
|
4
4
|
* api ADR-0079, TASK-0199): the grant and installation shapes the admin API
|
|
5
5
|
* answers with, the checks `apex grant` makes before a round trip — the
|
|
6
|
-
* control plane makes them again — and how each reads as a line.
|
|
6
|
+
* control plane makes them again — and how each reads as a line. A grant belongs to an Account and covers
|
|
7
|
+
* its Organizations, the operator's left out (decisions-log O66, O73).
|
|
7
8
|
*/
|
|
8
9
|
/** The admin API's limits on a grant (control-plane `domain/grant.ts`): the largest plan's — Business's 80,000 since the re-pricing (O48). */
|
|
9
10
|
export const MAX_GRANT_MINUTES = 80_000;
|
|
@@ -31,11 +32,13 @@ export const endOfDay = (day, now) => {
|
|
|
31
32
|
};
|
|
32
33
|
const minutesProblem = `--minutes must be a whole number from 1 to ${MAX_GRANT_MINUTES.toLocaleString('en-US')}`;
|
|
33
34
|
const concurrencyProblem = `--concurrency must be a whole number from 1 to ${MAX_GRANT_CONCURRENCY}`;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/** An Account id is the control plane's own text; this only refuses one that cannot be. */
|
|
36
|
+
const MAX_ACCOUNT_ID = 200;
|
|
37
|
+
/** `apex grant create`'s arguments, checked: `account` is `--account`, the Account the grant belongs to (O73). */
|
|
38
|
+
export const createGrantBody = (account, flags, now) => {
|
|
39
|
+
const accountId = account?.trim() ?? '';
|
|
40
|
+
if (accountId === '' || accountId.length > MAX_ACCOUNT_ID || /\s/.test(accountId))
|
|
41
|
+
return { ok: false, problem: `--account must be an Account id (see: apex tenant installations), got ${JSON.stringify(account ?? '')}` };
|
|
39
42
|
const minutesPerMonth = whole(flags.minutes, 1, MAX_GRANT_MINUTES);
|
|
40
43
|
if (minutesPerMonth === undefined)
|
|
41
44
|
return { ok: false, problem: minutesProblem };
|
|
@@ -46,9 +49,9 @@ export const createGrantBody = (installation, flags, now) => {
|
|
|
46
49
|
if (reason === '' || reason.length > MAX_GRANT_REASON)
|
|
47
50
|
return { ok: false, problem: `--reason is required, up to ${MAX_GRANT_REASON} characters — who this is for, and why they run free` };
|
|
48
51
|
if (flags.until === undefined)
|
|
49
|
-
return { ok: true, value: {
|
|
52
|
+
return { ok: true, value: { accountId, minutesPerMonth, concurrency, reason } };
|
|
50
53
|
const until = endOfDay(flags.until, now);
|
|
51
|
-
return until.ok ? { ok: true, value: {
|
|
54
|
+
return until.ok ? { ok: true, value: { accountId, minutesPerMonth, concurrency, reason, expiresAt: until.value } } : until;
|
|
52
55
|
};
|
|
53
56
|
/** `apex grant update`'s flags, checked: at least one change, each well-formed, and not both an end date and none. */
|
|
54
57
|
export const updateGrantBody = (flags, now) => {
|
|
@@ -80,18 +83,22 @@ export const updateGrantBody = (flags, now) => {
|
|
|
80
83
|
return { ok: true, value: body };
|
|
81
84
|
};
|
|
82
85
|
const day = (iso) => iso?.slice(0, 10);
|
|
86
|
+
/** The Organizations a grant covers, by login — or that it covers none yet. */
|
|
87
|
+
export const coveredLine = (g) => g.organizations.length === 0 ? 'no Organization yet' : g.organizations.map((o) => o.login).join(', ');
|
|
88
|
+
/** Whose a grant is: the Account, and the Organizations that share it. */
|
|
89
|
+
const whose = (g) => `${g.accountName} (Account ${g.accountId}; covers ${coveredLine(g)})`;
|
|
83
90
|
/** One grant as a line: whose, how it stands, the month so far, and who granted it and why. */
|
|
84
91
|
export const grantLine = (g) => {
|
|
85
92
|
const used = g.usedMinutes === undefined ? '' : `${Math.floor(g.usedMinutes).toLocaleString('en-US')} of `;
|
|
86
93
|
const state = g.state === 'ended' ? `ended ${day(g.endedAt ?? g.revokedAt) ?? ''}${g.revokedBy === undefined ? '' : ` by ${g.revokedBy}`}`.trim() : g.dormant ? `${g.state}, dormant — they pay for a plan` : g.state;
|
|
87
94
|
const until = g.expiresAt === undefined ? '' : `, until ${day(g.expiresAt) ?? ''}`;
|
|
88
|
-
return `${g.id} ${g.
|
|
95
|
+
return `${g.id} ${whose(g)}${g.madeOn === undefined ? '' : `, made on ${g.madeOn}`} — ${state}; ${used}${g.minutesPerMonth.toLocaleString('en-US')} minutes a month, ${g.concurrency} at once${until}; granted by ${g.grantedBy}: ${g.reason}`;
|
|
89
96
|
};
|
|
90
97
|
/** What a create means, in the Console's words: open, a replacement, or dormant behind a subscription. */
|
|
91
98
|
export const createdLine = (c) => c.dormant
|
|
92
|
-
? `Granted to ${c.grant.
|
|
93
|
-
: `Granted to ${c.grant.
|
|
94
|
-
export const revokedLine = (r) => `Revoked the grant for ${r.grant.
|
|
99
|
+
? `Granted to ${c.grant.accountName}. They pay for a plan, so the grant is dormant until they stop.`
|
|
100
|
+
: `Granted to ${c.grant.accountName}.${c.superseded === undefined ? '' : ' It replaces the grant the Account held.'}`;
|
|
101
|
+
export const revokedLine = (r) => `Revoked the grant for ${r.grant.accountName}.${r.cancelledJobs > 0 ? ` ${r.cancelledJobs} queued job${r.cancelledJobs === 1 ? ' was' : 's were'} cancelled.` : ''}`;
|
|
95
102
|
/** One installation as a line: whose, and what Apex knows of it — or that it is new. */
|
|
96
103
|
export const installationLine = (i) => {
|
|
97
104
|
const known = [
|