faces-cli 1.6.17 → 1.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +2 -0
- package/dist/client.js +12 -4
- package/dist/commands/auth/connect.d.ts +2 -1
- package/dist/commands/auth/connect.js +162 -194
- package/dist/commands/auth/connections.js +13 -1
- package/oclif.manifest.json +914 -920
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ export declare class FacesAPIError extends Error {
|
|
|
2
2
|
statusCode: number;
|
|
3
3
|
errorCode?: string;
|
|
4
4
|
fallbackAvailable?: boolean;
|
|
5
|
+
details?: Record<string, unknown>;
|
|
5
6
|
constructor(statusCode: number, message: string, opts?: {
|
|
6
7
|
errorCode?: string;
|
|
7
8
|
fallbackAvailable?: boolean;
|
|
9
|
+
details?: Record<string, unknown>;
|
|
8
10
|
});
|
|
9
11
|
}
|
|
10
12
|
export interface ResponseWithHeaders<T = unknown> {
|
package/dist/client.js
CHANGED
|
@@ -2,12 +2,14 @@ export class FacesAPIError extends Error {
|
|
|
2
2
|
statusCode;
|
|
3
3
|
errorCode;
|
|
4
4
|
fallbackAvailable;
|
|
5
|
+
details;
|
|
5
6
|
constructor(statusCode, message, opts) {
|
|
6
7
|
super(message);
|
|
7
8
|
this.statusCode = statusCode;
|
|
8
9
|
this.name = 'FacesAPIError';
|
|
9
10
|
this.errorCode = opts?.errorCode;
|
|
10
11
|
this.fallbackAvailable = opts?.fallbackAvailable;
|
|
12
|
+
this.details = opts?.details;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
export class FacesClient {
|
|
@@ -46,6 +48,7 @@ export class FacesClient {
|
|
|
46
48
|
let msg = resp.statusText;
|
|
47
49
|
let errorCode;
|
|
48
50
|
let fallbackAvailable;
|
|
51
|
+
let details;
|
|
49
52
|
try {
|
|
50
53
|
const body = await resp.json();
|
|
51
54
|
// Structured OAuth rejection (422)
|
|
@@ -56,11 +59,16 @@ export class FacesClient {
|
|
|
56
59
|
}
|
|
57
60
|
else {
|
|
58
61
|
const raw = body.detail ?? body.error ?? body.message ?? msg;
|
|
59
|
-
if (typeof raw === 'object' && raw !== null
|
|
60
|
-
|
|
62
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
63
|
+
// Structured error detail, e.g. {error, message, settings_url, settings_steps}
|
|
64
|
+
const obj = raw;
|
|
65
|
+
details = obj;
|
|
66
|
+
if (typeof obj.error === 'string')
|
|
67
|
+
errorCode = obj.error;
|
|
68
|
+
msg = 'message' in obj ? String(obj.message) : JSON.stringify(obj);
|
|
61
69
|
}
|
|
62
70
|
else {
|
|
63
|
-
msg =
|
|
71
|
+
msg = String(raw);
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
}
|
|
@@ -71,7 +79,7 @@ export class FacesClient {
|
|
|
71
79
|
msg = `Payment required: ${msg}`;
|
|
72
80
|
if (resp.status === 403)
|
|
73
81
|
msg = `Forbidden: ${msg}`;
|
|
74
|
-
return new FacesAPIError(resp.status, msg, { errorCode, fallbackAvailable });
|
|
82
|
+
return new FacesAPIError(resp.status, msg, { errorCode, fallbackAvailable, details });
|
|
75
83
|
}
|
|
76
84
|
async get(path, opts = {}) {
|
|
77
85
|
let url = this.url(path);
|
|
@@ -5,11 +5,12 @@ export default class AuthConnect extends BaseCommand {
|
|
|
5
5
|
provider: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
6
|
};
|
|
7
7
|
static flags: {
|
|
8
|
-
manual: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
8
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
9
|
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
10
|
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
11
|
};
|
|
13
12
|
static examples: string[];
|
|
14
13
|
run(): Promise<unknown>;
|
|
14
|
+
private startDevice;
|
|
15
|
+
private handleStartError;
|
|
15
16
|
}
|
|
@@ -1,71 +1,27 @@
|
|
|
1
|
-
import { Args
|
|
2
|
-
import * as http from 'node:http';
|
|
3
|
-
import * as readline from 'node:readline';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
5
2
|
import { BaseCommand } from '../../base.js';
|
|
6
3
|
import { FacesAPIError } from '../../client.js';
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const [cmd, args] = p === 'darwin' ? ['open', [url]] :
|
|
21
|
-
p === 'win32' ? ['cmd', ['/c', 'start', url]] :
|
|
22
|
-
['xdg-open', [url]];
|
|
23
|
-
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
4
|
+
const VERIFICATION_FALLBACK = 'https://auth.openai.com/codex/device';
|
|
5
|
+
const SETTINGS_URL = 'https://chatgpt.com/#settings/Security';
|
|
6
|
+
const HARD_STOP_MS = 15 * 60 * 1000; // device codes expire after 15 minutes
|
|
7
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8
|
+
const emit = (msg = '') => process.stderr.write(msg + '\n');
|
|
9
|
+
// Unwrap a possible StandardResponse envelope ({data: {...}}) without
|
|
10
|
+
// clobbering a flat payload that already has the expected key.
|
|
11
|
+
function unwrap(raw, key) {
|
|
12
|
+
const r = raw;
|
|
13
|
+
if (r && typeof r === 'object' && !(key in r) && r.data && typeof r.data === 'object') {
|
|
14
|
+
return r.data;
|
|
15
|
+
}
|
|
16
|
+
return raw;
|
|
24
17
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (url.pathname !== '/auth/callback') {
|
|
30
|
-
res.writeHead(404);
|
|
31
|
-
res.end();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const code = url.searchParams.get('code');
|
|
35
|
-
const state = url.searchParams.get('state');
|
|
36
|
-
if (!code || !state) {
|
|
37
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
38
|
-
res.end(HTML_ERROR);
|
|
39
|
-
server.close();
|
|
40
|
-
reject(new Error('OAuth callback missing code or state'));
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
44
|
-
res.end(HTML_SUCCESS);
|
|
45
|
-
server.close();
|
|
46
|
-
resolve({ code, state });
|
|
47
|
-
});
|
|
48
|
-
server.on('error', (err) => {
|
|
49
|
-
const code = err.code;
|
|
50
|
-
if (code === 'EADDRINUSE') {
|
|
51
|
-
reject(new Error(`Port ${CALLBACK_PORT} is already in use. ` +
|
|
52
|
-
'Make sure no other process is listening on that port and retry.'));
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
reject(err);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
server.listen(CALLBACK_PORT, '127.0.0.1');
|
|
59
|
-
const timer = setTimeout(() => {
|
|
60
|
-
server.close();
|
|
61
|
-
reject(new Error('Timed out waiting for OAuth callback (5 minutes). Please try again.'));
|
|
62
|
-
}, timeoutMs);
|
|
63
|
-
// Don't let the timer keep the process alive
|
|
64
|
-
timer.unref();
|
|
65
|
-
});
|
|
18
|
+
function planLabel(plan) {
|
|
19
|
+
if (!plan)
|
|
20
|
+
return '';
|
|
21
|
+
return plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
66
22
|
}
|
|
67
23
|
export default class AuthConnect extends BaseCommand {
|
|
68
|
-
static description = 'Connect
|
|
24
|
+
static description = 'Connect your ChatGPT account to Faces via device-code authorization (Subscription Connect plan)';
|
|
69
25
|
static args = {
|
|
70
26
|
provider: Args.string({
|
|
71
27
|
description: 'OAuth provider to connect',
|
|
@@ -75,165 +31,177 @@ export default class AuthConnect extends BaseCommand {
|
|
|
75
31
|
};
|
|
76
32
|
static flags = {
|
|
77
33
|
...BaseCommand.baseFlags,
|
|
78
|
-
manual: Flags.boolean({
|
|
79
|
-
description: 'Manual mode for headless environments: prints the authorize URL and ' +
|
|
80
|
-
'prompts you to paste the callback URL after approving in any browser.',
|
|
81
|
-
default: false,
|
|
82
|
-
}),
|
|
83
34
|
};
|
|
84
35
|
static examples = [
|
|
85
36
|
'<%= config.bin %> auth connect openai',
|
|
86
|
-
'<%= config.bin %> auth connect openai --
|
|
37
|
+
'<%= config.bin %> auth connect openai --json',
|
|
87
38
|
];
|
|
88
39
|
async run() {
|
|
89
|
-
const {
|
|
40
|
+
const { flags } = await this.parse(AuthConnect);
|
|
90
41
|
const client = this.makeClient(flags, true);
|
|
91
|
-
// 0. Short-circuit if already connected
|
|
42
|
+
// 0. Short-circuit if already connected.
|
|
92
43
|
try {
|
|
93
44
|
const existing = (await client.get('/v1/oauth', { requireJwt: true }));
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
45
|
+
const row = existing.find((r) => r.provider === 'openai');
|
|
46
|
+
if (row) {
|
|
47
|
+
const email = row.account_email ?? null;
|
|
48
|
+
const plan = row.chatgpt_plan_type ?? null;
|
|
49
|
+
const output = { provider: 'openai', connected: true, already_connected: true, account_email: email, chatgpt_plan_type: plan };
|
|
50
|
+
if (!this.jsonEnabled()) {
|
|
51
|
+
const who = email ? ` as ${email}` : '';
|
|
52
|
+
const tier = plan ? ` (${planLabel(plan)})` : '';
|
|
53
|
+
this.log(`Already connected to ChatGPT${who}${tier}. Run 'faces auth:disconnect openai' to reconnect.`);
|
|
54
|
+
}
|
|
98
55
|
return output;
|
|
99
56
|
}
|
|
100
57
|
}
|
|
101
|
-
catch {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
58
|
+
catch {
|
|
59
|
+
/* ignore — proceed with connect flow */
|
|
60
|
+
}
|
|
61
|
+
// 1. Start the device authorization (backend brokers OpenAI).
|
|
62
|
+
let start;
|
|
105
63
|
try {
|
|
106
|
-
|
|
107
|
-
requireJwt: true,
|
|
108
|
-
}));
|
|
64
|
+
start = await this.startDevice(client);
|
|
109
65
|
}
|
|
110
66
|
catch (err) {
|
|
111
|
-
|
|
112
|
-
if (err.statusCode === 403)
|
|
113
|
-
this.error('OAuth provider routing requires the connect plan.\n' +
|
|
114
|
-
'Upgrade with: faces billing checkout');
|
|
115
|
-
if (err.statusCode === 501)
|
|
116
|
-
this.error(`Provider '${args.provider}' is not yet available.`);
|
|
117
|
-
this.error(`Failed to get authorize URL (${err.statusCode}): ${err.message}`);
|
|
118
|
-
}
|
|
119
|
-
throw err;
|
|
67
|
+
this.handleStartError(err);
|
|
120
68
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
69
|
+
const verificationUri = start.verification_uri || VERIFICATION_FALLBACK;
|
|
70
|
+
// 2. Cancel the device session if the user aborts (Ctrl-C).
|
|
71
|
+
let done = false;
|
|
72
|
+
const onSigint = () => {
|
|
73
|
+
if (done)
|
|
74
|
+
return;
|
|
75
|
+
done = true;
|
|
76
|
+
emit('\nAborted. Cancelling device authorization…');
|
|
77
|
+
Promise.race([
|
|
78
|
+
client.post('/v1/oauth/openai/device/cancel', { requireJwt: true, body: { device_auth_id: start.device_auth_id } }).catch(() => { }),
|
|
79
|
+
sleep(2000),
|
|
80
|
+
]).finally(() => process.exit(130));
|
|
81
|
+
};
|
|
82
|
+
process.once('SIGINT', onSigint);
|
|
83
|
+
// 3. Show the code + URL and the one-time enable-setting guidance.
|
|
84
|
+
emit('');
|
|
85
|
+
emit('To connect ChatGPT:');
|
|
86
|
+
emit(` 1. Open this URL in any browser (phone, laptop — anywhere):`);
|
|
87
|
+
emit(` ${verificationUri}`);
|
|
88
|
+
emit(` 2. Enter this code: ${start.user_code}`);
|
|
89
|
+
emit(` 3. Sign in to ChatGPT and click Continue.`);
|
|
90
|
+
emit('');
|
|
91
|
+
emit('If this is your first time, you may need to enable device-code authorization in ChatGPT first:');
|
|
92
|
+
emit(` 1. Open ${SETTINGS_URL} (the settings panel can take a few seconds to appear — give it a moment)`);
|
|
93
|
+
emit(' 2. Turn on "Enable device code authorization for Codex"');
|
|
94
|
+
emit(' 3. Then enter the code above');
|
|
95
|
+
emit('');
|
|
96
|
+
emit('Waiting for approval (the code expires in 15 minutes; press Ctrl-C to cancel)…');
|
|
97
|
+
// 4. Poll until completed / expired / timed out.
|
|
98
|
+
const startedAt = Date.now();
|
|
99
|
+
const expiresAt = Date.parse(start.expires_at); // NaN if unparseable
|
|
100
|
+
let intervalMs = Math.max(1, start.interval || 5) * 1000;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (now - startedAt > HARD_STOP_MS || (!Number.isNaN(expiresAt) && now >= expiresAt)) {
|
|
104
|
+
process.removeListener('SIGINT', onSigint);
|
|
105
|
+
this.error('The code expired before it was approved. Run `faces auth:connect openai` again to get a new code.');
|
|
106
|
+
}
|
|
107
|
+
await sleep(intervalMs);
|
|
108
|
+
let poll;
|
|
109
|
+
try {
|
|
110
|
+
const raw = await client.post('/v1/oauth/openai/device/poll', {
|
|
111
|
+
requireJwt: true,
|
|
112
|
+
body: { device_auth_id: start.device_auth_id },
|
|
138
113
|
});
|
|
139
|
-
|
|
140
|
-
const result = await Promise.race([
|
|
141
|
-
pollForConnection(client, args.provider, 300_000),
|
|
142
|
-
pastePromise,
|
|
143
|
-
]);
|
|
144
|
-
// Clean up readline + stdin if polling won (keeps Node from hanging)
|
|
145
|
-
if (rl) {
|
|
146
|
-
rl.close();
|
|
147
|
-
rl = null;
|
|
114
|
+
poll = unwrap(raw, 'status');
|
|
148
115
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
116
|
+
catch (err) {
|
|
117
|
+
// Transient upstream errors: keep polling. Everything else is fatal.
|
|
118
|
+
if (err instanceof FacesAPIError && (err.statusCode === 502 || err.statusCode === 503)) {
|
|
119
|
+
process.stderr.write('.');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
process.removeListener('SIGINT', onSigint);
|
|
123
|
+
if (err instanceof FacesAPIError)
|
|
124
|
+
this.error(`Polling failed (${err.statusCode}): ${err.message}`);
|
|
125
|
+
throw err;
|
|
155
126
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
127
|
+
if (poll.status === 'completed') {
|
|
128
|
+
done = true;
|
|
129
|
+
process.removeListener('SIGINT', onSigint);
|
|
130
|
+
const email = poll.account_email ?? null;
|
|
131
|
+
const plan = poll.chatgpt_plan_type ?? null;
|
|
132
|
+
const output = { provider: 'openai', connected: true, account_email: email, chatgpt_plan_type: plan };
|
|
133
|
+
if (!this.jsonEnabled()) {
|
|
134
|
+
const who = email ? ` as ${email}` : '';
|
|
135
|
+
const tier = plan ? ` (${planLabel(plan)})` : '';
|
|
136
|
+
this.log(`\n✅ ChatGPT connected${who}${tier}.`);
|
|
137
|
+
if (!plan || plan === 'free') {
|
|
138
|
+
this.log('⚠️ Your ChatGPT plan may not include API access. Connecting requires a paid plan (Plus/Pro/Team, etc.).');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this.log('Requests to supported OpenAI models will now route through your ChatGPT subscription.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return output;
|
|
164
145
|
}
|
|
165
|
-
|
|
166
|
-
|
|
146
|
+
if (poll.status === 'expired') {
|
|
147
|
+
process.removeListener('SIGINT', onSigint);
|
|
148
|
+
this.error('The code expired or was not recognized. Run `faces auth:connect openai` again to get a new code.');
|
|
167
149
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.error('The pasted URL is missing code or state parameters. Did you copy the full URL?');
|
|
150
|
+
// pending — honor server-supplied retry_after, else fall back to interval.
|
|
151
|
+
intervalMs = (poll.retry_after && poll.retry_after > 0 ? poll.retry_after : (start.interval || 5)) * 1000;
|
|
152
|
+
process.stderr.write('.');
|
|
172
153
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
154
|
+
}
|
|
155
|
+
// Start the device flow, retrying transient upstream (502/503) errors.
|
|
156
|
+
async startDevice(client) {
|
|
157
|
+
let attempts502 = 0;
|
|
158
|
+
let attempts503 = 0;
|
|
159
|
+
for (;;) {
|
|
179
160
|
try {
|
|
180
|
-
;
|
|
181
|
-
(
|
|
161
|
+
const raw = await client.post('/v1/oauth/openai/device/start', { requireJwt: true });
|
|
162
|
+
return unwrap(raw, 'device_auth_id');
|
|
182
163
|
}
|
|
183
164
|
catch (err) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const inner = (raw.data ?? raw);
|
|
196
|
-
if (!inner.connected)
|
|
197
|
-
this.error('Exchange succeeded but the server did not confirm the connection.');
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
if (err instanceof FacesAPIError) {
|
|
201
|
-
// If state was already consumed (browser extension beat us to it),
|
|
202
|
-
// check whether the connection was stored anyway and succeed if so.
|
|
203
|
-
if (err.statusCode === 400) {
|
|
204
|
-
try {
|
|
205
|
-
const rows = (await client.get('/v1/oauth', { requireJwt: true }));
|
|
206
|
-
if (rows.some((r) => r.provider === args.provider)) {
|
|
207
|
-
const output = { provider: args.provider, connected: true };
|
|
208
|
-
if (!this.jsonEnabled())
|
|
209
|
-
this.log(`\n✅ ${args.provider} connected. Your connect plan requests will route through your subscription.`);
|
|
210
|
-
return output;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
catch { /* ignore */ }
|
|
165
|
+
if (err instanceof FacesAPIError && err.statusCode === 503 && attempts503 < 4) {
|
|
166
|
+
attempts503++;
|
|
167
|
+
emit('OpenAI is temporarily unavailable, retrying…');
|
|
168
|
+
await sleep(2000);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (err instanceof FacesAPIError && err.statusCode === 502 && attempts502 < 2) {
|
|
172
|
+
attempts502++;
|
|
173
|
+
emit('OpenAI returned an error, retrying…');
|
|
174
|
+
await sleep(2000);
|
|
175
|
+
continue;
|
|
214
176
|
}
|
|
215
|
-
|
|
177
|
+
throw err;
|
|
216
178
|
}
|
|
217
|
-
throw err;
|
|
218
179
|
}
|
|
219
|
-
const output = { provider: args.provider, connected: true };
|
|
220
|
-
if (!this.jsonEnabled())
|
|
221
|
-
this.log(`\n✅ ${args.provider} connected. Your connect plan requests will route through your subscription.`);
|
|
222
|
-
return output;
|
|
223
180
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
181
|
+
handleStartError(err) {
|
|
182
|
+
if (err instanceof FacesAPIError) {
|
|
183
|
+
if (err.statusCode === 401) {
|
|
184
|
+
this.error('Your Faces session is missing or expired. Run `faces auth:login` and try again.');
|
|
185
|
+
}
|
|
186
|
+
if (err.statusCode === 403 || err.errorCode === 'connect_plan_required') {
|
|
187
|
+
this.error('Connecting ChatGPT requires the Subscription Connect plan.\n' +
|
|
188
|
+
'Upgrade with: faces billing checkout');
|
|
189
|
+
}
|
|
190
|
+
if (err.statusCode === 422 || err.errorCode === 'device_auth_not_enabled') {
|
|
191
|
+
const d = err.details ?? {};
|
|
192
|
+
const url = d.settings_url || SETTINGS_URL;
|
|
193
|
+
const steps = Array.isArray(d.settings_steps) && d.settings_steps.length > 0
|
|
194
|
+
? d.settings_steps
|
|
195
|
+
: [
|
|
196
|
+
`Open ${url} (the settings panel can take a few seconds to appear — give it a moment)`,
|
|
197
|
+
'Turn on "Enable device code authorization for Codex"',
|
|
198
|
+
'Then run `faces auth:connect openai` again',
|
|
199
|
+
];
|
|
200
|
+
this.error('Device-code authorization is not enabled on your ChatGPT account yet:\n' +
|
|
201
|
+
steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n'));
|
|
202
|
+
}
|
|
203
|
+
this.error(`Failed to start device authorization (${err.statusCode}): ${err.message}`);
|
|
236
204
|
}
|
|
205
|
+
throw err;
|
|
237
206
|
}
|
|
238
|
-
throw new Error('Timed out waiting for connection (5 minutes).');
|
|
239
207
|
}
|
|
@@ -19,7 +19,19 @@ export default class AuthConnections extends BaseCommand {
|
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
21
|
for (const row of rows) {
|
|
22
|
-
|
|
22
|
+
if (row.provider === 'openai') {
|
|
23
|
+
const plan = row.chatgpt_plan_type;
|
|
24
|
+
const label = plan ? plan.charAt(0).toUpperCase() + plan.slice(1) : null;
|
|
25
|
+
const who = row.account_email ? ` as ${row.account_email}` : '';
|
|
26
|
+
const tier = label ? ` (${label})` : '';
|
|
27
|
+
this.log(`openai connected${who}${tier} connected_at=${row.connected_at}`);
|
|
28
|
+
if (!plan || plan === 'free') {
|
|
29
|
+
this.log(' ⚠️ This ChatGPT plan may not include API access — connecting requires a paid plan (Plus/Pro/Team, etc.).');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.log(`${row.provider} connected_at=${row.connected_at}${row.scope ? ` scope=${row.scope}` : ''}`);
|
|
34
|
+
}
|
|
23
35
|
}
|
|
24
36
|
}
|
|
25
37
|
}
|