@timo972/cc-router 0.13.0-rc.1 → 0.13.0-rc.2
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/CHANGELOG.md +2 -0
- package/dist/cli/account-flows.js +18 -17
- package/dist/cli/cmd-status.js +15 -4
- package/dist/cli/prompt-cancel.js +21 -0
- package/dist/telemetry/setup-diagnostics.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -38,6 +38,8 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
38
38
|
|
|
39
39
|
### Fixed
|
|
40
40
|
|
|
41
|
+
- Escape or Ctrl-C at any prompt of the dashboard's `n` / `l` flows returns
|
|
42
|
+
to the dashboard with a quiet "Cancelled." instead of a red failure line.
|
|
41
43
|
- Pressing `n` (and now `l`) in the dashboard no longer kills the process
|
|
42
44
|
before the sign-in prompt can take input. Ink unrefs stdin when it
|
|
43
45
|
unmounts and nothing ref'd it again, so with no other work pending the
|
|
@@ -59,7 +59,7 @@ export async function collectClaudeAccount(options) {
|
|
|
59
59
|
const method = options.method ?? await select({
|
|
60
60
|
message: "How do you want to add the account?",
|
|
61
61
|
choices: claudeMethodChoices(options.offer ?? "all"),
|
|
62
|
-
});
|
|
62
|
+
}, { signal: options.signal });
|
|
63
63
|
const attempt = createSetupAttempt({ provider: "anthropic", method: CLAUDE_METHOD_TELEMETRY[method] });
|
|
64
64
|
attempt.stageCompleted("credential_source_selection");
|
|
65
65
|
// Shared by reference: the file-extraction fallback swaps in a manual-token attempt.
|
|
@@ -106,12 +106,12 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
106
106
|
message: "Paste the token (sk-ant-oat01-...):",
|
|
107
107
|
mask: "•",
|
|
108
108
|
validate: v => v.startsWith("sk-ant-oat01-") || "Must start with sk-ant-oat01-",
|
|
109
|
-
});
|
|
109
|
+
}, { signal: options.signal });
|
|
110
110
|
}
|
|
111
|
-
const useDefaultExpiry = await confirm({ message: "Token valid for 1 year (default)?", default: true });
|
|
111
|
+
const useDefaultExpiry = await confirm({ message: "Token valid for 1 year (default)?", default: true }, { signal: options.signal });
|
|
112
112
|
const expiresAt = useDefaultExpiry
|
|
113
113
|
? Date.now() + LONG_LIVED_TOKEN_TTL_MS
|
|
114
|
-
: await promptExpiry();
|
|
114
|
+
: await promptExpiry(options.signal);
|
|
115
115
|
tokens = { accessToken, refreshToken: undefined, expiresAt, scopes: ["user:inference"] };
|
|
116
116
|
console.log(chalk.gray(" This token has no refresh token and the inference scope only: usage and identity metadata are unavailable for it."));
|
|
117
117
|
}
|
|
@@ -129,7 +129,7 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
129
129
|
console.log(chalk.yellow(" Could not find credentials in Keychain."));
|
|
130
130
|
console.log(chalk.gray(" Make sure Claude Code is logged in: run `claude login` first."));
|
|
131
131
|
printDiagnosticId(attempt.stageFailed(extraction.error, "credential_read"));
|
|
132
|
-
const retry = await confirm({ message: "Try another extraction method?", default: true });
|
|
132
|
+
const retry = await confirm({ message: "Try another extraction method?", default: true }, { signal: options.signal });
|
|
133
133
|
attempt.cancelled();
|
|
134
134
|
if (!retry)
|
|
135
135
|
return { account: null, attempt };
|
|
@@ -147,7 +147,7 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
147
147
|
else {
|
|
148
148
|
console.log(chalk.red(" ✗ ~/.claude/.credentials.json not found or unreadable."));
|
|
149
149
|
console.log(chalk.gray(" Make sure Claude Code is installed and you've run `claude login`."));
|
|
150
|
-
const retry = await confirm({ message: "Paste tokens manually instead?", default: true });
|
|
150
|
+
const retry = await confirm({ message: "Paste tokens manually instead?", default: true }, { signal: options.signal });
|
|
151
151
|
if (!retry) {
|
|
152
152
|
printDiagnosticId(attempt.stageFailed(extraction.error, "credential_read"));
|
|
153
153
|
attempt.cancelled();
|
|
@@ -158,11 +158,11 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
158
158
|
attempt = createSetupAttempt({ provider: "anthropic", method: "manual_token" });
|
|
159
159
|
current.attempt = attempt;
|
|
160
160
|
attempt.stageCompleted("credential_source_selection");
|
|
161
|
-
tokens = await promptManualTokens();
|
|
161
|
+
tokens = await promptManualTokens(options.signal);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
if (method === "manual") {
|
|
165
|
-
tokens = await promptManualTokens();
|
|
165
|
+
tokens = await promptManualTokens(options.signal);
|
|
166
166
|
}
|
|
167
167
|
if (!tokens) {
|
|
168
168
|
attempt.cancelled();
|
|
@@ -175,7 +175,7 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
175
175
|
message: "Account ID (press Enter to accept default):",
|
|
176
176
|
default: `max-account-${options.index}`,
|
|
177
177
|
validate: v => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
178
|
-
});
|
|
178
|
+
}, { signal: options.signal });
|
|
179
179
|
process.stdout.write(chalk.gray(" Validating tokens against Anthropic... "));
|
|
180
180
|
const validation = await validateToken(tokens.accessToken);
|
|
181
181
|
if (validation.valid) {
|
|
@@ -187,7 +187,7 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
187
187
|
console.log(chalk.yellow(` Reason: ${validation.reason}`));
|
|
188
188
|
printDiagnosticId(attempt.stageFailed(validation.diagnostic, "token_validation"));
|
|
189
189
|
console.log(chalk.gray(" The token will be saved but may not work until refreshed."));
|
|
190
|
-
const keepAnyway = await confirm({ message: "Save this account anyway?", default: false });
|
|
190
|
+
const keepAnyway = await confirm({ message: "Save this account anyway?", default: false }, { signal: options.signal });
|
|
191
191
|
if (!keepAnyway) {
|
|
192
192
|
attempt.cancelled();
|
|
193
193
|
return { account: null, attempt };
|
|
@@ -215,7 +215,7 @@ async function collectAnthropicAccount(options, method, current, reached) {
|
|
|
215
215
|
* The refresh token stays required here: a pasted long-lived `setup-token`
|
|
216
216
|
* credential has none, and it has its own method rather than this one.
|
|
217
217
|
*/
|
|
218
|
-
async function promptManualTokens() {
|
|
218
|
+
async function promptManualTokens(signal) {
|
|
219
219
|
console.log(chalk.gray("\n You can find your tokens by running:\n" +
|
|
220
220
|
" macOS: security find-generic-password -s 'Claude Code-credentials' -w\n" +
|
|
221
221
|
" Linux/Windows: cat ~/.claude/.credentials.json\n"));
|
|
@@ -225,21 +225,21 @@ async function promptManualTokens() {
|
|
|
225
225
|
validate: (v) => v.startsWith("sk-ant-oat01-") || v.startsWith("sk-ant-")
|
|
226
226
|
? true
|
|
227
227
|
: "Must start with sk-ant-oat01-",
|
|
228
|
-
});
|
|
228
|
+
}, { signal });
|
|
229
229
|
const refreshToken = await password({
|
|
230
230
|
message: "Paste refreshToken (sk-ant-ort01-...):",
|
|
231
231
|
mask: "•",
|
|
232
232
|
validate: (v) => v.startsWith("sk-ant-ort01-") || v.startsWith("sk-ant-")
|
|
233
233
|
? true
|
|
234
234
|
: "Must start with sk-ant-ort01-",
|
|
235
|
-
});
|
|
235
|
+
}, { signal });
|
|
236
236
|
const useDefaultExpiry = await confirm({
|
|
237
237
|
message: "Use default expiry (8 hours from now)?",
|
|
238
238
|
default: true,
|
|
239
|
-
});
|
|
239
|
+
}, { signal });
|
|
240
240
|
const expiresAt = useDefaultExpiry
|
|
241
241
|
? Date.now() + 8 * 60 * 60 * 1000
|
|
242
|
-
: await promptExpiry();
|
|
242
|
+
: await promptExpiry(signal);
|
|
243
243
|
return {
|
|
244
244
|
accessToken,
|
|
245
245
|
refreshToken,
|
|
@@ -383,15 +383,16 @@ export async function collectReauthRecord(target, options = {}) {
|
|
|
383
383
|
...(target.email ? { email: target.email } : {}),
|
|
384
384
|
offer: "login",
|
|
385
385
|
...(options.longLived ? { method: "setup_token" } : {}),
|
|
386
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
386
387
|
});
|
|
387
388
|
return account ? { record: credentialsOnly(accountToRecord(account)), attempt } : null;
|
|
388
389
|
}
|
|
389
390
|
/** An explicit expiry: ISO date or Unix milliseconds, validated as typed. */
|
|
390
|
-
async function promptExpiry() {
|
|
391
|
+
async function promptExpiry(signal) {
|
|
391
392
|
const raw = await input({
|
|
392
393
|
message: "Paste expiresAt (ISO date or ms timestamp):",
|
|
393
394
|
validate: v => parseExpiryInput(v) !== null || "Enter an ISO date (2027-01-01) or a Unix timestamp in milliseconds",
|
|
394
|
-
});
|
|
395
|
+
}, { signal });
|
|
395
396
|
return parseExpiryInput(raw);
|
|
396
397
|
}
|
|
397
398
|
/**
|
package/dist/cli/cmd-status.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { PROXY_PORT } from "../config/paths.js";
|
|
3
3
|
import { readConfig } from "../config/manager.js";
|
|
4
|
-
import { classifyHttpSetupFailure, failAttemptFromError, } from "../telemetry/setup-diagnostics.js";
|
|
4
|
+
import { classifyHttpSetupFailure, failAttemptFromError, isPromptCancellation, } from "../telemetry/setup-diagnostics.js";
|
|
5
|
+
import { withEscapeCancel } from "./prompt-cancel.js";
|
|
5
6
|
export function resolveStatusTarget(port) {
|
|
6
7
|
const cfg = readConfig();
|
|
7
8
|
if (cfg.client) {
|
|
@@ -169,11 +170,12 @@ async function runReauthFlow(target, intent) {
|
|
|
169
170
|
let attempt;
|
|
170
171
|
try {
|
|
171
172
|
const { collectReauthRecord } = await import("./account-flows.js");
|
|
172
|
-
|
|
173
|
+
console.log(chalk.gray(" Esc or Ctrl-C returns to the dashboard.\n"));
|
|
174
|
+
const result = await withEscapeCancel(signal => collectReauthRecord({
|
|
173
175
|
id: intent.id,
|
|
174
176
|
provider: intent.provider,
|
|
175
177
|
...(intent.email ? { email: intent.email } : {}),
|
|
176
|
-
});
|
|
178
|
+
}, { signal }));
|
|
177
179
|
if (!result)
|
|
178
180
|
return null;
|
|
179
181
|
attempt = result.attempt;
|
|
@@ -201,6 +203,10 @@ async function runReauthFlow(target, intent) {
|
|
|
201
203
|
return result.record.id;
|
|
202
204
|
}
|
|
203
205
|
catch (err) {
|
|
206
|
+
if (isPromptCancellation(err)) {
|
|
207
|
+
console.log(chalk.gray("\n Cancelled."));
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
204
210
|
console.error(chalk.red(`\n✗ Re-authentication failed: ${err.message}`));
|
|
205
211
|
if (attempt) {
|
|
206
212
|
const outcome = failAttemptFromError(attempt, err, "persistence");
|
|
@@ -219,8 +225,9 @@ async function runAddAccountFlow(target) {
|
|
|
219
225
|
let attempt;
|
|
220
226
|
try {
|
|
221
227
|
const { collectClaudeAccount, accountToRecord } = await import("./account-flows.js");
|
|
228
|
+
console.log(chalk.gray(" Esc or Ctrl-C returns to the dashboard.\n"));
|
|
222
229
|
// The index shown in the flow is just for display, pick something neutral.
|
|
223
|
-
const setup = await collectClaudeAccount({ index: 1 });
|
|
230
|
+
const setup = await withEscapeCancel(signal => collectClaudeAccount({ index: 1, signal }));
|
|
224
231
|
attempt = setup.attempt;
|
|
225
232
|
const account = setup.account;
|
|
226
233
|
if (!account)
|
|
@@ -250,6 +257,10 @@ async function runAddAccountFlow(target) {
|
|
|
250
257
|
return account.id;
|
|
251
258
|
}
|
|
252
259
|
catch (err) {
|
|
260
|
+
if (isPromptCancellation(err)) {
|
|
261
|
+
console.log(chalk.gray("\n Cancelled."));
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
253
264
|
console.error(chalk.red(`\n✗ Failed to add account: ${err.message}`));
|
|
254
265
|
if (attempt) {
|
|
255
266
|
const outcome = failAttemptFromError(attempt, err, "persistence");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
/** The Escape key itself — not an escape sequence and not an Alt-chord. */
|
|
3
|
+
export function isEscapeKey(key) {
|
|
4
|
+
return key?.name === "escape" && key.sequence === "\x1b";
|
|
5
|
+
}
|
|
6
|
+
export async function withEscapeCancel(flow, stdin = process.stdin) {
|
|
7
|
+
// Idempotent: the prompt's own readline interface installs the same decoder.
|
|
8
|
+
readline.emitKeypressEvents(stdin);
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const onKeypress = (_input, key) => {
|
|
11
|
+
if (isEscapeKey(key))
|
|
12
|
+
controller.abort();
|
|
13
|
+
};
|
|
14
|
+
stdin.on("keypress", onKeypress);
|
|
15
|
+
try {
|
|
16
|
+
return await flow(controller.signal);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
stdin.off("keypress", onKeypress);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -224,7 +224,10 @@ export async function withSetupTelemetryFlush(operation) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
export function isPromptCancellation(error) {
|
|
227
|
-
|
|
227
|
+
const name = ownStringProperty(error, "name");
|
|
228
|
+
// ExitPromptError: Ctrl-C or the process leaving. AbortPromptError: the
|
|
229
|
+
// prompt's AbortSignal fired — the dashboard's Escape key.
|
|
230
|
+
return name === "ExitPromptError" || name === "AbortPromptError";
|
|
228
231
|
}
|
|
229
232
|
/**
|
|
230
233
|
* Close an attempt that ended in a thrown error: a cancelled prompt is a user
|
package/package.json
CHANGED