@brass-build/cli 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/AGENTS.md +170 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/api.d.ts +73 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +97 -0
- package/dist/api.js.map +1 -0
- package/dist/args.d.ts +10 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +94 -0
- package/dist/args.js.map +1 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +12 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin/brass.d.ts +3 -0
- package/dist/bin/brass.d.ts.map +1 -0
- package/dist/bin/brass.js +11 -0
- package/dist/bin/brass.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +364 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +94 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +559 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +76 -0
- package/dist/config.js.map +1 -0
- package/dist/log.d.ts +9 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +32 -0
- package/dist/log.js.map +1 -0
- package/dist/login.d.ts +21 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +158 -0
- package/dist/login.js.map +1 -0
- package/dist/project.d.ts +32 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +129 -0
- package/dist/project.js.map +1 -0
- package/dist/session.d.ts +47 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +224 -0
- package/dist/session.js.map +1 -0
- package/dist/store.d.ts +17 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +90 -0
- package/dist/store.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +42 -0
- package/src/api.ts +195 -0
- package/src/args.ts +107 -0
- package/src/auth.ts +16 -0
- package/src/bin/brass.ts +11 -0
- package/src/cli.ts +422 -0
- package/src/commands.ts +864 -0
- package/src/config.ts +132 -0
- package/src/log.ts +41 -0
- package/src/login.ts +211 -0
- package/src/project.ts +176 -0
- package/src/session.ts +319 -0
- package/src/store.ts +123 -0
- package/src/version.ts +8 -0
package/src/args.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// A minimal argv parser: no dependency, and small enough to unit-test the
|
|
2
|
+
// exact precedence the commands rely on. Supports `--flag value`,
|
|
3
|
+
// `--flag=value`, and boolean `--flag`; everything else is a positional.
|
|
4
|
+
|
|
5
|
+
export interface ParsedArgs {
|
|
6
|
+
positionals: string[];
|
|
7
|
+
// A flag present with no value (`--json`) stores `true`; a valued flag
|
|
8
|
+
// (`--doc abc` / `--doc=abc`) stores the string.
|
|
9
|
+
flags: Record<string, string | true>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Flags that never take a value, so `brass publish --yes ./dist` parses
|
|
13
|
+
// `./dist` as a positional rather than the value of `--yes`.
|
|
14
|
+
const BOOLEAN_FLAGS = new Set([
|
|
15
|
+
'json',
|
|
16
|
+
'yes',
|
|
17
|
+
'help',
|
|
18
|
+
'version',
|
|
19
|
+
'stdout',
|
|
20
|
+
'start',
|
|
21
|
+
'check',
|
|
22
|
+
'new',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// Every flag any command reads. A flag outside this set is rejected rather
|
|
26
|
+
// than ignored, because ignoring one silently retargets the invocation: the
|
|
27
|
+
// origin flags default to production, so a misspelled `--api-ur1` publishes an
|
|
28
|
+
// app, or mints a sign-in, against the real platform while naming another
|
|
29
|
+
// stack on the command line.
|
|
30
|
+
const KNOWN_FLAGS = new Set([
|
|
31
|
+
...BOOLEAN_FLAGS,
|
|
32
|
+
'api-url',
|
|
33
|
+
'app',
|
|
34
|
+
'auth-url',
|
|
35
|
+
'client-token',
|
|
36
|
+
'dashboard-url',
|
|
37
|
+
'doc',
|
|
38
|
+
'gate',
|
|
39
|
+
'manifest',
|
|
40
|
+
'name',
|
|
41
|
+
'org',
|
|
42
|
+
'out',
|
|
43
|
+
'slug',
|
|
44
|
+
'token',
|
|
45
|
+
'visibility',
|
|
46
|
+
'wait',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// The flag names this CLI does not know, in the order given, for an error
|
|
50
|
+
// naming all of them rather than one per run.
|
|
51
|
+
export function unknownFlags(parsed: ParsedArgs): string[] {
|
|
52
|
+
return Object.keys(parsed.flags).filter((name) => !KNOWN_FLAGS.has(name));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Which of `names` were given without a value (`--api-url --json`, or
|
|
56
|
+
// `--api-url` last on the line). The parser stores those as `true` and
|
|
57
|
+
// `stringFlag` reads that as absent, so an origin flag in this state resolves
|
|
58
|
+
// the production default while the command line names another stack. That is
|
|
59
|
+
// the same silent retarget `unknownFlags` catches for a misspelled name.
|
|
60
|
+
export function valuelessFlags(parsed: ParsedArgs, names: readonly string[]): string[] {
|
|
61
|
+
return names.filter((name) => parsed.flags[name] === true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
65
|
+
const positionals: string[] = [];
|
|
66
|
+
const flags: Record<string, string | true> = {};
|
|
67
|
+
for (let i = 0; i < argv.length; i++) {
|
|
68
|
+
const arg = argv[i];
|
|
69
|
+
if (arg === undefined) continue;
|
|
70
|
+
if (arg.startsWith('--')) {
|
|
71
|
+
const body = arg.slice(2);
|
|
72
|
+
const eq = body.indexOf('=');
|
|
73
|
+
if (eq !== -1) {
|
|
74
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (BOOLEAN_FLAGS.has(body)) {
|
|
78
|
+
flags[body] = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const next = argv[i + 1];
|
|
82
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
83
|
+
flags[body] = next;
|
|
84
|
+
i++;
|
|
85
|
+
} else {
|
|
86
|
+
flags[body] = true;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
positionals.push(arg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { positionals, flags };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read a flag expected to carry a string value; a bare boolean flag (no
|
|
96
|
+
// value) is treated as absent so `--doc` alone doesn't resolve to `true`.
|
|
97
|
+
export function stringFlag(
|
|
98
|
+
parsed: ParsedArgs,
|
|
99
|
+
name: string,
|
|
100
|
+
): string | undefined {
|
|
101
|
+
const v = parsed.flags[name];
|
|
102
|
+
return typeof v === 'string' ? v : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function boolFlag(parsed: ParsedArgs, name: string): boolean {
|
|
106
|
+
return parsed.flags[name] === true || typeof parsed.flags[name] === 'string';
|
|
107
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// How a command authenticates each request. Two kinds resolve to the same
|
|
2
|
+
// interface so `BrassApi` is agnostic: a service token is a static bearer;
|
|
3
|
+
// a login session refreshes a short-lived access token on demand (see
|
|
4
|
+
// `sessionAuth` in session.ts). The provider returns the exact headers to
|
|
5
|
+
// merge onto a data-API request.
|
|
6
|
+
|
|
7
|
+
export interface AuthProvider {
|
|
8
|
+
headers(): Promise<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// A service token (`brass_sk_...`) is presented verbatim as the bearer.
|
|
12
|
+
export function serviceTokenAuth(token: string): AuthProvider {
|
|
13
|
+
return {
|
|
14
|
+
headers: () => Promise.resolve({ authorization: `Bearer ${token}` }),
|
|
15
|
+
};
|
|
16
|
+
}
|
package/src/bin/brass.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from '../cli.js';
|
|
3
|
+
|
|
4
|
+
run(process.argv.slice(2))
|
|
5
|
+
.then((code) => {
|
|
6
|
+
process.exitCode = code;
|
|
7
|
+
})
|
|
8
|
+
.catch((err: unknown) => {
|
|
9
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// Argv dispatch: resolve the target environment + credential, build the API
|
|
2
|
+
// client, and route to a command. Kept thin over `commands.ts` (which holds
|
|
3
|
+
// the workflow logic) so the wiring here is easy to read end to end.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
parseArgs,
|
|
7
|
+
stringFlag,
|
|
8
|
+
boolFlag,
|
|
9
|
+
unknownFlags,
|
|
10
|
+
valuelessFlags,
|
|
11
|
+
type ParsedArgs,
|
|
12
|
+
} from './args.js';
|
|
13
|
+
import {
|
|
14
|
+
resolveOrigins,
|
|
15
|
+
resolveCredential,
|
|
16
|
+
profileForOrigins,
|
|
17
|
+
type OriginOverrides,
|
|
18
|
+
type Origins,
|
|
19
|
+
type Profile,
|
|
20
|
+
} from './config.js';
|
|
21
|
+
import { readCredentialsFile, readPendingLogin, writeStoredCredential } from './store.js';
|
|
22
|
+
import { BrassApi, BrassApiError, type AppVisibility } from './api.js';
|
|
23
|
+
import { serviceTokenAuth, type AuthProvider } from './auth.js';
|
|
24
|
+
import { loginDevice, sessionAuth } from './session.js';
|
|
25
|
+
import { loginStart, loginCheck } from './login.js';
|
|
26
|
+
import { createLogger, type Logger } from './log.js';
|
|
27
|
+
import {
|
|
28
|
+
publish,
|
|
29
|
+
schemaPull,
|
|
30
|
+
agentsPull,
|
|
31
|
+
whoami,
|
|
32
|
+
status,
|
|
33
|
+
type CommandContext,
|
|
34
|
+
} from './commands.js';
|
|
35
|
+
import { readProjectState, resolveAppId, readManifest } from './project.js';
|
|
36
|
+
|
|
37
|
+
import { VERSION } from './version.js';
|
|
38
|
+
|
|
39
|
+
export { VERSION };
|
|
40
|
+
|
|
41
|
+
const USAGE = `brass ${VERSION} - publish Brass apps and pull schemas
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
brass login Sign in with your browser (opens the approval page; approve, done).
|
|
45
|
+
brass login --start Start a sign-in and exit: prints the approval URL + code to relay.
|
|
46
|
+
Resumes the code already waiting; --new forces a fresh one.
|
|
47
|
+
brass login --check Check a started sign-in once; stores the session when approved.
|
|
48
|
+
--wait [seconds] polls until approved (default 120s), renewing
|
|
49
|
+
an expired code in place and printing the new one.
|
|
50
|
+
brass logout Forget the stored sign-in for this environment.
|
|
51
|
+
brass status [dir] Report the credential + app state and the one command to run next.
|
|
52
|
+
brass publish [dir] Build output in [dir] (default: dist) is deployed to the app's hosting.
|
|
53
|
+
brass schema pull --doc <docId> [--out brass-app.json]
|
|
54
|
+
Fetch a document's schema and write it into a manifest, verbatim.
|
|
55
|
+
brass agents pull [--out AGENTS.md | --stdout] [--org <organizationId>]
|
|
56
|
+
Write your organization's agent instructions (its
|
|
57
|
+
AGENTS.md / CLAUDE.md) to a file, or --stdout to print
|
|
58
|
+
them (an agent reads them inline). Needs 'brass login';
|
|
59
|
+
--org is optional when you belong to one organization.
|
|
60
|
+
brass whoami Verify the current credential authenticates.
|
|
61
|
+
|
|
62
|
+
Authentication:
|
|
63
|
+
Run 'brass login' to sign in with your browser (needed for schema pull),
|
|
64
|
+
or set BRASS_SERVICE_TOKEN to a service token minted in the dashboard
|
|
65
|
+
(org Settings -> Service tokens) for CI, or pass --token <token>.
|
|
66
|
+
|
|
67
|
+
Common flags:
|
|
68
|
+
--token <token> Service token to authenticate with.
|
|
69
|
+
--json Emit the machine-readable result on stdout.
|
|
70
|
+
|
|
71
|
+
Publish flags:
|
|
72
|
+
--app <appId> Publish to a specific app (else .brass/project.json / BRASS_APP_ID).
|
|
73
|
+
--name <name> Name for the app when creating one on first publish.
|
|
74
|
+
--org <organizationId> Organization to own a newly created app (a signed-in user defaults to their own org; a service token to the token's org).
|
|
75
|
+
--client-token <key> Stable key that makes a first create idempotent (else brass-app.json "client_token").
|
|
76
|
+
--slug <slug> Preferred hosting subdomain on first enable.
|
|
77
|
+
--visibility <v> Set the app's visibility (private | invitee_visible | public).
|
|
78
|
+
--gate <on|off> Set the hosted load gate (on = audience-gated, off = world-loadable).
|
|
79
|
+
--manifest <path> Served manifest to read a create name from (default: brass-app.json).
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
export async function run(argv: readonly string[]): Promise<number> {
|
|
83
|
+
const parsed = parseArgs(argv);
|
|
84
|
+
const command = parsed.positionals[0];
|
|
85
|
+
|
|
86
|
+
if (boolFlag(parsed, 'version') && command === undefined) {
|
|
87
|
+
process.stdout.write(`${VERSION}\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
if (command === undefined || boolFlag(parsed, 'help') || command === 'help') {
|
|
91
|
+
process.stdout.write(USAGE);
|
|
92
|
+
return command === undefined && !boolFlag(parsed, 'help') ? 1 : 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Checked after --help / --version so a caller reaching for usage still gets
|
|
96
|
+
// it, and before any command runs so an unrecognised flag never reaches a
|
|
97
|
+
// request. Ignoring one would silently target production, since that is what
|
|
98
|
+
// the origin flags default to.
|
|
99
|
+
const unknown = unknownFlags(parsed);
|
|
100
|
+
if (unknown.length > 0) {
|
|
101
|
+
const names = unknown.map((n) => `--${n}`).join(', ');
|
|
102
|
+
process.stderr.write(`error: unknown ${unknown.length === 1 ? 'flag' : 'flags'} ${names}\n\n${USAGE}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const json = boolFlag(parsed, 'json');
|
|
107
|
+
const log = createLogger(json);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// login / logout run before any credential is required (login is how
|
|
111
|
+
// one is obtained); everything else needs a resolved credential.
|
|
112
|
+
if (command === 'login') return await runLogin(parsed, log);
|
|
113
|
+
if (command === 'logout') return await runLogout(parsed, log);
|
|
114
|
+
// status runs before a credential is required: reporting "no credential"
|
|
115
|
+
// (and the next step to obtain one) is a first-class outcome, not an error.
|
|
116
|
+
if (command === 'status') return await runStatus(parsed, log);
|
|
117
|
+
|
|
118
|
+
const ctx = await buildContext(parsed);
|
|
119
|
+
switch (command) {
|
|
120
|
+
case 'publish':
|
|
121
|
+
log.result(await runPublish(ctx, parsed));
|
|
122
|
+
return 0;
|
|
123
|
+
case 'schema':
|
|
124
|
+
log.result(await runSchema(ctx, parsed));
|
|
125
|
+
return 0;
|
|
126
|
+
case 'agents':
|
|
127
|
+
log.result(await runAgents(ctx, parsed));
|
|
128
|
+
return 0;
|
|
129
|
+
case 'whoami':
|
|
130
|
+
log.result(await whoami(ctx));
|
|
131
|
+
return 0;
|
|
132
|
+
default:
|
|
133
|
+
process.stderr.write(`Unknown command "${command}".\n\n${USAGE}`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
process.stderr.write(`${formatError(err)}\n`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type Context = CommandContext;
|
|
143
|
+
|
|
144
|
+
interface Base {
|
|
145
|
+
origins: Origins;
|
|
146
|
+
profile: Profile;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The flags that name the target stack. Production is the default, so every
|
|
150
|
+
// other stack is reached through these.
|
|
151
|
+
const ORIGIN_FLAGS = ['api-url', 'auth-url', 'dashboard-url'] as const;
|
|
152
|
+
|
|
153
|
+
// Resolve the target origins + profile from the origin flags, shared by every
|
|
154
|
+
// command (login and the credential-bearing ones).
|
|
155
|
+
function resolveBase(parsed: ParsedArgs): Base {
|
|
156
|
+
const valueless = valuelessFlags(parsed, ORIGIN_FLAGS);
|
|
157
|
+
if (valueless.length > 0) {
|
|
158
|
+
throw new Error(`Missing value for ${valueless.map((name) => `--${name}`).join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
const overrides: OriginOverrides = {};
|
|
161
|
+
const apiUrl = stringFlag(parsed, 'api-url');
|
|
162
|
+
if (apiUrl !== undefined) overrides.apiBaseUrl = apiUrl;
|
|
163
|
+
const authUrl = stringFlag(parsed, 'auth-url');
|
|
164
|
+
if (authUrl !== undefined) overrides.authBaseUrl = authUrl;
|
|
165
|
+
const dashUrl = stringFlag(parsed, 'dashboard-url');
|
|
166
|
+
if (dashUrl !== undefined) overrides.dashboardBaseUrl = dashUrl;
|
|
167
|
+
|
|
168
|
+
const origins = resolveOrigins(overrides);
|
|
169
|
+
return { origins, profile: profileForOrigins(origins) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function buildContext(parsed: ParsedArgs): Promise<Context> {
|
|
173
|
+
const { origins, profile } = resolveBase(parsed);
|
|
174
|
+
const { auth, credentialKind } = await resolveAuth(parsed, origins, profile);
|
|
175
|
+
return {
|
|
176
|
+
api: new BrassApi(origins.apiBaseUrl, auth),
|
|
177
|
+
cwd: process.cwd(),
|
|
178
|
+
profile,
|
|
179
|
+
credentialKind,
|
|
180
|
+
log: createLogger(boolFlag(parsed, 'json')),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Build the request auth from the resolved credential: a service token is a
|
|
185
|
+
// static bearer; a login session refreshes short-lived access tokens. Returns
|
|
186
|
+
// the credential kind alongside, so a create can resolve the owning org from a
|
|
187
|
+
// human's membership but skip the lookup for a service token.
|
|
188
|
+
async function resolveAuth(
|
|
189
|
+
parsed: ParsedArgs,
|
|
190
|
+
origins: Origins,
|
|
191
|
+
profile: Profile,
|
|
192
|
+
): Promise<{ auth: AuthProvider; credentialKind: 'service' | 'session' }> {
|
|
193
|
+
const file = await readCredentialsFile();
|
|
194
|
+
const flagToken = stringFlag(parsed, 'token');
|
|
195
|
+
const envToken = process.env['BRASS_SERVICE_TOKEN'];
|
|
196
|
+
const credential = resolveCredential({
|
|
197
|
+
profile,
|
|
198
|
+
...(flagToken !== undefined ? { flagToken } : {}),
|
|
199
|
+
...(envToken !== undefined ? { envToken } : {}),
|
|
200
|
+
...(file !== null ? { file } : {}),
|
|
201
|
+
});
|
|
202
|
+
if (credential === null) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'No credential. Run `brass login`, or set BRASS_SERVICE_TOKEN (mint one in the dashboard: org Settings -> Service tokens), or pass --token.',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const auth =
|
|
208
|
+
credential.kind === 'service'
|
|
209
|
+
? serviceTokenAuth(credential.token)
|
|
210
|
+
: sessionAuth(origins.authBaseUrl, credential.sid);
|
|
211
|
+
return { auth, credentialKind: credential.kind };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function runLogin(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
215
|
+
const { origins, profile } = resolveBase(parsed);
|
|
216
|
+
// The two-phase variant for automation: `--start` mints the grant and
|
|
217
|
+
// exits; `--check` polls it. Plain `brass login` stays the blocking
|
|
218
|
+
// interactive flow.
|
|
219
|
+
const start = boolFlag(parsed, 'start');
|
|
220
|
+
const check = boolFlag(parsed, 'check');
|
|
221
|
+
const waitSeconds = parseWaitFlag(parsed);
|
|
222
|
+
if (start && check) throw new Error('Pass one of --start or --check.');
|
|
223
|
+
if (start) {
|
|
224
|
+
const code = await loginStart({
|
|
225
|
+
authBaseUrl: origins.authBaseUrl,
|
|
226
|
+
profile,
|
|
227
|
+
log,
|
|
228
|
+
...(boolFlag(parsed, 'new') ? { force: true } : {}),
|
|
229
|
+
});
|
|
230
|
+
// `--start --wait` is the whole sign-in in one command: relay the code it
|
|
231
|
+
// prints, then it holds for the approval up to the deadline.
|
|
232
|
+
if (code !== 0 || waitSeconds === undefined) return code;
|
|
233
|
+
return loginCheck({ profile, log, waitSeconds });
|
|
234
|
+
}
|
|
235
|
+
if (check) {
|
|
236
|
+
return loginCheck({ profile, log, ...(waitSeconds !== undefined ? { waitSeconds } : {}) });
|
|
237
|
+
}
|
|
238
|
+
// The RFC 8628 device grant: open the approval page (code prefilled), print
|
|
239
|
+
// the URL + code as a fallback for a headless box, and poll until approval.
|
|
240
|
+
const result = await loginDevice({ authBaseUrl: origins.authBaseUrl });
|
|
241
|
+
await writeStoredCredential(profile, { session: { sid: result.sessionToken } });
|
|
242
|
+
log.success(result.email ? `Signed in as ${result.email}.` : 'Signed in.');
|
|
243
|
+
log.result({ signed_in: true, ...(result.email !== undefined ? { email: result.email } : {}) });
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function runLogout(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
248
|
+
const { profile } = resolveBase(parsed);
|
|
249
|
+
await writeStoredCredential(profile, null);
|
|
250
|
+
log.success('Signed out.');
|
|
251
|
+
log.result({ signed_out: true });
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function runStatus(parsed: ParsedArgs, log: Logger): Promise<number> {
|
|
256
|
+
const { origins, profile } = resolveBase(parsed);
|
|
257
|
+
|
|
258
|
+
// Resolve the credential directly (not via `resolveAuth`, which throws when
|
|
259
|
+
// none is found); a missing credential is a reported state here.
|
|
260
|
+
const file = await readCredentialsFile();
|
|
261
|
+
const flagToken = stringFlag(parsed, 'token');
|
|
262
|
+
const envToken = process.env['BRASS_SERVICE_TOKEN'];
|
|
263
|
+
const credential = resolveCredential({
|
|
264
|
+
profile,
|
|
265
|
+
...(flagToken !== undefined ? { flagToken } : {}),
|
|
266
|
+
...(envToken !== undefined ? { envToken } : {}),
|
|
267
|
+
...(file !== null ? { file } : {}),
|
|
268
|
+
});
|
|
269
|
+
const api =
|
|
270
|
+
credential === null
|
|
271
|
+
? null
|
|
272
|
+
: new BrassApi(
|
|
273
|
+
origins.apiBaseUrl,
|
|
274
|
+
credential.kind === 'service'
|
|
275
|
+
? serviceTokenAuth(credential.token)
|
|
276
|
+
: sessionAuth(origins.authBaseUrl, credential.sid),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// What `publish` would target: the same app-id and manifest-name resolution
|
|
280
|
+
// it does, so the reported next step matches what actually runs.
|
|
281
|
+
const cwd = process.cwd();
|
|
282
|
+
const dir = parsed.positionals[1] ?? 'dist';
|
|
283
|
+
const state = await readProjectState(cwd);
|
|
284
|
+
const flagApp = stringFlag(parsed, 'app');
|
|
285
|
+
const envApp = process.env['BRASS_APP_ID'];
|
|
286
|
+
const appId = resolveAppId({
|
|
287
|
+
...(flagApp !== undefined ? { flagApp } : {}),
|
|
288
|
+
...(envApp !== undefined ? { envApp } : {}),
|
|
289
|
+
state,
|
|
290
|
+
profile,
|
|
291
|
+
});
|
|
292
|
+
const manifest = await readManifest(stringFlag(parsed, 'manifest') ?? 'brass-app.json');
|
|
293
|
+
const manifestName =
|
|
294
|
+
typeof manifest?.name === 'string' && manifest.name.trim() !== ''
|
|
295
|
+
? manifest.name.trim()
|
|
296
|
+
: null;
|
|
297
|
+
|
|
298
|
+
const pending = await readPendingLogin(profile);
|
|
299
|
+
const result = await status({
|
|
300
|
+
api,
|
|
301
|
+
profile,
|
|
302
|
+
log,
|
|
303
|
+
credential,
|
|
304
|
+
appId,
|
|
305
|
+
manifestName,
|
|
306
|
+
publishDir: dir,
|
|
307
|
+
pendingLogin:
|
|
308
|
+
pending === null
|
|
309
|
+
? null
|
|
310
|
+
: {
|
|
311
|
+
userCode: pending.userCode,
|
|
312
|
+
verificationUrl: pending.verificationUriComplete ?? pending.verificationUri,
|
|
313
|
+
expiresAt: pending.expiresAt,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
log.result(result);
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function runPublish(ctx: Context, parsed: ParsedArgs): Promise<unknown> {
|
|
321
|
+
const dir = parsed.positionals[1] ?? 'dist';
|
|
322
|
+
const flagApp = stringFlag(parsed, 'app');
|
|
323
|
+
const envApp = process.env['BRASS_APP_ID'];
|
|
324
|
+
const name = stringFlag(parsed, 'name');
|
|
325
|
+
const org = stringFlag(parsed, 'org');
|
|
326
|
+
const slug = stringFlag(parsed, 'slug');
|
|
327
|
+
const clientToken = stringFlag(parsed, 'client-token');
|
|
328
|
+
const visibility = parseVisibilityFlag(stringFlag(parsed, 'visibility'));
|
|
329
|
+
const requireAccess = parseGateFlag(stringFlag(parsed, 'gate'));
|
|
330
|
+
const state = await readProjectState(ctx.cwd);
|
|
331
|
+
const appId = resolveAppId({
|
|
332
|
+
...(flagApp !== undefined ? { flagApp } : {}),
|
|
333
|
+
...(envApp !== undefined ? { envApp } : {}),
|
|
334
|
+
state,
|
|
335
|
+
profile: ctx.profile,
|
|
336
|
+
});
|
|
337
|
+
return publish(ctx, {
|
|
338
|
+
dir,
|
|
339
|
+
manifestPath: stringFlag(parsed, 'manifest') ?? 'brass-app.json',
|
|
340
|
+
...(appId !== null ? { appId } : {}),
|
|
341
|
+
...(name !== undefined ? { name } : {}),
|
|
342
|
+
...(org !== undefined ? { organizationId: org } : {}),
|
|
343
|
+
...(clientToken !== undefined ? { clientToken } : {}),
|
|
344
|
+
...(slug !== undefined ? { slug } : {}),
|
|
345
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
346
|
+
...(requireAccess !== undefined ? { requireAccess } : {}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// How long `--wait` holds for the approval: bare `--wait` takes the default,
|
|
351
|
+
// `--wait <seconds>` an explicit bound. Capped so a mistyped value cannot
|
|
352
|
+
// park an automated caller for hours; the grant survives the cap either way,
|
|
353
|
+
// so a second check resumes the same sign-in.
|
|
354
|
+
const DEFAULT_WAIT_SECONDS = 120;
|
|
355
|
+
const MAX_WAIT_SECONDS = 3600;
|
|
356
|
+
|
|
357
|
+
function parseWaitFlag(parsed: ParsedArgs): number | undefined {
|
|
358
|
+
const raw = parsed.flags['wait'];
|
|
359
|
+
if (raw === undefined) return undefined;
|
|
360
|
+
if (raw === true) return DEFAULT_WAIT_SECONDS;
|
|
361
|
+
const seconds = Number(raw);
|
|
362
|
+
if (!Number.isInteger(seconds) || seconds <= 0 || seconds > MAX_WAIT_SECONDS) {
|
|
363
|
+
throw new Error(`Invalid --wait "${raw}" (expected whole seconds, 1 to ${MAX_WAIT_SECONDS})`);
|
|
364
|
+
}
|
|
365
|
+
return seconds;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Validate `--visibility` against the three reserved values, so a typo is a
|
|
369
|
+
// clear CLI error rather than a server 400 mid-publish.
|
|
370
|
+
function parseVisibilityFlag(value: string | undefined): AppVisibility | undefined {
|
|
371
|
+
if (value === undefined) return undefined;
|
|
372
|
+
if (value !== 'private' && value !== 'invitee_visible' && value !== 'public') {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Invalid --visibility "${value}" (expected private, invitee_visible, or public)`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Map `--gate on|off` to the desired load-gate state (on = audience-gated).
|
|
381
|
+
function parseGateFlag(value: string | undefined): boolean | undefined {
|
|
382
|
+
if (value === undefined) return undefined;
|
|
383
|
+
if (value !== 'on' && value !== 'off') {
|
|
384
|
+
throw new Error(`Invalid --gate "${value}" (expected on or off)`);
|
|
385
|
+
}
|
|
386
|
+
return value === 'on';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function runSchema(ctx: Context, parsed: ParsedArgs): Promise<unknown> {
|
|
390
|
+
const sub = parsed.positionals[1];
|
|
391
|
+
if (sub === 'pull') {
|
|
392
|
+
const docId = stringFlag(parsed, 'doc');
|
|
393
|
+
if (docId === undefined) throw new Error('brass schema pull requires --doc <docId>');
|
|
394
|
+
return schemaPull(ctx, { docId, outPath: stringFlag(parsed, 'out') ?? 'brass-app.json' });
|
|
395
|
+
}
|
|
396
|
+
throw new Error('Usage: brass schema pull --doc <docId> [--out brass-app.json]');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function runAgents(ctx: Context, parsed: ParsedArgs): Promise<unknown> {
|
|
400
|
+
if (parsed.positionals[1] !== 'pull') {
|
|
401
|
+
throw new Error(
|
|
402
|
+
'Usage: brass agents pull [--out AGENTS.md | --stdout] [--org <organizationId>]',
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const org = stringFlag(parsed, 'org');
|
|
406
|
+
return agentsPull(ctx, {
|
|
407
|
+
outPath: stringFlag(parsed, 'out') ?? 'AGENTS.md',
|
|
408
|
+
...(boolFlag(parsed, 'stdout') ? { stdout: true } : {}),
|
|
409
|
+
...(org !== undefined ? { organizationId: org } : {}),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function formatError(err: unknown): string {
|
|
414
|
+
if (err instanceof BrassApiError) {
|
|
415
|
+
if (err.status === 401) {
|
|
416
|
+
return `error: ${err.message} (the credential was rejected; check BRASS_SERVICE_TOKEN or --token)`;
|
|
417
|
+
}
|
|
418
|
+
return `error: ${err.message}`;
|
|
419
|
+
}
|
|
420
|
+
if (err instanceof Error) return `error: ${err.message}`;
|
|
421
|
+
return `error: ${String(err)}`;
|
|
422
|
+
}
|