@mitsein-ai/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/README.md +142 -0
- package/dist/index.js +143 -0
- package/package.json +49 -0
- package/src/commands/agent.ts +91 -0
- package/src/commands/api-auto.ts +245 -0
- package/src/commands/auth-browser.ts +126 -0
- package/src/commands/auth-device.ts +97 -0
- package/src/commands/auth-internal.ts +49 -0
- package/src/commands/auth.ts +105 -0
- package/src/commands/command-opts.ts +41 -0
- package/src/commands/dev.ts +100 -0
- package/src/commands/messages.ts +61 -0
- package/src/commands/project.ts +77 -0
- package/src/commands/run.ts +134 -0
- package/src/commands/thread.ts +187 -0
- package/src/commands/version.ts +17 -0
- package/src/core/client.ts +201 -0
- package/src/core/config.ts +26 -0
- package/src/core/credentials.ts +173 -0
- package/src/core/errors.ts +76 -0
- package/src/core/openapi.ts +182 -0
- package/src/core/output.ts +59 -0
- package/src/core/sse.ts +135 -0
- package/src/core/waiters-output.ts +95 -0
- package/src/core/waiters.ts +169 -0
- package/src/index.ts +66 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { ApiClient } from '../core/client.js';
|
|
3
|
+
import { resolveCredentials } from '../core/credentials.js';
|
|
4
|
+
import { CliError, ExitCode, handleErrors } from '../core/errors.js';
|
|
5
|
+
import { emit, setJsonMode } from '../core/output.js';
|
|
6
|
+
import { waitForRun } from '../core/waiters.js';
|
|
7
|
+
import { commaSet, httpTimeoutSec, readGlobals } from './command-opts.js';
|
|
8
|
+
|
|
9
|
+
export function registerAgent(program: Command): void {
|
|
10
|
+
const agent = program.command('agent').description('Agent operations');
|
|
11
|
+
|
|
12
|
+
agent
|
|
13
|
+
.command('invoke <thread_id> <prompt>')
|
|
14
|
+
.description('Send a message, start the agent, optional wait/stream')
|
|
15
|
+
.option('--wait', 'Block until run completes', false)
|
|
16
|
+
.option('--stream', 'Stream run events', false)
|
|
17
|
+
.option('--cancel-on-interrupt', 'Cancel run on Ctrl-C (reserved)', false)
|
|
18
|
+
.option('--filter <types>', 'Comma-separated SSE event types')
|
|
19
|
+
.option('--model <name>', 'Model override')
|
|
20
|
+
.option('--timeout <sec>', 'Wait/stream timeout (0 = no limit)', '120')
|
|
21
|
+
.action(
|
|
22
|
+
handleErrors(async function agentInvokeAction(this: Command, threadId: string, prompt: string) {
|
|
23
|
+
const g = readGlobals(this);
|
|
24
|
+
const opts = this.opts() as {
|
|
25
|
+
wait?: boolean;
|
|
26
|
+
stream?: boolean;
|
|
27
|
+
filter?: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
timeout?: string;
|
|
30
|
+
};
|
|
31
|
+
setJsonMode(Boolean(g.json));
|
|
32
|
+
|
|
33
|
+
if (opts.wait && opts.stream) {
|
|
34
|
+
throw new CliError('--wait and --stream are mutually exclusive', ExitCode.USAGE_ERROR);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const creds = resolveCredentials({
|
|
38
|
+
token: g.token,
|
|
39
|
+
endpoint: g.endpoint,
|
|
40
|
+
profile: g.profile ?? 'e2e',
|
|
41
|
+
real: g.real,
|
|
42
|
+
});
|
|
43
|
+
const client = new ApiClient(creds, {
|
|
44
|
+
debug: g.debug,
|
|
45
|
+
timeoutSec: httpTimeoutSec(g),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const addResult = await client.post('/api/message/add', {
|
|
49
|
+
thread_id: threadId,
|
|
50
|
+
content: prompt,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const startData: Record<string, string> = {};
|
|
54
|
+
if (opts.model) {
|
|
55
|
+
startData.model_name = opts.model;
|
|
56
|
+
}
|
|
57
|
+
const startResult = (await client.postForm(`/api/thread/${threadId}/agent/start`, startData)) as {
|
|
58
|
+
agent_run_id?: string;
|
|
59
|
+
};
|
|
60
|
+
const agentRunId = startResult.agent_run_id ?? '';
|
|
61
|
+
|
|
62
|
+
if (!opts.wait && !opts.stream) {
|
|
63
|
+
emit({
|
|
64
|
+
message: addResult,
|
|
65
|
+
agent_run_id: agentRunId,
|
|
66
|
+
status: 'started',
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rawT = opts.timeout ?? '120';
|
|
72
|
+
const tNum = Number.parseFloat(rawT);
|
|
73
|
+
const effectiveTimeout = Number.isFinite(tNum) && tNum === 0 ? null : Number.isFinite(tNum) ? tNum : 120;
|
|
74
|
+
|
|
75
|
+
const result = await waitForRun({
|
|
76
|
+
endpoint: creds.endpoint,
|
|
77
|
+
token: creds.token,
|
|
78
|
+
agent_run_id: agentRunId,
|
|
79
|
+
timeout: effectiveTimeout ?? undefined,
|
|
80
|
+
stream_output: Boolean(opts.stream),
|
|
81
|
+
json_mode: Boolean(g.json),
|
|
82
|
+
debug: g.debug,
|
|
83
|
+
event_filter: commaSet(opts.filter),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (opts.wait && !opts.stream) {
|
|
87
|
+
emit(result);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import consola from 'consola';
|
|
4
|
+
import { ApiClient } from '../core/client.js';
|
|
5
|
+
import { CliError, ExitCode, handleErrors } from '../core/errors.js';
|
|
6
|
+
import { buildIndex, type OperationInfo } from '../core/openapi.js';
|
|
7
|
+
import { emit, setJsonMode } from '../core/output.js';
|
|
8
|
+
import { readGlobals } from './command-opts.js';
|
|
9
|
+
|
|
10
|
+
function parseExtraArgs(args: string[]): Record<string, string> {
|
|
11
|
+
const params: Record<string, string> = {};
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < args.length) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
if (arg === undefined) {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
if (arg.startsWith('--')) {
|
|
19
|
+
const key = arg.replace(/^-+/, '');
|
|
20
|
+
const next = args[i + 1];
|
|
21
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
22
|
+
params[key] = next;
|
|
23
|
+
i += 2;
|
|
24
|
+
} else {
|
|
25
|
+
params[key] = 'true';
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return params;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveBody(body: string | undefined, bodyStdin: boolean): unknown | undefined {
|
|
36
|
+
if (bodyStdin) {
|
|
37
|
+
const raw = readFileSync(0, 'utf8');
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw) as unknown;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
throw new CliError(`Invalid JSON from stdin: ${e}`, ExitCode.USAGE_ERROR);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (body === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (body.startsWith('@')) {
|
|
48
|
+
const filePath = body.slice(1);
|
|
49
|
+
try {
|
|
50
|
+
const text = readFileSync(filePath, 'utf8');
|
|
51
|
+
return JSON.parse(text) as unknown;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
54
|
+
throw new CliError(`File not found: ${filePath}`, ExitCode.USAGE_ERROR);
|
|
55
|
+
}
|
|
56
|
+
throw new CliError(`Invalid JSON in ${filePath}: ${e}`, ExitCode.USAGE_ERROR);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(body) as unknown;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
throw new CliError(`Invalid JSON body: ${e}`, ExitCode.USAGE_ERROR);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function executeOperation(
|
|
67
|
+
client: ApiClient,
|
|
68
|
+
op: OperationInfo,
|
|
69
|
+
body: unknown | undefined,
|
|
70
|
+
extraParams: Record<string, string>
|
|
71
|
+
): Promise<unknown> {
|
|
72
|
+
let path = op.path;
|
|
73
|
+
const method = op.method;
|
|
74
|
+
const pathParams = [...path.matchAll(/\{(\w+)\}/g)].map((m) => m[1] ?? '');
|
|
75
|
+
const queryParams = { ...extraParams };
|
|
76
|
+
for (const param of pathParams) {
|
|
77
|
+
if (param in queryParams) {
|
|
78
|
+
const v = queryParams[param];
|
|
79
|
+
delete queryParams[param];
|
|
80
|
+
path = path.replace(`{${param}}`, v ?? '');
|
|
81
|
+
} else {
|
|
82
|
+
throw new CliError(`Missing required path parameter: --${param}`, ExitCode.USAGE_ERROR);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (method === 'get') {
|
|
87
|
+
const q = Object.keys(queryParams).length > 0 ? { ...queryParams } : undefined;
|
|
88
|
+
return client.get(path, q as Record<string, unknown> | undefined);
|
|
89
|
+
}
|
|
90
|
+
if (method === 'post') {
|
|
91
|
+
return client.post(path, body);
|
|
92
|
+
}
|
|
93
|
+
if (method === 'patch') {
|
|
94
|
+
return client.patch(path, body);
|
|
95
|
+
}
|
|
96
|
+
if (method === 'put') {
|
|
97
|
+
return client.put(path, body);
|
|
98
|
+
}
|
|
99
|
+
if (method === 'delete') {
|
|
100
|
+
return client.delete(path);
|
|
101
|
+
}
|
|
102
|
+
throw new CliError(`Unsupported HTTP method: ${method}`, ExitCode.USAGE_ERROR);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function tagHuman(data: unknown): void {
|
|
106
|
+
const d = data as { tag?: string; operations?: number };
|
|
107
|
+
consola.log(` \x1b[1m${d.tag ?? ''}\x1b[0m (${d.operations ?? 0} operations)`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function opHuman(data: unknown): void {
|
|
111
|
+
const d = data as { method?: string; operation?: string; summary?: string };
|
|
112
|
+
const method = d.method ?? '';
|
|
113
|
+
const color =
|
|
114
|
+
method === 'GET'
|
|
115
|
+
? '\x1b[32m'
|
|
116
|
+
: method === 'POST'
|
|
117
|
+
? '\x1b[34m'
|
|
118
|
+
: method === 'PATCH'
|
|
119
|
+
? '\x1b[33m'
|
|
120
|
+
: method === 'DELETE'
|
|
121
|
+
? '\x1b[31m'
|
|
122
|
+
: '\x1b[37m';
|
|
123
|
+
const op = (d.operation ?? '').padEnd(30);
|
|
124
|
+
consola.log(` ${color}${method.padEnd(6)}\x1b[0m ${op} ${d.summary ?? ''}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function registerApi(program: Command): void {
|
|
128
|
+
const api = program
|
|
129
|
+
.command('api')
|
|
130
|
+
.description('Auto-generated API commands from OpenAPI spec');
|
|
131
|
+
|
|
132
|
+
api
|
|
133
|
+
.command('list-tags')
|
|
134
|
+
.description('List all available API tags')
|
|
135
|
+
.option('--endpoint <url>', 'API base URL')
|
|
136
|
+
.action(
|
|
137
|
+
handleErrors(async function listTagsAction(this: Command) {
|
|
138
|
+
const g = readGlobals(this);
|
|
139
|
+
const o = this.opts() as { endpoint?: string };
|
|
140
|
+
setJsonMode(Boolean(g.json));
|
|
141
|
+
const index = await buildIndex({ endpoint: o.endpoint ?? g.endpoint });
|
|
142
|
+
for (const tag of index.tags) {
|
|
143
|
+
const ops = index.operationsForTag(tag);
|
|
144
|
+
emit({ tag, operations: ops.length }, tagHuman);
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
api
|
|
150
|
+
.command('list-ops <tag>')
|
|
151
|
+
.description('List operations for a tag')
|
|
152
|
+
.option('--endpoint <url>', 'API base URL')
|
|
153
|
+
.action(
|
|
154
|
+
handleErrors(async function listOpsAction(this: Command, tag: string) {
|
|
155
|
+
const g = readGlobals(this);
|
|
156
|
+
const o = this.opts() as { endpoint?: string };
|
|
157
|
+
setJsonMode(Boolean(g.json));
|
|
158
|
+
const index = await buildIndex({ endpoint: o.endpoint ?? g.endpoint });
|
|
159
|
+
const ops = index.operationsForTag(tag);
|
|
160
|
+
if (ops.length === 0) {
|
|
161
|
+
throw new CliError(
|
|
162
|
+
`Unknown tag '${tag}'. Available: ${index.tags.join(', ')}`,
|
|
163
|
+
ExitCode.USAGE_ERROR
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
for (const op of ops) {
|
|
167
|
+
emit(
|
|
168
|
+
{ operation: op.operation_id, method: op.method.toUpperCase(), path: op.path, summary: op.summary },
|
|
169
|
+
opHuman
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
api
|
|
176
|
+
.command('api-call <tag> <operation>')
|
|
177
|
+
.description(
|
|
178
|
+
'Execute an API operation (examples: mitsein api api-call threads list-threads --body \'{"limit":20}\')'
|
|
179
|
+
)
|
|
180
|
+
.allowUnknownOption(true)
|
|
181
|
+
.allowExcessArguments(true)
|
|
182
|
+
.option('--body <json>', 'JSON body: inline or @file.json')
|
|
183
|
+
.option('--body-stdin', 'Read JSON body from stdin', false)
|
|
184
|
+
.option('--endpoint <url>', 'API base URL')
|
|
185
|
+
.option('--token <token>', 'Bearer token')
|
|
186
|
+
.option('--timeout <sec>', 'HTTP timeout (0 = none)', '30')
|
|
187
|
+
.option('--debug', 'Print HTTP details', false)
|
|
188
|
+
.action(
|
|
189
|
+
handleErrors(async function apiCallAction(this: Command, tag: string, operation: string) {
|
|
190
|
+
const g = readGlobals(this);
|
|
191
|
+
const opts = this.opts() as {
|
|
192
|
+
body?: string;
|
|
193
|
+
bodyStdin?: boolean;
|
|
194
|
+
endpoint?: string;
|
|
195
|
+
token?: string;
|
|
196
|
+
timeout?: string;
|
|
197
|
+
debug?: boolean;
|
|
198
|
+
};
|
|
199
|
+
setJsonMode(Boolean(g.json));
|
|
200
|
+
|
|
201
|
+
const index = await buildIndex({ endpoint: opts.endpoint ?? g.endpoint });
|
|
202
|
+
const op = index.getOperation(operation);
|
|
203
|
+
if (op === undefined) {
|
|
204
|
+
const ops = index.operationsForTag(tag);
|
|
205
|
+
if (ops.length === 0) {
|
|
206
|
+
const available =
|
|
207
|
+
index.tags.length > 0
|
|
208
|
+
? index.tags.join(', ')
|
|
209
|
+
: '(none — run `mitsein dev openapi --refresh`)';
|
|
210
|
+
throw new CliError(`Unknown tag '${tag}'. Available tags: ${available}`, ExitCode.USAGE_ERROR);
|
|
211
|
+
}
|
|
212
|
+
const available = ops.map((o) => o.operation_id).join(', ');
|
|
213
|
+
throw new CliError(
|
|
214
|
+
`Unknown operation '${operation}' in tag '${tag}'. Available: ${available}`,
|
|
215
|
+
ExitCode.USAGE_ERROR
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (op.tag !== tag) {
|
|
219
|
+
throw new CliError(
|
|
220
|
+
`Operation '${operation}' belongs to tag '${op.tag}', not '${tag}'.`,
|
|
221
|
+
ExitCode.USAGE_ERROR
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const requestBody = resolveBody(opts.body, Boolean(opts.bodyStdin));
|
|
226
|
+
const extra = parseExtraArgs((this.args as string[]).slice(2));
|
|
227
|
+
|
|
228
|
+
const rawTimeout = opts.timeout ?? g.timeout ?? '30';
|
|
229
|
+
const tn = Number.parseFloat(rawTimeout);
|
|
230
|
+
const effectiveTimeout = Number.isFinite(tn) && tn === 0 ? undefined : Number.isFinite(tn) ? tn : 30;
|
|
231
|
+
|
|
232
|
+
const client = ApiClient.fromOptions({
|
|
233
|
+
token: opts.token ?? g.token,
|
|
234
|
+
endpoint: opts.endpoint ?? g.endpoint,
|
|
235
|
+
profile: g.profile ?? 'e2e',
|
|
236
|
+
real: g.real,
|
|
237
|
+
timeoutSec: effectiveTimeout,
|
|
238
|
+
debug: Boolean(opts.debug ?? g.debug),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await executeOperation(client, op, requestBody, extra);
|
|
242
|
+
emit(result);
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { parse as parseQuery, type ParsedUrlQuery } from 'node:querystring';
|
|
3
|
+
import consola from 'consola';
|
|
4
|
+
import { CliError, ExitCode } from '../core/errors.js';
|
|
5
|
+
import { emit } from '../core/output.js';
|
|
6
|
+
import { LOCALHOST_PORT, openBrowserUrl, saveOAuthCredentials } from './auth-internal.js';
|
|
7
|
+
|
|
8
|
+
function firstQuery(q: ParsedUrlQuery, key: string): string | undefined {
|
|
9
|
+
const v = q[key];
|
|
10
|
+
if (Array.isArray(v)) {
|
|
11
|
+
return v[0];
|
|
12
|
+
}
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function loginBrowserFlow(
|
|
17
|
+
endpoint: string,
|
|
18
|
+
provider: string,
|
|
19
|
+
profile: string,
|
|
20
|
+
jsonOutput: boolean
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const received: Record<string, string | boolean> = {};
|
|
23
|
+
|
|
24
|
+
await new Promise<void>((resolve, reject) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
|
|
28
|
+
const finish = (): void => {
|
|
29
|
+
if (settled) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
settled = true;
|
|
33
|
+
if (timer !== undefined) {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
server.close(() => resolve());
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const server = createServer((req, res) => {
|
|
40
|
+
const u = req.url ?? '';
|
|
41
|
+
let pathname = '';
|
|
42
|
+
try {
|
|
43
|
+
pathname = new URL(u, 'http://127.0.0.1').pathname;
|
|
44
|
+
} catch {
|
|
45
|
+
pathname = '';
|
|
46
|
+
}
|
|
47
|
+
if (pathname !== '/callback') {
|
|
48
|
+
res.writeHead(404);
|
|
49
|
+
res.end();
|
|
50
|
+
finish();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const q = u.includes('?') ? parseQuery(u.split('?', 2)[1] ?? '') : {};
|
|
54
|
+
if (q.error) {
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
56
|
+
const msg = firstQuery(q, 'error_description') ?? firstQuery(q, 'error') ?? 'Unknown error';
|
|
57
|
+
res.end(`<html><body><h2>Login failed</h2><p>${String(msg)}</p></body></html>`);
|
|
58
|
+
received.error = String(msg);
|
|
59
|
+
finish();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (q.account) {
|
|
63
|
+
received.account = firstQuery(q, 'account') ?? '';
|
|
64
|
+
}
|
|
65
|
+
if (q.email) {
|
|
66
|
+
received.email = firstQuery(q, 'email') ?? '';
|
|
67
|
+
}
|
|
68
|
+
if (q.token) {
|
|
69
|
+
received.token = firstQuery(q, 'token') ?? '';
|
|
70
|
+
}
|
|
71
|
+
if (q.session_id) {
|
|
72
|
+
received.session_id = firstQuery(q, 'session_id') ?? '';
|
|
73
|
+
}
|
|
74
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
75
|
+
res.end(
|
|
76
|
+
'<html><body><h2>Login successful!</h2><p>You can close this window.</p></body></html>'
|
|
77
|
+
);
|
|
78
|
+
received.success = true;
|
|
79
|
+
finish();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
server.once('error', (err) => {
|
|
83
|
+
if (!settled) {
|
|
84
|
+
settled = true;
|
|
85
|
+
if (timer !== undefined) {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
reject(err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
timer = setTimeout(() => finish(), 300_000);
|
|
93
|
+
|
|
94
|
+
server.listen(LOCALHOST_PORT, '127.0.0.1', () => {
|
|
95
|
+
const loginUrl = `${endpoint}/api/auth/cognito/login?provider=${encodeURIComponent(provider)}`;
|
|
96
|
+
if (!jsonOutput) {
|
|
97
|
+
consola.log('\nOpening browser for login...');
|
|
98
|
+
consola.log(`If browser does not open, visit: ${loginUrl}\n`);
|
|
99
|
+
}
|
|
100
|
+
openBrowserUrl(loginUrl);
|
|
101
|
+
if (!jsonOutput) {
|
|
102
|
+
consola.log('Waiting for browser login...');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (received.error !== undefined) {
|
|
108
|
+
throw new CliError(`Login failed: ${String(received.error)}`, ExitCode.BUSINESS_ERROR);
|
|
109
|
+
}
|
|
110
|
+
if (received.success === true) {
|
|
111
|
+
const result = {
|
|
112
|
+
access_token: String(received.token ?? ''),
|
|
113
|
+
user_email: String(received.email ?? ''),
|
|
114
|
+
user_id: String(received.account ?? ''),
|
|
115
|
+
};
|
|
116
|
+
saveOAuthCredentials(profile, endpoint, result);
|
|
117
|
+
emit(
|
|
118
|
+
{ status: 'logged_in', email: result.user_email, profile },
|
|
119
|
+
() => {
|
|
120
|
+
consola.success(`Logged in as ${result.user_email}`);
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
throw new CliError('Login timed out. Try again.', ExitCode.TIMEOUT);
|
|
126
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import consola from 'consola';
|
|
2
|
+
import { CliError, ExitCode } from '../core/errors.js';
|
|
3
|
+
import { emit } from '../core/output.js';
|
|
4
|
+
import { openBrowserUrl, saveOAuthCredentials } from './auth-internal.js';
|
|
5
|
+
|
|
6
|
+
async function sleep(ms: number): Promise<void> {
|
|
7
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loginDeviceCodeFlow(
|
|
11
|
+
endpoint: string,
|
|
12
|
+
profile: string,
|
|
13
|
+
jsonOutput: boolean
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const deviceRes = await fetch(`${endpoint}/api/auth/cli/device`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
signal: AbortSignal.timeout(10_000),
|
|
18
|
+
});
|
|
19
|
+
if (!deviceRes.ok) {
|
|
20
|
+
throw new CliError(`Failed to create device code: HTTP ${deviceRes.status}`, ExitCode.HTTP_ERROR);
|
|
21
|
+
}
|
|
22
|
+
const data = (await deviceRes.json()) as {
|
|
23
|
+
device_code: string;
|
|
24
|
+
user_code: string;
|
|
25
|
+
verification_url: string;
|
|
26
|
+
expires_in: number;
|
|
27
|
+
interval?: number;
|
|
28
|
+
};
|
|
29
|
+
const {
|
|
30
|
+
device_code: deviceCodeVal,
|
|
31
|
+
user_code: userCode,
|
|
32
|
+
verification_url: verificationUrl,
|
|
33
|
+
expires_in: expiresIn,
|
|
34
|
+
} = data;
|
|
35
|
+
const interval = data.interval ?? 5;
|
|
36
|
+
const fullUrl = `${verificationUrl}?user_code=${encodeURIComponent(userCode)}`;
|
|
37
|
+
|
|
38
|
+
if (!jsonOutput) {
|
|
39
|
+
consola.log('\nMitsein CLI Login\n');
|
|
40
|
+
consola.log('Open this URL in your browser:');
|
|
41
|
+
consola.log(fullUrl);
|
|
42
|
+
consola.log(`Or go to ${verificationUrl} and enter code:`);
|
|
43
|
+
consola.log(userCode);
|
|
44
|
+
consola.log(`Waiting for authorization (expires in ${Math.floor(expiresIn / 60)} min)...`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
openBrowserUrl(fullUrl);
|
|
48
|
+
|
|
49
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
await sleep(interval * 1000);
|
|
52
|
+
try {
|
|
53
|
+
const pollResp = await fetch(`${endpoint}/api/auth/cli/token`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ device_code: deviceCodeVal, grant_type: 'device_code' }),
|
|
57
|
+
signal: AbortSignal.timeout(10_000),
|
|
58
|
+
});
|
|
59
|
+
const result = (await pollResp.json()) as {
|
|
60
|
+
status?: string;
|
|
61
|
+
user_email?: string;
|
|
62
|
+
access_token?: string;
|
|
63
|
+
user_id?: string;
|
|
64
|
+
expires_in?: number;
|
|
65
|
+
};
|
|
66
|
+
const status = result.status;
|
|
67
|
+
|
|
68
|
+
if (status === 'authorized') {
|
|
69
|
+
saveOAuthCredentials(profile, endpoint, result);
|
|
70
|
+
emit(
|
|
71
|
+
{ status: 'logged_in', email: result.user_email ?? '', profile },
|
|
72
|
+
() => {
|
|
73
|
+
consola.success(`\nLogged in as ${result.user_email ?? ''}`);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (status === 'expired') {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
'Device code expired. Run `mitsein auth login` again.',
|
|
81
|
+
ExitCode.BUSINESS_ERROR
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (!jsonOutput) {
|
|
85
|
+
process.stderr.write('.');
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (e instanceof CliError) {
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
if (!jsonOutput) {
|
|
92
|
+
consola.log(`\nPoll error: ${e}, retrying...`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new CliError('Login timed out. Run `mitsein auth login` again.', ExitCode.TIMEOUT);
|
|
97
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { ensureConfigDir, getOauthPath, DEFAULT_ENDPOINT } from '../core/config.js';
|
|
5
|
+
|
|
6
|
+
export const LOCALHOST_PORT = 19284;
|
|
7
|
+
|
|
8
|
+
export function saveOAuthCredentials(
|
|
9
|
+
profile: string,
|
|
10
|
+
endpoint: string,
|
|
11
|
+
result: {
|
|
12
|
+
access_token?: string;
|
|
13
|
+
user_email?: string;
|
|
14
|
+
user_id?: string;
|
|
15
|
+
expires_in?: number;
|
|
16
|
+
}
|
|
17
|
+
): void {
|
|
18
|
+
ensureConfigDir();
|
|
19
|
+
const oauthPath = getOauthPath(profile);
|
|
20
|
+
mkdirSync(dirname(oauthPath), { recursive: true, mode: 0o700 });
|
|
21
|
+
const data = {
|
|
22
|
+
access_token: result.access_token ?? '',
|
|
23
|
+
user_email: result.user_email ?? '',
|
|
24
|
+
user_id: result.user_id ?? '',
|
|
25
|
+
endpoint,
|
|
26
|
+
created_at: Date.now() / 1000,
|
|
27
|
+
expires_in: result.expires_in ?? 86400,
|
|
28
|
+
};
|
|
29
|
+
writeFileSync(oauthPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
30
|
+
chmodSync(oauthPath, 0o600);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function openBrowserUrl(url: string): void {
|
|
34
|
+
try {
|
|
35
|
+
if (process.platform === 'darwin') {
|
|
36
|
+
execFileSync('open', [url], { stdio: 'ignore' });
|
|
37
|
+
} else if (process.platform === 'win32') {
|
|
38
|
+
execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
39
|
+
} else {
|
|
40
|
+
execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeEndpoint(endpoint: string | undefined): string {
|
|
48
|
+
return (endpoint ?? process.env.MITSEIN_API_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, '');
|
|
49
|
+
}
|