lh-ops 0.1.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 +93 -0
- package/bin/lh-ops +2 -0
- package/dist/commands/audit.js +66 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/cache-coverage.js +54 -0
- package/dist/commands/cache-coverage.js.map +1 -0
- package/dist/commands/export.js +138 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/login.js +50 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/member.js +25 -0
- package/dist/commands/member.js.map +1 -0
- package/dist/commands/models.js +23 -0
- package/dist/commands/models.js.map +1 -0
- package/dist/commands/org-list.js +45 -0
- package/dist/commands/org-list.js.map +1 -0
- package/dist/commands/org-usage.js +77 -0
- package/dist/commands/org-usage.js.map +1 -0
- package/dist/commands/org.js +72 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/phase2-status.js +41 -0
- package/dist/commands/phase2-status.js.map +1 -0
- package/dist/commands/plan.js +70 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/pricebook.js +39 -0
- package/dist/commands/pricebook.js.map +1 -0
- package/dist/commands/scanners.js +33 -0
- package/dist/commands/scanners.js.map +1 -0
- package/dist/commands/statement.js +101 -0
- package/dist/commands/statement.js.map +1 -0
- package/dist/commands/subscription.js +32 -0
- package/dist/commands/subscription.js.map +1 -0
- package/dist/commands/token.js +61 -0
- package/dist/commands/token.js.map +1 -0
- package/dist/commands/topup.js +34 -0
- package/dist/commands/topup.js.map +1 -0
- package/dist/index.js +795 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/client.js +11 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/credentials.js +68 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/errors.js +45 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/output.js +41 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/resolve-client.js +42 -0
- package/dist/lib/resolve-client.js.map +1 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const commander_1 = require("commander");
|
|
37
|
+
const client_1 = require("./lib/client");
|
|
38
|
+
const errors_1 = require("./lib/errors");
|
|
39
|
+
const resolve_client_1 = require("./lib/resolve-client");
|
|
40
|
+
const output_1 = require("./lib/output");
|
|
41
|
+
const login_1 = require("./commands/login");
|
|
42
|
+
const plan_1 = require("./commands/plan");
|
|
43
|
+
const pricebook_1 = require("./commands/pricebook");
|
|
44
|
+
const org_1 = require("./commands/org");
|
|
45
|
+
const org_list_1 = require("./commands/org-list");
|
|
46
|
+
const statement_1 = require("./commands/statement");
|
|
47
|
+
const org_usage_1 = require("./commands/org-usage");
|
|
48
|
+
const cache_coverage_1 = require("./commands/cache-coverage");
|
|
49
|
+
const scanners_1 = require("./commands/scanners");
|
|
50
|
+
const export_1 = require("./commands/export");
|
|
51
|
+
const phase2_status_1 = require("./commands/phase2-status");
|
|
52
|
+
const subscription_1 = require("./commands/subscription");
|
|
53
|
+
const models_1 = require("./commands/models");
|
|
54
|
+
const topup_1 = require("./commands/topup");
|
|
55
|
+
const member_1 = require("./commands/member");
|
|
56
|
+
const audit_1 = require("./commands/audit");
|
|
57
|
+
const token_1 = require("./commands/token");
|
|
58
|
+
// LH backend mounts all routes under the /api global prefix, so the operator-
|
|
59
|
+
// facing API base ends with /api. Override via LH_OPS_API_BASE.
|
|
60
|
+
const DEFAULT_API_BASE = process.env.LH_OPS_API_BASE ?? 'http://localhost:3002/api';
|
|
61
|
+
function commonAuthOptions(cmd) {
|
|
62
|
+
return cmd
|
|
63
|
+
// normalizeApiBase coerces a bare origin (e.g. https://launchhill.com) to
|
|
64
|
+
// include the /api global prefix so operators needn't remember the suffix.
|
|
65
|
+
// #597 — default is LH_OPS_API_BASE env ONLY (not localhost): when the env
|
|
66
|
+
// is unset + no --api-base, opts.apiBase is undefined so resolveClient falls
|
|
67
|
+
// to the apiBase saved in ~/.lh-ops/credentials.json. A localhost default
|
|
68
|
+
// here would shadow stored creds and silently hit local (the footgun).
|
|
69
|
+
.option('--api-base <url>', 'override API base URL', resolve_client_1.normalizeApiBase, process.env.LH_OPS_API_BASE)
|
|
70
|
+
.option('--token <jwt>', 'override saved access token (skip login)')
|
|
71
|
+
.option('--json', 'emit JSON output instead of human-readable');
|
|
72
|
+
}
|
|
73
|
+
async function run(action) {
|
|
74
|
+
try {
|
|
75
|
+
await action();
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof client_1.ApiError) {
|
|
79
|
+
const detail = (0, errors_1.extractApiMessage)(err.body);
|
|
80
|
+
process.stderr.write(`Error ${err.status}: ${detail ?? err.message}\n`);
|
|
81
|
+
const hint = (0, errors_1.hintForApiError)(err);
|
|
82
|
+
if (hint)
|
|
83
|
+
process.stderr.write(`Hint: ${hint}\n`);
|
|
84
|
+
// Only dump the raw body when we couldn't surface a clean message from it.
|
|
85
|
+
if (!detail)
|
|
86
|
+
process.stderr.write(JSON.stringify(err.body, null, 2) + '\n');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const e = err;
|
|
90
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
91
|
+
}
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// T5 of v0.1.7 #279 / #283 — `--since 90d` parser. Accepts `<digits>d`
|
|
96
|
+
// for now; weeks/hours can be added later. Defaults to 90 if unparseable
|
|
97
|
+
// rather than throwing — keeps the CLI forgiving for the common case.
|
|
98
|
+
function parseSinceDays(since) {
|
|
99
|
+
const m = /^(\d+)d$/.exec(since);
|
|
100
|
+
if (!m) {
|
|
101
|
+
throw new Error(`--since must match <digits>d (e.g. 90d), got: ${since}`);
|
|
102
|
+
}
|
|
103
|
+
return parseInt(m[1], 10);
|
|
104
|
+
}
|
|
105
|
+
// T5 of v0.1.7 #279 / #283 — emit export body. `--output <path>` writes
|
|
106
|
+
// to file (atomic semantics not required for one-shot export); otherwise
|
|
107
|
+
// stdout. JSON gets a trailing newline so shells render it cleanly;
|
|
108
|
+
// CSV's renderer already appends \n.
|
|
109
|
+
async function emitExport(body, outputPath, isCsv) {
|
|
110
|
+
if (outputPath) {
|
|
111
|
+
const { promises: fsp } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
112
|
+
await fsp.writeFile(outputPath, body);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write(body + (isCsv ? '' : '\n'));
|
|
116
|
+
}
|
|
117
|
+
const program = new commander_1.Command();
|
|
118
|
+
program.name('lh-ops').version('0.1.0').description('LH Platform Operator CLI');
|
|
119
|
+
// ===== login =====
|
|
120
|
+
const loginCmd = program
|
|
121
|
+
.command('login')
|
|
122
|
+
.description('Authenticate as a Platform Operator and persist the access token')
|
|
123
|
+
.requiredOption('--email <email>')
|
|
124
|
+
.requiredOption('--password <password>')
|
|
125
|
+
.option('--api-base <url>', 'API base URL', resolve_client_1.normalizeApiBase, DEFAULT_API_BASE)
|
|
126
|
+
.option('--json', 'emit JSON output')
|
|
127
|
+
.action(async (opts) => {
|
|
128
|
+
await run(async () => {
|
|
129
|
+
const result = await (0, login_1.login)({
|
|
130
|
+
apiBase: opts.apiBase,
|
|
131
|
+
email: opts.email,
|
|
132
|
+
password: opts.password,
|
|
133
|
+
json: !!opts.json,
|
|
134
|
+
});
|
|
135
|
+
(0, login_1.renderLogin)(result, !!opts.json);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
void loginCmd;
|
|
139
|
+
// ===== plan =====
|
|
140
|
+
const plan = program.command('plan').description('Manage plan SKUs');
|
|
141
|
+
commonAuthOptions(plan.command('publish'))
|
|
142
|
+
.description('Create a plan from a JSON file')
|
|
143
|
+
.requiredOption('--file <path>', 'path to JSON dto')
|
|
144
|
+
.action(async (opts) => {
|
|
145
|
+
await run(async () => {
|
|
146
|
+
const result = await (0, plan_1.publishPlan)({
|
|
147
|
+
filePath: opts.file,
|
|
148
|
+
apiBase: opts.apiBase,
|
|
149
|
+
token: opts.token,
|
|
150
|
+
});
|
|
151
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
commonAuthOptions(plan.command('list'))
|
|
155
|
+
.description('List plan SKUs (US-1)')
|
|
156
|
+
.option('--family <family>', 'filter by plan family (OFFICE | CODING | ...)')
|
|
157
|
+
.option('--status <status>', 'filter by status (ACTIVE | ARCHIVED)')
|
|
158
|
+
.action(async (opts) => {
|
|
159
|
+
await run(async () => {
|
|
160
|
+
const plans = await (0, plan_1.listPlans)({
|
|
161
|
+
family: opts.family,
|
|
162
|
+
status: opts.status,
|
|
163
|
+
apiBase: opts.apiBase,
|
|
164
|
+
token: opts.token,
|
|
165
|
+
});
|
|
166
|
+
process.stdout.write((0, plan_1.renderPlanList)(plans, { json: !!opts.json }));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
commonAuthOptions(plan.command('show <planId>'))
|
|
170
|
+
.description('Show a plan incl. quotas + updatedAt (the --if-match lock token)')
|
|
171
|
+
.action(async (planId, opts) => {
|
|
172
|
+
await run(async () => {
|
|
173
|
+
const p = await (0, plan_1.showPlan)({
|
|
174
|
+
planId,
|
|
175
|
+
apiBase: opts.apiBase,
|
|
176
|
+
token: opts.token,
|
|
177
|
+
});
|
|
178
|
+
process.stdout.write((0, plan_1.renderPlan)(p, { json: !!opts.json }));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
commonAuthOptions(plan.command('update <planId>'))
|
|
182
|
+
.description('Update a plan from a JSON file (optimistic lock via --if-match)')
|
|
183
|
+
.requiredOption('--file <path>', 'JSON dto: displayName / quotas / status / isBaseTier')
|
|
184
|
+
.requiredOption('--if-match <updatedAt>', "plan's current updatedAt (copy from `plan show`)")
|
|
185
|
+
.action(async (planId, opts) => {
|
|
186
|
+
await run(async () => {
|
|
187
|
+
const result = await (0, plan_1.updatePlan)({
|
|
188
|
+
planId,
|
|
189
|
+
filePath: opts.file,
|
|
190
|
+
ifMatch: opts.ifMatch,
|
|
191
|
+
apiBase: opts.apiBase,
|
|
192
|
+
token: opts.token,
|
|
193
|
+
});
|
|
194
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
commonAuthOptions(plan.command('archive <planId>'))
|
|
198
|
+
.description('Archive a plan — ARCHIVED (optimistic lock via --if-match)')
|
|
199
|
+
.requiredOption('--if-match <updatedAt>', "plan's current updatedAt (copy from `plan show`)")
|
|
200
|
+
.action(async (planId, opts) => {
|
|
201
|
+
await run(async () => {
|
|
202
|
+
const result = await (0, plan_1.archivePlan)({
|
|
203
|
+
planId,
|
|
204
|
+
ifMatch: opts.ifMatch,
|
|
205
|
+
apiBase: opts.apiBase,
|
|
206
|
+
token: opts.token,
|
|
207
|
+
});
|
|
208
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// ===== pricebook =====
|
|
212
|
+
const pb = program.command('pricebook').description('Manage versioned price books');
|
|
213
|
+
commonAuthOptions(pb.command('publish'))
|
|
214
|
+
.description('Create a DRAFT price book version from a JSON file')
|
|
215
|
+
.requiredOption('--file <path>')
|
|
216
|
+
.action(async (opts) => {
|
|
217
|
+
await run(async () => {
|
|
218
|
+
const result = await (0, pricebook_1.publishPriceBook)({
|
|
219
|
+
filePath: opts.file,
|
|
220
|
+
apiBase: opts.apiBase,
|
|
221
|
+
token: opts.token,
|
|
222
|
+
});
|
|
223
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
commonAuthOptions(pb.command('activate <version>'))
|
|
227
|
+
.description('Activate a price book version (forward-only; pass --allow-removals to drop models)')
|
|
228
|
+
.option('--allow-removals', 'permit dropping models present in current ACTIVE')
|
|
229
|
+
.action(async (version, opts) => {
|
|
230
|
+
await run(async () => {
|
|
231
|
+
const result = await (0, pricebook_1.activatePriceBook)({
|
|
232
|
+
version,
|
|
233
|
+
allowRemovals: !!opts.allowRemovals,
|
|
234
|
+
apiBase: opts.apiBase,
|
|
235
|
+
token: opts.token,
|
|
236
|
+
});
|
|
237
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
commonAuthOptions(pb.command('list'))
|
|
241
|
+
.description('List price book versions (US-2)')
|
|
242
|
+
.action(async (opts) => {
|
|
243
|
+
await run(async () => {
|
|
244
|
+
const rows = await (0, pricebook_1.listPriceBooks)({
|
|
245
|
+
apiBase: opts.apiBase,
|
|
246
|
+
token: opts.token,
|
|
247
|
+
});
|
|
248
|
+
process.stdout.write((0, pricebook_1.renderPriceBookList)(rows, { json: !!opts.json }));
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
// ===== cache-coverage (#595) =====
|
|
252
|
+
commonAuthOptions(program.command('cache-coverage'))
|
|
253
|
+
.description('Platform-wide cache-hit audit by model (#595) — which models return prompt-cache tokens + hit rate')
|
|
254
|
+
.option('--format <fmt>', 'md (default) | json', 'md')
|
|
255
|
+
.action(async (opts) => {
|
|
256
|
+
await run(async () => {
|
|
257
|
+
const coverage = await (0, cache_coverage_1.getCacheCoverage)({
|
|
258
|
+
apiBase: opts.apiBase,
|
|
259
|
+
token: opts.token,
|
|
260
|
+
});
|
|
261
|
+
const body = (0, cache_coverage_1.renderCacheCoverage)(coverage, { format: opts.format ?? 'md' });
|
|
262
|
+
process.stdout.write(body + '\n');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
// ===== models (canary prep #445) =====
|
|
266
|
+
const models = program
|
|
267
|
+
.command('models')
|
|
268
|
+
.description('Inspect the upstream model catalog');
|
|
269
|
+
commonAuthOptions(models.command('list'))
|
|
270
|
+
.description('List models the upstream new-api offers — the canonical names for plan authoring')
|
|
271
|
+
.action(async (opts) => {
|
|
272
|
+
await run(async () => {
|
|
273
|
+
const rows = await (0, models_1.listModels)({
|
|
274
|
+
apiBase: opts.apiBase,
|
|
275
|
+
token: opts.token,
|
|
276
|
+
});
|
|
277
|
+
process.stdout.write((0, models_1.renderModels)(rows, { json: !!opts.json }));
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
// ===== token (#304 / #473) — service-account tokens for agents / MCP =====
|
|
281
|
+
const token = program
|
|
282
|
+
.command('token')
|
|
283
|
+
.description('Manage service-account tokens (lh_pat_…) for agents / MCP');
|
|
284
|
+
commonAuthOptions(token.command('issue'))
|
|
285
|
+
.description('Issue a scoped service-account token (plaintext shown ONCE)')
|
|
286
|
+
.requiredOption('--scopes <list>', 'comma-separated scopes, e.g. read:orgs,read:billing,read:keys,read:audit,write:keys')
|
|
287
|
+
.option('--label <label>', 'human label shown in `token list`')
|
|
288
|
+
.option('--expires-days <n>', 'token lifetime in days (omit = non-expiring)')
|
|
289
|
+
.action(async (opts) => {
|
|
290
|
+
await run(async () => {
|
|
291
|
+
const result = await (0, token_1.issueToken)({
|
|
292
|
+
scopes: opts.scopes
|
|
293
|
+
.split(',')
|
|
294
|
+
.map((s) => s.trim())
|
|
295
|
+
.filter((s) => s.length > 0),
|
|
296
|
+
label: opts.label,
|
|
297
|
+
expiresInDays: opts.expiresDays !== undefined
|
|
298
|
+
? parseInt(opts.expiresDays, 10)
|
|
299
|
+
: undefined,
|
|
300
|
+
apiBase: opts.apiBase,
|
|
301
|
+
token: opts.token,
|
|
302
|
+
});
|
|
303
|
+
process.stdout.write((0, token_1.renderIssuedToken)(result, { json: !!opts.json }));
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
commonAuthOptions(token.command('list'))
|
|
307
|
+
.description('List issued service-account tokens (never shows the secret)')
|
|
308
|
+
.action(async (opts) => {
|
|
309
|
+
await run(async () => {
|
|
310
|
+
const tokens = await (0, token_1.listTokens)({
|
|
311
|
+
apiBase: opts.apiBase,
|
|
312
|
+
token: opts.token,
|
|
313
|
+
});
|
|
314
|
+
process.stdout.write((0, token_1.renderTokenList)(tokens, { json: !!opts.json }));
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
commonAuthOptions(token.command('revoke <tokenId>'))
|
|
318
|
+
.description('Revoke a service-account token by id')
|
|
319
|
+
.action(async (tokenId, opts) => {
|
|
320
|
+
await run(async () => {
|
|
321
|
+
const result = await (0, token_1.revokeToken)({
|
|
322
|
+
tokenId,
|
|
323
|
+
apiBase: opts.apiBase,
|
|
324
|
+
token: opts.token,
|
|
325
|
+
});
|
|
326
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
// ===== org =====
|
|
330
|
+
const org = program.command('org').description('Manage customer orgs');
|
|
331
|
+
commonAuthOptions(org.command('create'))
|
|
332
|
+
.description('Create an org and assign its initial Enterprise Admin (US-12)')
|
|
333
|
+
.requiredOption('--name <orgName>')
|
|
334
|
+
.requiredOption('--admin-email <email>')
|
|
335
|
+
.requiredOption('--admin-name <name>')
|
|
336
|
+
.option('--plan <planId>')
|
|
337
|
+
.action(async (opts) => {
|
|
338
|
+
await run(async () => {
|
|
339
|
+
const result = await (0, org_1.createOrg)({
|
|
340
|
+
orgName: opts.name,
|
|
341
|
+
adminEmail: opts.adminEmail,
|
|
342
|
+
adminName: opts.adminName,
|
|
343
|
+
planId: opts.plan,
|
|
344
|
+
apiBase: opts.apiBase,
|
|
345
|
+
token: opts.token,
|
|
346
|
+
});
|
|
347
|
+
if (opts.json) {
|
|
348
|
+
(0, output_1.printValue)(result, { json: true });
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
(0, output_1.printValue)({
|
|
352
|
+
orgId: result.organization.id,
|
|
353
|
+
orgName: result.organization.name,
|
|
354
|
+
adminEmail: result.adminUser.email,
|
|
355
|
+
subscriptionId: result.subscription?.id ?? null,
|
|
356
|
+
}, { json: false });
|
|
357
|
+
(0, output_1.printSecret)('TEMP PASSWORD for first login', result.tempPassword);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
commonAuthOptions(org.command('show <orgId>'))
|
|
362
|
+
.description('Show org details + active subscription + month-to-date usage')
|
|
363
|
+
.action(async (orgId, opts) => {
|
|
364
|
+
await run(async () => {
|
|
365
|
+
const result = await (0, org_1.showOrg)({
|
|
366
|
+
orgId,
|
|
367
|
+
apiBase: opts.apiBase,
|
|
368
|
+
token: opts.token,
|
|
369
|
+
});
|
|
370
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
commonAuthOptions(org.command('rename <orgId>'))
|
|
374
|
+
.description('Rename a customer org (intent 1)')
|
|
375
|
+
.requiredOption('--name <newName>', 'new org display name')
|
|
376
|
+
.action(async (orgId, opts) => {
|
|
377
|
+
await run(async () => {
|
|
378
|
+
const result = await (0, org_1.renameOrg)({
|
|
379
|
+
orgId,
|
|
380
|
+
name: opts.name,
|
|
381
|
+
apiBase: opts.apiBase,
|
|
382
|
+
token: opts.token,
|
|
383
|
+
});
|
|
384
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
commonAuthOptions(org.command('tokens <orgId>'))
|
|
388
|
+
.description("Show the org's new-api token state for triage (US-7 read; reset/recreate deferred per ADR-020 §4)")
|
|
389
|
+
.action(async (orgId, opts) => {
|
|
390
|
+
await run(async () => {
|
|
391
|
+
const rows = await (0, org_1.listOrgTokens)({
|
|
392
|
+
orgId,
|
|
393
|
+
apiBase: opts.apiBase,
|
|
394
|
+
token: opts.token,
|
|
395
|
+
});
|
|
396
|
+
process.stdout.write((0, org_1.renderOrgTokens)(rows, { json: !!opts.json }));
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
commonAuthOptions(org.command('suspend <orgId>'))
|
|
400
|
+
.description('Suspend an org — relay rejects existing api-keys (#27)')
|
|
401
|
+
.option('--reason <text>', 'free-form note recorded in audit log')
|
|
402
|
+
.action(async (orgId, opts) => {
|
|
403
|
+
await run(async () => {
|
|
404
|
+
const result = await (0, org_1.suspendOrg)({
|
|
405
|
+
orgId,
|
|
406
|
+
reason: opts.reason,
|
|
407
|
+
apiBase: opts.apiBase,
|
|
408
|
+
token: opts.token,
|
|
409
|
+
});
|
|
410
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
// US-OP-9 (T2 of v0.1.7 #279 / #280) — cross-org overview.
|
|
414
|
+
// Filter cycles (C3-C6 on backend, mirrored here): --near-limit / --over-budget
|
|
415
|
+
// / --quiet-since-days / --sort-by. All are pass-through to the service.
|
|
416
|
+
commonAuthOptions(org.command('list'))
|
|
417
|
+
.description('List customer orgs (US-OP-9) — Day-1-to-7 ops monitoring')
|
|
418
|
+
.option('--status <status>', 'ACTIVE | SUSPENDED | CLOSED (default ACTIVE)')
|
|
419
|
+
.option('--near-limit', 'orgs with any sub ≥ 70% of maxKeys active')
|
|
420
|
+
.option('--over-budget', 'orgs with balance ≤ 0')
|
|
421
|
+
.option('--quiet-since-days <n>', 'orgs with no usage in the last N days (incl. never-used)')
|
|
422
|
+
.option('--expiring-within <Xd>', 'orgs with any ACTIVE non-base sub commitmentEnd within X days (e.g. 14d)')
|
|
423
|
+
.option('--sort-by <key>', 'balance-asc')
|
|
424
|
+
.action(async (opts) => {
|
|
425
|
+
await run(async () => {
|
|
426
|
+
const quietDays = opts.quietSinceDays !== undefined && /^\d+$/.test(opts.quietSinceDays)
|
|
427
|
+
? parseInt(opts.quietSinceDays, 10)
|
|
428
|
+
: undefined;
|
|
429
|
+
const expiringDays = opts.expiringWithin !== undefined
|
|
430
|
+
? parseSinceDays(opts.expiringWithin)
|
|
431
|
+
: undefined;
|
|
432
|
+
const rows = await (0, org_list_1.listOrgs)({
|
|
433
|
+
status: opts.status,
|
|
434
|
+
nearLimit: opts.nearLimit,
|
|
435
|
+
overBudget: opts.overBudget,
|
|
436
|
+
quietSinceDays: quietDays,
|
|
437
|
+
expiringWithinDays: expiringDays,
|
|
438
|
+
sortBy: opts.sortBy === 'balance-asc' ? 'balance-asc' : undefined,
|
|
439
|
+
apiBase: opts.apiBase,
|
|
440
|
+
token: opts.token,
|
|
441
|
+
});
|
|
442
|
+
process.stdout.write((0, org_list_1.renderOrgList)(rows, { json: !!opts.json }));
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
// US-OP-10 (T3 of v0.1.7 #279 / #281) — monthly checkpoint statement.
|
|
446
|
+
// PR B (2026-05-18) added md + csv renderers; server stays JSON-only,
|
|
447
|
+
// presentation is dispatched client-side via `renderStatement`.
|
|
448
|
+
commonAuthOptions(org.command('statement'))
|
|
449
|
+
.description('Generate a monthly statement for an org (US-OP-10) — sales reconciliation')
|
|
450
|
+
.requiredOption('--org <orgId>')
|
|
451
|
+
.requiredOption('--month <YYYY-MM>')
|
|
452
|
+
.option('--format <fmt>', 'json | md | csv', 'json')
|
|
453
|
+
.option('--output <path>', 'write rendered output to file instead of stdout')
|
|
454
|
+
.action(async (opts) => {
|
|
455
|
+
await run(async () => {
|
|
456
|
+
const stmt = await (0, statement_1.getStatement)({
|
|
457
|
+
orgId: opts.org,
|
|
458
|
+
month: opts.month,
|
|
459
|
+
apiBase: opts.apiBase,
|
|
460
|
+
token: opts.token,
|
|
461
|
+
});
|
|
462
|
+
const body = (0, statement_1.renderStatement)(stmt, { format: opts.format ?? 'json' });
|
|
463
|
+
if (opts.output) {
|
|
464
|
+
const { promises: fsp } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
465
|
+
await fsp.writeFile(opts.output, body);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
process.stdout.write(body + '\n');
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
// #589 — all-time usage reconciliation (counterpart to `statement` monthly).
|
|
473
|
+
// "How much have they used / what's left" view: balance + deposits/charges +
|
|
474
|
+
// usage by billing-decision + by model.
|
|
475
|
+
commonAuthOptions(org.command('usage'))
|
|
476
|
+
.description('All-time usage reconciliation for an org (#589) — balance, deposits/charges, by-model')
|
|
477
|
+
.requiredOption('--org <orgId>')
|
|
478
|
+
.option('--format <fmt>', 'md (default) | json', 'md')
|
|
479
|
+
.action(async (opts) => {
|
|
480
|
+
await run(async () => {
|
|
481
|
+
const summary = await (0, org_usage_1.getOrgUsage)({
|
|
482
|
+
orgId: opts.org,
|
|
483
|
+
apiBase: opts.apiBase,
|
|
484
|
+
token: opts.token,
|
|
485
|
+
});
|
|
486
|
+
const body = (0, org_usage_1.renderOrgUsage)(summary, { format: opts.format ?? 'md' });
|
|
487
|
+
process.stdout.write(body + '\n');
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
// T5 of v0.1.7 #279 / #283 — compliance export (US-OP-12).
|
|
491
|
+
// Two siblings: audit + keys. Both emit chain-of-custody sha256.
|
|
492
|
+
commonAuthOptions(org.command('export-audit'))
|
|
493
|
+
.description('Export every org-scoped audit entry over a window (US-OP-12 compliance)')
|
|
494
|
+
.requiredOption('--org <orgId>')
|
|
495
|
+
.option('--since <duration>', 'window length, e.g. 90d (default 90d)', '90d')
|
|
496
|
+
.option('--format <fmt>', 'csv (default) | json', 'csv')
|
|
497
|
+
.option('--output <path>', 'write to file instead of stdout')
|
|
498
|
+
.action(async (opts) => {
|
|
499
|
+
await run(async () => {
|
|
500
|
+
const days = parseSinceDays(opts.since ?? '90d');
|
|
501
|
+
const to = new Date();
|
|
502
|
+
const from = new Date(to.getTime() - days * 24 * 60 * 60 * 1000);
|
|
503
|
+
const rows = await (0, export_1.exportAudit)({
|
|
504
|
+
orgId: opts.org,
|
|
505
|
+
from: from.toISOString(),
|
|
506
|
+
to: to.toISOString(),
|
|
507
|
+
apiBase: opts.apiBase,
|
|
508
|
+
token: opts.token,
|
|
509
|
+
});
|
|
510
|
+
const body = opts.format === 'json'
|
|
511
|
+
? (0, export_1.renderAuditExportJson)(rows)
|
|
512
|
+
: (0, export_1.renderAuditExportCsv)(rows);
|
|
513
|
+
await emitExport(body, opts.output, opts.format !== 'json');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
commonAuthOptions(org.command('export-keys'))
|
|
517
|
+
.description('Export every api_key (incl. REVOKED) for an org (US-OP-12 compliance)')
|
|
518
|
+
.requiredOption('--org <orgId>')
|
|
519
|
+
.option('--format <fmt>', 'csv (default) | json', 'csv')
|
|
520
|
+
.option('--output <path>', 'write to file instead of stdout')
|
|
521
|
+
.action(async (opts) => {
|
|
522
|
+
await run(async () => {
|
|
523
|
+
const rows = await (0, export_1.exportKeys)({
|
|
524
|
+
orgId: opts.org,
|
|
525
|
+
apiBase: opts.apiBase,
|
|
526
|
+
token: opts.token,
|
|
527
|
+
});
|
|
528
|
+
const body = opts.format === 'json'
|
|
529
|
+
? (0, export_1.renderKeysExportJson)(rows)
|
|
530
|
+
: (0, export_1.renderKeysExportCsv)(rows);
|
|
531
|
+
await emitExport(body, opts.output, opts.format !== 'json');
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
commonAuthOptions(org.command('activate <orgId>'))
|
|
535
|
+
.description('Reactivate a suspended org (#27)')
|
|
536
|
+
.option('--reason <text>', 'free-form note recorded in audit log')
|
|
537
|
+
.action(async (orgId, opts) => {
|
|
538
|
+
await run(async () => {
|
|
539
|
+
const result = await (0, org_1.activateOrg)({
|
|
540
|
+
orgId,
|
|
541
|
+
reason: opts.reason,
|
|
542
|
+
apiBase: opts.apiBase,
|
|
543
|
+
token: opts.token,
|
|
544
|
+
});
|
|
545
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
// ===== subscription =====
|
|
549
|
+
const subscription = program
|
|
550
|
+
.command('subscription')
|
|
551
|
+
.description('Manage org subscriptions (operator-only sales motion)');
|
|
552
|
+
commonAuthOptions(subscription.command('add <orgId>'))
|
|
553
|
+
.description('Add an additional subscription to an existing org (issue #83 — change order)')
|
|
554
|
+
.requiredOption('--plan-id <planId>')
|
|
555
|
+
.requiredOption('--max-keys <n>', 'API key cap for this subscription')
|
|
556
|
+
.requiredOption('--contract-start <iso>', 'ISO8601 contract start')
|
|
557
|
+
.requiredOption('--commitment-end <iso>', 'ISO8601 commitment end (must be in the future)')
|
|
558
|
+
.option('--price-book-version <version>', 'pin to a specific PriceBook version (default: capture current ACTIVE)')
|
|
559
|
+
.action(async (orgId, opts) => {
|
|
560
|
+
await run(async () => {
|
|
561
|
+
const maxKeys = Number(opts.maxKeys);
|
|
562
|
+
if (!Number.isFinite(maxKeys) || !Number.isInteger(maxKeys)) {
|
|
563
|
+
throw new Error(`--max-keys must be an integer, got: ${opts.maxKeys}`);
|
|
564
|
+
}
|
|
565
|
+
const result = await (0, subscription_1.addSubscription)({
|
|
566
|
+
orgId,
|
|
567
|
+
planId: opts.planId,
|
|
568
|
+
maxKeys,
|
|
569
|
+
contractStart: opts.contractStart,
|
|
570
|
+
commitmentEnd: opts.commitmentEnd,
|
|
571
|
+
priceBookVersion: opts.priceBookVersion,
|
|
572
|
+
apiBase: opts.apiBase,
|
|
573
|
+
token: opts.token,
|
|
574
|
+
});
|
|
575
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
commonAuthOptions(subscription.command('extend <orgId> <subId>'))
|
|
579
|
+
.description("Extend a subscription's contract commitmentEnd (US-8 renewal; tier changes go via add + cancel)")
|
|
580
|
+
.requiredOption('--new-end <iso>', 'new ISO8601 commitmentEnd (must be in the future + later than current)')
|
|
581
|
+
.option('--memo <text>', 'free-form note recorded in the audit log')
|
|
582
|
+
.action(async (orgId, subId, opts) => {
|
|
583
|
+
await run(async () => {
|
|
584
|
+
const result = await (0, subscription_1.extendContract)({
|
|
585
|
+
orgId,
|
|
586
|
+
subId,
|
|
587
|
+
commitmentEnd: opts.newEnd,
|
|
588
|
+
memo: opts.memo,
|
|
589
|
+
apiBase: opts.apiBase,
|
|
590
|
+
token: opts.token,
|
|
591
|
+
});
|
|
592
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
// ===== topup =====
|
|
596
|
+
const topup = program.command('topup').description('Manage org balance');
|
|
597
|
+
commonAuthOptions(topup.command('execute'))
|
|
598
|
+
.description('Credit an org\'s LH balance immediately (smoke A subset of #28)')
|
|
599
|
+
.requiredOption('--org <orgId>')
|
|
600
|
+
.requiredOption('--usd <amount>', 'positive USD amount')
|
|
601
|
+
.option('--memo <text>', 'free-form note (recorded in BalanceLog + audit)')
|
|
602
|
+
.action(async (opts) => {
|
|
603
|
+
await run(async () => {
|
|
604
|
+
const amountUsd = Number(opts.usd);
|
|
605
|
+
if (!Number.isFinite(amountUsd)) {
|
|
606
|
+
throw new Error(`--usd must be a number, got: ${opts.usd}`);
|
|
607
|
+
}
|
|
608
|
+
const result = await (0, topup_1.executeTopup)({
|
|
609
|
+
orgId: opts.org,
|
|
610
|
+
amountUsd,
|
|
611
|
+
memo: opts.memo,
|
|
612
|
+
apiBase: opts.apiBase,
|
|
613
|
+
token: opts.token,
|
|
614
|
+
});
|
|
615
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
commonAuthOptions(topup.command('list'))
|
|
619
|
+
.description('List executed top-ups (filterable by org / time window / actor)')
|
|
620
|
+
.option('--org-id <orgId>')
|
|
621
|
+
.option('--from <iso>', 'inclusive lower bound on createdAt (ISO8601)')
|
|
622
|
+
.option('--to <iso>', 'exclusive upper bound on createdAt (ISO8601)')
|
|
623
|
+
.option('--actor <operatorId>', 'filter to top-ups executed by this operator id')
|
|
624
|
+
.option('--limit <n>', 'page size (default 50, max 200)')
|
|
625
|
+
.option('--offset <n>', 'page offset (default 0)')
|
|
626
|
+
.action(async (opts) => {
|
|
627
|
+
await run(async () => {
|
|
628
|
+
const limit = opts.limit !== undefined ? Number(opts.limit) : undefined;
|
|
629
|
+
const offset = opts.offset !== undefined ? Number(opts.offset) : undefined;
|
|
630
|
+
if (limit !== undefined && !Number.isFinite(limit)) {
|
|
631
|
+
throw new Error(`--limit must be a number, got: ${opts.limit}`);
|
|
632
|
+
}
|
|
633
|
+
if (offset !== undefined && !Number.isFinite(offset)) {
|
|
634
|
+
throw new Error(`--offset must be a number, got: ${opts.offset}`);
|
|
635
|
+
}
|
|
636
|
+
const result = await (0, topup_1.listTopups)({
|
|
637
|
+
orgId: opts.orgId,
|
|
638
|
+
from: opts.from,
|
|
639
|
+
to: opts.to,
|
|
640
|
+
actor: opts.actor,
|
|
641
|
+
limit,
|
|
642
|
+
offset,
|
|
643
|
+
apiBase: opts.apiBase,
|
|
644
|
+
token: opts.token,
|
|
645
|
+
});
|
|
646
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
// ===== member (ADR-028 budget guardian, #578 CLI parity of #574) =====
|
|
650
|
+
const member = program
|
|
651
|
+
.command('member')
|
|
652
|
+
.description('Member budget-guardian ops (unfreeze / daily cap)');
|
|
653
|
+
commonAuthOptions(member.command('unfreeze'))
|
|
654
|
+
.description('Lift a budget-guardian freeze (FROZEN → ACTIVE) so the member\'s keys work again')
|
|
655
|
+
.requiredOption('--org <orgId>')
|
|
656
|
+
.requiredOption('--member <memberId>')
|
|
657
|
+
.action(async (opts) => {
|
|
658
|
+
await run(async () => {
|
|
659
|
+
const result = await (0, member_1.unfreezeMember)({
|
|
660
|
+
orgId: opts.org,
|
|
661
|
+
memberId: opts.member,
|
|
662
|
+
apiBase: opts.apiBase,
|
|
663
|
+
token: opts.token,
|
|
664
|
+
});
|
|
665
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
commonAuthOptions(member.command('set-cap'))
|
|
669
|
+
.description('Set / change / clear a member\'s daily budget cap (in the org settlement currency)')
|
|
670
|
+
.requiredOption('--org <orgId>')
|
|
671
|
+
.requiredOption('--member <memberId>')
|
|
672
|
+
.option('--cap <amount>', 'daily cap amount (≥ 0) in the org settlement currency')
|
|
673
|
+
.option('--clear', 'clear the cap (member unbounded)')
|
|
674
|
+
.action(async (opts) => {
|
|
675
|
+
await run(async () => {
|
|
676
|
+
if (opts.clear && opts.cap !== undefined) {
|
|
677
|
+
throw new Error('Pass either --cap <amount> or --clear, not both.');
|
|
678
|
+
}
|
|
679
|
+
if (!opts.clear && opts.cap === undefined) {
|
|
680
|
+
throw new Error('Pass --cap <amount> to set, or --clear to remove.');
|
|
681
|
+
}
|
|
682
|
+
let dailyBudgetCap;
|
|
683
|
+
if (opts.clear) {
|
|
684
|
+
dailyBudgetCap = null;
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
dailyBudgetCap = Number(opts.cap);
|
|
688
|
+
if (!Number.isFinite(dailyBudgetCap) || dailyBudgetCap < 0) {
|
|
689
|
+
throw new Error(`--cap must be a number ≥ 0, got: ${opts.cap}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const result = await (0, member_1.setMemberBudgetCap)({
|
|
693
|
+
orgId: opts.org,
|
|
694
|
+
memberId: opts.member,
|
|
695
|
+
dailyBudgetCap,
|
|
696
|
+
apiBase: opts.apiBase,
|
|
697
|
+
token: opts.token,
|
|
698
|
+
});
|
|
699
|
+
(0, output_1.printValue)(result, { json: !!opts.json });
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
// ===== audit =====
|
|
703
|
+
const audit = program
|
|
704
|
+
.command('audit')
|
|
705
|
+
.description('Query the platform audit log (incident triage without psql)');
|
|
706
|
+
commonAuthOptions(audit.command('query'))
|
|
707
|
+
.description('Read /platform/audit with filters; default output: one line per row')
|
|
708
|
+
.option('--org <orgId>', 'filter to a single org')
|
|
709
|
+
.option('--action <prefix>', 'filter to action (e.g. api_key.created)')
|
|
710
|
+
.option('--actor <emailOrId>', 'filter to operator/admin who performed the action')
|
|
711
|
+
.option('--from <iso>', 'ISO8601 lower bound on occurredAt')
|
|
712
|
+
.option('--to <iso>', 'ISO8601 upper bound on occurredAt')
|
|
713
|
+
.option('--limit <n>', 'page size (default 50, max 200)', '50')
|
|
714
|
+
.action(async (opts) => {
|
|
715
|
+
await run(async () => {
|
|
716
|
+
const limit = opts.limit !== undefined ? Number(opts.limit) : undefined;
|
|
717
|
+
if (limit !== undefined && !Number.isFinite(limit)) {
|
|
718
|
+
throw new Error(`--limit must be a number, got: ${opts.limit}`);
|
|
719
|
+
}
|
|
720
|
+
const rows = await (0, audit_1.queryAudit)({
|
|
721
|
+
orgId: opts.org,
|
|
722
|
+
action: opts.action,
|
|
723
|
+
actor: opts.actor,
|
|
724
|
+
from: opts.from,
|
|
725
|
+
to: opts.to,
|
|
726
|
+
limit,
|
|
727
|
+
apiBase: opts.apiBase,
|
|
728
|
+
token: opts.token,
|
|
729
|
+
});
|
|
730
|
+
process.stdout.write((0, audit_1.renderAudit)(rows, { json: !!opts.json }));
|
|
731
|
+
if (opts.json)
|
|
732
|
+
process.stdout.write('\n');
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
// ===== scanners (T4 of v0.1.7 #279 / #282 — US-OP-11) =====
|
|
736
|
+
const scanners = program
|
|
737
|
+
.command('scanners')
|
|
738
|
+
.description('Background-scanner liveness');
|
|
739
|
+
commonAuthOptions(scanners.command('status'))
|
|
740
|
+
.description('Show last-tick / next-tick / counters per scanner')
|
|
741
|
+
.action(async (opts) => {
|
|
742
|
+
await run(async () => {
|
|
743
|
+
const rows = await (0, scanners_1.getScannersStatus)({
|
|
744
|
+
apiBase: opts.apiBase,
|
|
745
|
+
token: opts.token,
|
|
746
|
+
});
|
|
747
|
+
process.stdout.write((0, scanners_1.renderScannersStatus)(rows, { json: !!opts.json }));
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
// ===== phase2-status (T9 of v0.1.7 #279 / #284) =====
|
|
751
|
+
// Forcing function for the ADR-012 §4 Phase 2 SPA trigger conditions.
|
|
752
|
+
// Manual flags persist to ~/.lh-ops/phase2-state.json so day-2 runs
|
|
753
|
+
// don't reset to all-false when the operator omits flags.
|
|
754
|
+
commonAuthOptions(program.command('phase2-status'))
|
|
755
|
+
.description('Evaluate ADR-012 §4 Phase 2 SPA trigger state')
|
|
756
|
+
.option('--ops-team-size <n>', 'condition 1: ops team headcount')
|
|
757
|
+
.option('--has-nontech <bool>', 'condition 1: includes non-technical member')
|
|
758
|
+
.option('--compliance-ask <bool>', 'condition 2: at least one paid customer has asked for compliance')
|
|
759
|
+
.option('--sales-demo-requested <bool>', 'condition 3: sales has requested customer-facing audit-trail demo')
|
|
760
|
+
.action(async (opts) => {
|
|
761
|
+
await run(async () => {
|
|
762
|
+
const persisted = (await (0, phase2_status_1.loadPhase2State)()) ?? {
|
|
763
|
+
opsTeamSize: 0,
|
|
764
|
+
hasNontech: false,
|
|
765
|
+
complianceAsk: false,
|
|
766
|
+
salesDemoRequested: false,
|
|
767
|
+
};
|
|
768
|
+
const merged = {
|
|
769
|
+
opsTeamSize: opts.opsTeamSize !== undefined
|
|
770
|
+
? parseInt(opts.opsTeamSize, 10) || 0
|
|
771
|
+
: persisted.opsTeamSize,
|
|
772
|
+
hasNontech: parseBool(opts.hasNontech, persisted.hasNontech),
|
|
773
|
+
complianceAsk: parseBool(opts.complianceAsk, persisted.complianceAsk),
|
|
774
|
+
salesDemoRequested: parseBool(opts.salesDemoRequested, persisted.salesDemoRequested),
|
|
775
|
+
};
|
|
776
|
+
await (0, phase2_status_1.savePhase2State)(merged);
|
|
777
|
+
const orgs = await (0, org_list_1.listOrgs)({
|
|
778
|
+
status: 'ACTIVE',
|
|
779
|
+
apiBase: opts.apiBase,
|
|
780
|
+
token: opts.token,
|
|
781
|
+
});
|
|
782
|
+
const state = (0, phase2_status_1.evaluatePhase2State)(merged, orgs.length);
|
|
783
|
+
process.stdout.write((0, phase2_status_1.renderPhase2Status)(state, { json: !!opts.json }));
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
function parseBool(raw, fallback) {
|
|
787
|
+
if (raw === undefined)
|
|
788
|
+
return fallback;
|
|
789
|
+
return raw === 'true' || raw === '1' || raw === 'yes';
|
|
790
|
+
}
|
|
791
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
792
|
+
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
793
|
+
process.exitCode = 1;
|
|
794
|
+
});
|
|
795
|
+
//# sourceMappingURL=index.js.map
|