@brass-build/cli 0.3.0 → 0.4.1
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/AGENTS.md +177 -11
- package/CHANGELOG.md +29 -0
- package/README.md +76 -14
- package/dist/api.d.ts +45 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +21 -2
- package/dist/api.js.map +1 -1
- package/dist/approval-prompt.d.ts +25 -0
- package/dist/approval-prompt.d.ts.map +1 -0
- package/dist/approval-prompt.js +44 -0
- package/dist/approval-prompt.js.map +1 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +10 -1
- package/dist/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +197 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +28 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +369 -29
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/login.d.ts +1 -0
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +24 -12
- package/dist/login.js.map +1 -1
- package/dist/project.d.ts +8 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +75 -7
- package/dist/project.js.map +1 -1
- package/dist/sdk-pairing.d.ts +9 -0
- package/dist/sdk-pairing.d.ts.map +1 -0
- package/dist/sdk-pairing.js +42 -0
- package/dist/sdk-pairing.js.map +1 -0
- package/dist/session.d.ts +18 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +95 -12
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +10 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +41 -0
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/src/api.ts +80 -2
- package/src/approval-prompt.ts +75 -0
- package/src/args.ts +10 -1
- package/src/cli.ts +240 -18
- package/src/commands.ts +479 -36
- package/src/config.ts +15 -2
- package/src/login.ts +34 -11
- package/src/project.ts +82 -7
- package/src/sdk-pairing.ts +53 -0
- package/src/session.ts +135 -11
- package/src/store.ts +77 -0
- package/src/version.ts +1 -1
package/src/cli.ts
CHANGED
|
@@ -23,25 +23,44 @@ import {
|
|
|
23
23
|
} from './config.js';
|
|
24
24
|
import {
|
|
25
25
|
readCredentialsFile,
|
|
26
|
+
readPendingApproval,
|
|
26
27
|
readPendingLogin,
|
|
28
|
+
writePendingApproval,
|
|
27
29
|
writePendingLogin,
|
|
28
30
|
writeStoredCredential,
|
|
29
31
|
} from './store.js';
|
|
30
32
|
import { BrassApi, BrassApiError, type AppVisibility } from './api.js';
|
|
31
33
|
import { serviceTokenAuth, type AuthProvider } from './auth.js';
|
|
32
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
loginDevice,
|
|
36
|
+
postDeviceAppSession,
|
|
37
|
+
postDeviceCancel,
|
|
38
|
+
postSignOut,
|
|
39
|
+
sessionAuth,
|
|
40
|
+
} from './session.js';
|
|
33
41
|
import { loginStart, loginCheck } from './login.js';
|
|
42
|
+
import { renderApprovalPrompt } from './approval-prompt.js';
|
|
34
43
|
import { createLogger, type Logger } from './log.js';
|
|
35
44
|
import {
|
|
36
45
|
publish,
|
|
37
46
|
schemaPull,
|
|
47
|
+
documentContents,
|
|
38
48
|
agentsPull,
|
|
39
49
|
whoami,
|
|
40
50
|
status,
|
|
41
51
|
type CommandContext,
|
|
42
52
|
type CredentialKind,
|
|
43
53
|
} from './commands.js';
|
|
44
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
readProjectState,
|
|
56
|
+
resolveAppId,
|
|
57
|
+
loadFirstManifest,
|
|
58
|
+
firstExistingManifestPath,
|
|
59
|
+
publishManifestCandidates,
|
|
60
|
+
sourceManifestCandidates,
|
|
61
|
+
MANIFEST_FILENAME,
|
|
62
|
+
type AppManifest,
|
|
63
|
+
} from './project.js';
|
|
45
64
|
|
|
46
65
|
import { VERSION } from './version.js';
|
|
47
66
|
|
|
@@ -57,10 +76,21 @@ Usage:
|
|
|
57
76
|
--wait [seconds] polls until approved (default 120s), renewing
|
|
58
77
|
an expired code in place and printing the new one.
|
|
59
78
|
brass logout End the stored sign-in for this environment, here and on the server.
|
|
60
|
-
brass
|
|
79
|
+
brass browser-session --return-to <url> [--app <appId>] [--json]
|
|
80
|
+
Print a URL that opens <url> signed in as you, for a browser
|
|
81
|
+
you drive. The URL is single-use and expires in two minutes.
|
|
82
|
+
brass status [dir] [--app <appId>]
|
|
83
|
+
Report the credential + app state and the one command to run next.
|
|
61
84
|
brass publish [dir] Build output in [dir] (default: dist) is deployed to the app's hosting.
|
|
62
85
|
brass schema pull --doc <docId> [--out brass-app.json]
|
|
63
86
|
Fetch a document's schema and write it into a manifest, verbatim.
|
|
87
|
+
brass contents --doc <docId> [--out contents.json | --stdout] [--stream <name>] [--branch <id>]
|
|
88
|
+
Write everything a document holds as JSON: every
|
|
89
|
+
root container, read whole. The whole document,
|
|
90
|
+
not a sample, so every record type it carries is
|
|
91
|
+
in the file. A document still being imported is
|
|
92
|
+
waited out (--wait <seconds> moves the deadline,
|
|
93
|
+
default 900; --no-wait reads once and fails).
|
|
64
94
|
brass agents pull [--out AGENTS.md | --stdout] [--org <organizationId>]
|
|
65
95
|
Write your organization's agent instructions (its
|
|
66
96
|
AGENTS.md / CLAUDE.md) to a file, or --stdout to print
|
|
@@ -71,12 +101,24 @@ Usage:
|
|
|
71
101
|
Authentication:
|
|
72
102
|
Run 'brass login' to sign in with your browser (needed for schema pull),
|
|
73
103
|
or set BRASS_SERVICE_TOKEN to a service token minted in the dashboard
|
|
74
|
-
(org Settings ->
|
|
104
|
+
(org Settings -> Access tokens) for CI, or pass --token <token>.
|
|
75
105
|
|
|
76
106
|
Common flags:
|
|
77
107
|
--token <token> Service token to authenticate with.
|
|
78
108
|
--json Emit the machine-readable result on stdout.
|
|
79
109
|
|
|
110
|
+
Status flags:
|
|
111
|
+
--app <appId> Report on a specific app (else .brass/project.json / BRASS_APP_ID).
|
|
112
|
+
|
|
113
|
+
Sign-in flags:
|
|
114
|
+
--app <appId> Approve an app you do not own for the sign-in as
|
|
115
|
+
well, so 'brass browser-session' needs no second
|
|
116
|
+
approval. An app you own never does.
|
|
117
|
+
|
|
118
|
+
Browser-session flags:
|
|
119
|
+
--return-to <url> Where the browser lands, on the app's redirect allowlist.
|
|
120
|
+
--app <appId> The app to sign in to (else .brass/project.json / BRASS_APP_ID).
|
|
121
|
+
|
|
80
122
|
Publish flags:
|
|
81
123
|
--app <appId> Publish to a specific app (else .brass/project.json / BRASS_APP_ID).
|
|
82
124
|
--name <name> Name for the app when creating one on first publish.
|
|
@@ -162,6 +204,12 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
162
204
|
// one is obtained); everything else needs a resolved credential.
|
|
163
205
|
if (command === 'login') return await runLogin(parsed, log);
|
|
164
206
|
if (command === 'logout') return await runLogout(parsed, log);
|
|
207
|
+
// Reads the stored device pointer directly rather than through
|
|
208
|
+
// `buildContext`: what the handoff route takes is that credential, not an
|
|
209
|
+
// access token minted from it.
|
|
210
|
+
if (command === 'browser-session') {
|
|
211
|
+
return await runBrowserSession(parsed, log, boolFlag(parsed, 'json'));
|
|
212
|
+
}
|
|
165
213
|
// status runs before a credential is required: reporting "no credential"
|
|
166
214
|
// (and the next step to obtain one) is a first-class outcome, not an error.
|
|
167
215
|
if (command === 'status') return await runStatus(parsed, log);
|
|
@@ -193,7 +241,7 @@ type Context = CommandContext;
|
|
|
193
241
|
|
|
194
242
|
// The commands that need a resolved credential; `login` / `logout` / `status`
|
|
195
243
|
// are answered above without one.
|
|
196
|
-
const CREDENTIALED_COMMANDS = ['publish', 'schema', 'agents', 'whoami'] as const;
|
|
244
|
+
const CREDENTIALED_COMMANDS = ['publish', 'schema', 'contents', 'agents', 'whoami'] as const;
|
|
197
245
|
type CredentialedCommand = (typeof CREDENTIALED_COMMANDS)[number];
|
|
198
246
|
|
|
199
247
|
function isCredentialedCommand(value: string): value is CredentialedCommand {
|
|
@@ -212,6 +260,8 @@ function planCommand(command: CredentialedCommand, parsed: ParsedArgs): CommandP
|
|
|
212
260
|
return planPublish(parsed);
|
|
213
261
|
case 'schema':
|
|
214
262
|
return planSchema(parsed);
|
|
263
|
+
case 'contents':
|
|
264
|
+
return planContents(parsed);
|
|
215
265
|
case 'agents':
|
|
216
266
|
return planAgents(parsed);
|
|
217
267
|
case 'whoami':
|
|
@@ -283,13 +333,18 @@ async function resolveAuth(
|
|
|
283
333
|
});
|
|
284
334
|
if (credential === null) {
|
|
285
335
|
throw new Error(
|
|
286
|
-
'No credential. Run `brass login`, or set BRASS_SERVICE_TOKEN (mint one in the dashboard: org Settings ->
|
|
336
|
+
'No credential. Run `brass login`, or set BRASS_SERVICE_TOKEN (mint one in the dashboard: org Settings -> Access tokens), or pass --token.',
|
|
287
337
|
);
|
|
288
338
|
}
|
|
289
339
|
const auth =
|
|
290
340
|
credential.kind === 'service'
|
|
291
341
|
? serviceTokenAuth(credential.token)
|
|
292
|
-
:
|
|
342
|
+
: // The origin the session was minted on, so a command refreshes it
|
|
343
|
+
// there. The stored profile is keyed on the api host, so an
|
|
344
|
+
// invocation naming `--api-url` alone lands on a session whose auth
|
|
345
|
+
// origin the flags do not carry; refreshing against the default one
|
|
346
|
+
// instead answers 401 and reads as the sign-in having gone stale.
|
|
347
|
+
sessionAuth(credential.authBaseUrl ?? origins.authBaseUrl, credential.sid);
|
|
293
348
|
return { auth, credentialKind: credential.kind };
|
|
294
349
|
}
|
|
295
350
|
|
|
@@ -300,13 +355,17 @@ async function runLogin(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
|
300
355
|
// interactive flow.
|
|
301
356
|
const start = boolFlag(parsed, 'start');
|
|
302
357
|
const check = boolFlag(parsed, 'check');
|
|
303
|
-
const waitSeconds = parseWaitFlag(parsed);
|
|
358
|
+
const waitSeconds = parseWaitFlag(parsed, DEFAULT_WAIT_SECONDS);
|
|
304
359
|
if (start && check) throw new Error('Pass one of --start or --check.');
|
|
360
|
+
// The app this sign-in should also approve, so the human's one click covers
|
|
361
|
+
// the machine and the app it is about to hand a browser.
|
|
362
|
+
const targetAppId = stringFlag(parsed, 'app');
|
|
305
363
|
if (start) {
|
|
306
364
|
const code = await loginStart({
|
|
307
365
|
authBaseUrl: origins.authBaseUrl,
|
|
308
366
|
profile,
|
|
309
367
|
log,
|
|
368
|
+
...(targetAppId !== undefined ? { targetAppId } : {}),
|
|
310
369
|
...(boolFlag(parsed, 'new') ? { force: true } : {}),
|
|
311
370
|
...(waitSeconds !== undefined ? { resultFollows: true } : {}),
|
|
312
371
|
});
|
|
@@ -320,7 +379,10 @@ async function runLogin(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
|
320
379
|
}
|
|
321
380
|
// The RFC 8628 device grant: open the approval page (code prefilled), print
|
|
322
381
|
// the URL + code as a fallback for a headless box, and poll until approval.
|
|
323
|
-
const result = await loginDevice({
|
|
382
|
+
const result = await loginDevice({
|
|
383
|
+
authBaseUrl: origins.authBaseUrl,
|
|
384
|
+
...(targetAppId !== undefined ? { targetAppId } : {}),
|
|
385
|
+
});
|
|
324
386
|
await writeStoredCredential(profile, {
|
|
325
387
|
session: { sid: result.sessionToken, authBaseUrl: origins.authBaseUrl },
|
|
326
388
|
});
|
|
@@ -372,6 +434,103 @@ async function runLogout(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
|
372
434
|
return revoked ? 0 : 1;
|
|
373
435
|
}
|
|
374
436
|
|
|
437
|
+
// `brass browser-session`: turn this machine's sign-in into a URL that signs a
|
|
438
|
+
// browser in as the same human, so an agent that just built an app can open it
|
|
439
|
+
// and drive it rather than reporting it verified by types.
|
|
440
|
+
//
|
|
441
|
+
// The URL is what the command is for, so in the plain form it goes to stdout
|
|
442
|
+
// alone and the guidance to stderr, and `--json` puts it in the result object
|
|
443
|
+
// instead. Either way it is a credential with a two-minute life: it belongs in
|
|
444
|
+
// the browser being driven, not in a log or a commit.
|
|
445
|
+
async function runBrowserSession(
|
|
446
|
+
parsed: ParsedArgs,
|
|
447
|
+
log: Logger,
|
|
448
|
+
json: boolean,
|
|
449
|
+
): Promise<number> {
|
|
450
|
+
const { origins, profile } = resolveBase(parsed);
|
|
451
|
+
const returnTo = stringFlag(parsed, 'return-to');
|
|
452
|
+
if (returnTo === undefined) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
'brass browser-session requires --return-to <url> (where the browser should land)',
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const state = await readProjectState(process.cwd());
|
|
458
|
+
const flagApp = stringFlag(parsed, 'app');
|
|
459
|
+
const envApp = process.env['BRASS_APP_ID'];
|
|
460
|
+
const appId = resolveAppId({
|
|
461
|
+
...(flagApp !== undefined ? { flagApp } : {}),
|
|
462
|
+
...(envApp !== undefined ? { envApp } : {}),
|
|
463
|
+
state,
|
|
464
|
+
profile,
|
|
465
|
+
});
|
|
466
|
+
if (appId === null) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
'No app to sign in to. Pass --app <appId>, or run this from a directory `brass publish` has written .brass/project.json in.',
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The device pointer itself, not an access token minted from it: the handoff
|
|
473
|
+
// route authenticates the machine, and only that credential names one. A
|
|
474
|
+
// service token belongs to an organization rather than to a person, so it
|
|
475
|
+
// has no session for a browser to be handed.
|
|
476
|
+
const file = await readCredentialsFile();
|
|
477
|
+
const session = file?.credentials[profile]?.session;
|
|
478
|
+
if (session === undefined) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
'No Brass sign-in on this machine. Run `brass login` (a service token cannot be handed to a browser).',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const outcome = await postDeviceAppSession(
|
|
485
|
+
session.authBaseUrl ?? origins.authBaseUrl,
|
|
486
|
+
session.sid,
|
|
487
|
+
{ appId, returnTo },
|
|
488
|
+
);
|
|
489
|
+
if (outcome.state === 'needs_approval') {
|
|
490
|
+
// Recorded so `brass status` can report what is outstanding. A caller that
|
|
491
|
+
// relays the URL and then asks status what to do next is otherwise told
|
|
492
|
+
// the app is ready, which is true of publishing and false of the thing it
|
|
493
|
+
// was doing.
|
|
494
|
+
await writePendingApproval(profile, {
|
|
495
|
+
appId,
|
|
496
|
+
approvalUrl: outcome.approvalUrl,
|
|
497
|
+
authBaseUrl: session.authBaseUrl ?? origins.authBaseUrl,
|
|
498
|
+
expiresAt: Date.now() + outcome.expiresIn * 1000,
|
|
499
|
+
});
|
|
500
|
+
// Through the same frame the sign-in's approval prints in, and with the
|
|
501
|
+
// same relay note: this is the same request with a different subject, and
|
|
502
|
+
// it reaches the human only if whoever runs this passes it on.
|
|
503
|
+
log.info(
|
|
504
|
+
`\n${renderApprovalPrompt({
|
|
505
|
+
heading: ` To open ${appId} in a browser, it needs approving for this machine.`,
|
|
506
|
+
url: outcome.approvalUrl,
|
|
507
|
+
relaying: true,
|
|
508
|
+
closing:
|
|
509
|
+
'The person approving must be signed in to Brass as the account this ' +
|
|
510
|
+
'machine is signed in as.\nThen run this command again.',
|
|
511
|
+
})}`,
|
|
512
|
+
);
|
|
513
|
+
log.result({ state: 'needs_approval', app_id: appId, approval_url: outcome.approvalUrl });
|
|
514
|
+
return 1;
|
|
515
|
+
}
|
|
516
|
+
await writePendingApproval(profile, null);
|
|
517
|
+
if (json) {
|
|
518
|
+
log.result({
|
|
519
|
+
state: 'ready',
|
|
520
|
+
url: outcome.url,
|
|
521
|
+
app_id: appId,
|
|
522
|
+
return_to: returnTo,
|
|
523
|
+
expires_in_seconds: outcome.expiresIn,
|
|
524
|
+
});
|
|
525
|
+
} else {
|
|
526
|
+
log.info(
|
|
527
|
+
`Open this in the browser you are driving, within ${outcome.expiresIn} seconds. It signs in once and cannot be reused.`,
|
|
528
|
+
);
|
|
529
|
+
log.write(`${outcome.url}\n`);
|
|
530
|
+
}
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
375
534
|
async function runStatus(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
376
535
|
const { origins, profile } = resolveBase(parsed);
|
|
377
536
|
|
|
@@ -393,7 +552,8 @@ async function runStatus(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
|
393
552
|
origins.apiBaseUrl,
|
|
394
553
|
credential.kind === 'service'
|
|
395
554
|
? serviceTokenAuth(credential.token)
|
|
396
|
-
:
|
|
555
|
+
: // The origin the session was minted on, as `resolveAuth` takes.
|
|
556
|
+
sessionAuth(credential.authBaseUrl ?? origins.authBaseUrl, credential.sid),
|
|
397
557
|
);
|
|
398
558
|
|
|
399
559
|
// What `publish` would target: the same app-id and manifest-name resolution
|
|
@@ -409,13 +569,28 @@ async function runStatus(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
|
409
569
|
state,
|
|
410
570
|
profile,
|
|
411
571
|
});
|
|
412
|
-
const
|
|
572
|
+
const manifestFlag = stringFlag(parsed, 'manifest');
|
|
573
|
+
// `status` reports rather than acts, so a manifest that cannot be parsed is
|
|
574
|
+
// named and the rest of the report still runs. Publishing that same tree
|
|
575
|
+
// fails on it, which is the point at which it has to be fixed.
|
|
576
|
+
let manifest: AppManifest | undefined;
|
|
577
|
+
try {
|
|
578
|
+
manifest = (
|
|
579
|
+
await loadFirstManifest(
|
|
580
|
+
cwd,
|
|
581
|
+
manifestFlag !== undefined ? [manifestFlag] : publishManifestCandidates(dir),
|
|
582
|
+
)
|
|
583
|
+
)?.manifest;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
log.warn(err instanceof Error ? err.message : String(err));
|
|
586
|
+
}
|
|
413
587
|
const manifestName =
|
|
414
588
|
typeof manifest?.name === 'string' && manifest.name.trim() !== ''
|
|
415
589
|
? manifest.name.trim()
|
|
416
590
|
: null;
|
|
417
591
|
|
|
418
592
|
const pending = await readPendingLogin(profile);
|
|
593
|
+
const pendingApproval = await readPendingApproval(profile);
|
|
419
594
|
const result = await status({
|
|
420
595
|
api,
|
|
421
596
|
profile,
|
|
@@ -432,6 +607,7 @@ async function runStatus(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
|
432
607
|
verificationUrl: pending.verificationUriComplete ?? pending.verificationUri,
|
|
433
608
|
expiresAt: pending.expiresAt,
|
|
434
609
|
},
|
|
610
|
+
pendingApproval,
|
|
435
611
|
});
|
|
436
612
|
log.result(result);
|
|
437
613
|
return 0;
|
|
@@ -445,7 +621,7 @@ function planPublish(parsed: ParsedArgs): CommandPlan {
|
|
|
445
621
|
const org = stringFlag(parsed, 'org');
|
|
446
622
|
const slug = stringFlag(parsed, 'slug');
|
|
447
623
|
const clientToken = stringFlag(parsed, 'client-token');
|
|
448
|
-
const manifestPath = stringFlag(parsed, 'manifest')
|
|
624
|
+
const manifestPath = stringFlag(parsed, 'manifest');
|
|
449
625
|
const visibility = parseVisibilityFlag(stringFlag(parsed, 'visibility'));
|
|
450
626
|
const requireAccess = parseGateFlag(stringFlag(parsed, 'gate'));
|
|
451
627
|
return async (ctx): Promise<unknown> => {
|
|
@@ -458,7 +634,7 @@ function planPublish(parsed: ParsedArgs): CommandPlan {
|
|
|
458
634
|
});
|
|
459
635
|
return publish(ctx, {
|
|
460
636
|
dir,
|
|
461
|
-
manifestPath,
|
|
637
|
+
...(manifestPath !== undefined ? { manifestPath } : {}),
|
|
462
638
|
...(appId !== null ? { appId } : {}),
|
|
463
639
|
...(name !== undefined ? { name } : {}),
|
|
464
640
|
...(org !== undefined ? { organizationId: org } : {}),
|
|
@@ -476,11 +652,19 @@ function planPublish(parsed: ParsedArgs): CommandPlan {
|
|
|
476
652
|
// so a second check resumes the same sign-in.
|
|
477
653
|
const DEFAULT_WAIT_SECONDS = 120;
|
|
478
654
|
const MAX_WAIT_SECONDS = 3600;
|
|
479
|
-
|
|
480
|
-
|
|
655
|
+
// `brass contents` waits by default, so its `--wait` only moves the budget.
|
|
656
|
+
// The default is the extraction worker's own ceiling: past it the run has
|
|
657
|
+
// stopped rather than slowed.
|
|
658
|
+
const CONTENTS_WAIT_SECONDS = 900;
|
|
659
|
+
|
|
660
|
+
// `--wait` with no value means "the command's own default deadline", which
|
|
661
|
+
// differs per command, so the default is the caller's to name. `undefined`
|
|
662
|
+
// back means the flag was absent, which each command reads for itself: the
|
|
663
|
+
// sign-in poll does not wait at all, and a document read waits anyway.
|
|
664
|
+
function parseWaitFlag(parsed: ParsedArgs, bareValue: number): number | undefined {
|
|
481
665
|
const raw = parsed.flags['wait'];
|
|
482
666
|
if (raw === undefined) return undefined;
|
|
483
|
-
if (raw === true) return
|
|
667
|
+
if (raw === true) return bareValue;
|
|
484
668
|
const seconds = Number(raw);
|
|
485
669
|
if (!Number.isInteger(seconds) || seconds <= 0 || seconds > MAX_WAIT_SECONDS) {
|
|
486
670
|
throw new Error(`Invalid --wait "${raw}" (expected whole seconds, 1 to ${MAX_WAIT_SECONDS})`);
|
|
@@ -509,14 +693,52 @@ function parseGateFlag(value: string | undefined): boolean | undefined {
|
|
|
509
693
|
return value === 'on';
|
|
510
694
|
}
|
|
511
695
|
|
|
696
|
+
function planContents(parsed: ParsedArgs): CommandPlan {
|
|
697
|
+
const docId = stringFlag(parsed, 'doc');
|
|
698
|
+
if (docId === undefined) throw new Error('brass contents requires --doc <docId>');
|
|
699
|
+
const toStdout = boolFlag(parsed, 'stdout');
|
|
700
|
+
const outPath = stringFlag(parsed, 'out');
|
|
701
|
+
// Both name where the JSON goes, so together neither answer is the one the
|
|
702
|
+
// caller gets.
|
|
703
|
+
if (toStdout && outPath !== undefined) {
|
|
704
|
+
throw new Error('Pass one of --stdout or --out, not both');
|
|
705
|
+
}
|
|
706
|
+
// A document whose content is still being prepared is waited out rather
|
|
707
|
+
// than refused, because this command's own join is what starts that work.
|
|
708
|
+
// The two flags name the same budget from opposite ends, so together
|
|
709
|
+
// neither answer is the one the caller gets.
|
|
710
|
+
const noWait = boolFlag(parsed, 'no-wait');
|
|
711
|
+
const waitSeconds = parseWaitFlag(parsed, CONTENTS_WAIT_SECONDS);
|
|
712
|
+
if (noWait && waitSeconds !== undefined) {
|
|
713
|
+
throw new Error('Pass one of --wait or --no-wait, not both');
|
|
714
|
+
}
|
|
715
|
+
const opts = {
|
|
716
|
+
docId,
|
|
717
|
+
stream: stringFlag(parsed, 'stream'),
|
|
718
|
+
branch: stringFlag(parsed, 'branch'),
|
|
719
|
+
outPath: toStdout ? null : (outPath ?? 'contents.json'),
|
|
720
|
+
waitMs: noWait ? 0 : (waitSeconds ?? CONTENTS_WAIT_SECONDS) * 1000,
|
|
721
|
+
};
|
|
722
|
+
return (ctx): Promise<unknown> => documentContents(ctx, opts);
|
|
723
|
+
}
|
|
724
|
+
|
|
512
725
|
function planSchema(parsed: ParsedArgs): CommandPlan {
|
|
513
726
|
if (parsed.positionals[1] !== 'pull') {
|
|
514
727
|
throw new Error('Usage: brass schema pull --doc <docId> [--out brass-app.json]');
|
|
515
728
|
}
|
|
516
729
|
const docId = stringFlag(parsed, 'doc');
|
|
517
730
|
if (docId === undefined) throw new Error('brass schema pull requires --doc <docId>');
|
|
518
|
-
const
|
|
519
|
-
return (ctx): Promise<unknown> =>
|
|
731
|
+
const outFlag = stringFlag(parsed, 'out');
|
|
732
|
+
return async (ctx): Promise<unknown> => {
|
|
733
|
+
// Merge into the manifest the repo already keeps rather than writing a
|
|
734
|
+
// second one beside it: a pulled schema in a file the bundle never serves
|
|
735
|
+
// is a schema the platform never reads.
|
|
736
|
+
const outPath =
|
|
737
|
+
outFlag ??
|
|
738
|
+
(await firstExistingManifestPath(ctx.cwd, sourceManifestCandidates())) ??
|
|
739
|
+
MANIFEST_FILENAME;
|
|
740
|
+
return schemaPull(ctx, { docId, outPath });
|
|
741
|
+
};
|
|
520
742
|
}
|
|
521
743
|
|
|
522
744
|
function planAgents(parsed: ParsedArgs): CommandPlan {
|