@foxden-app/foxclaw 0.3.3 → 0.3.5
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/.env.example +7 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +16 -9
- package/dist/controller/controller.js +74 -4
- package/dist/main.js +15 -13
- package/docs/troubleshooting.md +25 -0
- package/docs/zh/troubleshooting.md +25 -0
- package/package.json +1 -1
- package/skills/npm-publish/SKILL.md +26 -14
package/.env.example
CHANGED
|
@@ -29,6 +29,13 @@ TELEGRAM_PREVIEW_THROTTLE_MS=800
|
|
|
29
29
|
THREAD_LIST_LIMIT=10
|
|
30
30
|
CODEX_CLI_BIN=/absolute/path/to/codex
|
|
31
31
|
|
|
32
|
+
# Optional: proxy for ChatGPT/Codex backend requests when FoxClaw runs as a service.
|
|
33
|
+
# Put these in the same env file that `foxclaw start` installs into systemd/launchd.
|
|
34
|
+
# HTTP_PROXY=http://127.0.0.1:7890
|
|
35
|
+
# HTTPS_PROXY=http://127.0.0.1:7890
|
|
36
|
+
# ALL_PROXY=socks5://127.0.0.1:7891
|
|
37
|
+
# NO_PROXY=127.0.0.1,localhost
|
|
38
|
+
|
|
32
39
|
# Weixin (iLink): run `foxclaw weixin-login` once, then enable:
|
|
33
40
|
# WX_ENABLED=true
|
|
34
41
|
# Comma-separated iLink user ids allowed to chat (see account JSON linkedIlinkUserId)
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const DEFAULT_LOCK_PATH: string;
|
|
|
8
8
|
export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
|
|
9
9
|
export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
|
|
10
10
|
export declare const DEFAULT_ENV_PATH: string;
|
|
11
|
+
export declare function resolveEnvPath(): string;
|
|
12
|
+
export declare function getLoadedEnvPath(): string | null;
|
|
11
13
|
export declare function loadEnv(): void;
|
|
12
14
|
export interface AppConfig {
|
|
13
15
|
tgBotToken: string;
|
package/dist/config.js
CHANGED
|
@@ -12,21 +12,28 @@ export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime'
|
|
|
12
12
|
export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
|
|
13
13
|
export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
|
|
14
14
|
let envLoaded = false;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return;
|
|
18
|
-
envLoaded = true;
|
|
15
|
+
let loadedEnvPath = null;
|
|
16
|
+
export function resolveEnvPath() {
|
|
19
17
|
const explicitPath = process.env.FOXCLAW_ENV?.trim();
|
|
20
18
|
if (explicitPath) {
|
|
21
|
-
|
|
22
|
-
return;
|
|
19
|
+
return path.resolve(explicitPath);
|
|
23
20
|
}
|
|
24
21
|
const cwdEnvPath = path.join(process.cwd(), '.env');
|
|
25
22
|
if (fs.existsSync(cwdEnvPath)) {
|
|
26
|
-
|
|
27
|
-
return;
|
|
23
|
+
return cwdEnvPath;
|
|
28
24
|
}
|
|
29
|
-
|
|
25
|
+
return DEFAULT_ENV_PATH;
|
|
26
|
+
}
|
|
27
|
+
export function getLoadedEnvPath() {
|
|
28
|
+
return loadedEnvPath;
|
|
29
|
+
}
|
|
30
|
+
export function loadEnv() {
|
|
31
|
+
if (envLoaded)
|
|
32
|
+
return;
|
|
33
|
+
envLoaded = true;
|
|
34
|
+
const envPath = resolveEnvPath();
|
|
35
|
+
loadedEnvPath = envPath;
|
|
36
|
+
dotenv.config({ path: envPath, override: Boolean(process.env.FOXCLAW_ENV?.trim()) });
|
|
30
37
|
}
|
|
31
38
|
export function loadConfig() {
|
|
32
39
|
loadEnv();
|
|
@@ -7065,6 +7065,9 @@ function cloneAuthRetryContext(context) {
|
|
|
7065
7065
|
};
|
|
7066
7066
|
}
|
|
7067
7067
|
function isCodexAuthRotationError(params) {
|
|
7068
|
+
if (isChatGptBackendAccessBlocked(collectCodexErrorText(params))) {
|
|
7069
|
+
return false;
|
|
7070
|
+
}
|
|
7068
7071
|
const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
|
|
7069
7072
|
if (code && /usageLimitExceeded|auth|unauthorized|forbidden|login/i.test(code)) {
|
|
7070
7073
|
return true;
|
|
@@ -7073,15 +7076,77 @@ function isCodexAuthRotationError(params) {
|
|
|
7073
7076
|
return /(usage limit|rate limit|not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message);
|
|
7074
7077
|
}
|
|
7075
7078
|
function formatCodexNotificationError(params) {
|
|
7079
|
+
const collected = collectCodexErrorText(params);
|
|
7080
|
+
const known = formatKnownCodexAccessError(collected);
|
|
7081
|
+
if (known) {
|
|
7082
|
+
return known;
|
|
7083
|
+
}
|
|
7076
7084
|
const message = stringOrNull(params?.error?.message);
|
|
7077
7085
|
if (message) {
|
|
7078
|
-
return message;
|
|
7086
|
+
return clipUserFacingError(cleanUserFacingError(message));
|
|
7079
7087
|
}
|
|
7080
7088
|
const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
|
|
7081
7089
|
if (code) {
|
|
7082
|
-
return code;
|
|
7090
|
+
return clipUserFacingError(cleanUserFacingError(code));
|
|
7091
|
+
}
|
|
7092
|
+
return clipUserFacingError(cleanUserFacingError(JSON.stringify(params?.error ?? params ?? {})));
|
|
7093
|
+
}
|
|
7094
|
+
function collectCodexErrorText(params) {
|
|
7095
|
+
const parts = [
|
|
7096
|
+
stringOrNull(params?.error?.message),
|
|
7097
|
+
stringOrNull(params?.error?.additionalDetails),
|
|
7098
|
+
stringOrNull(params?.additionalDetails),
|
|
7099
|
+
stringOrNull(params?.error?.codexErrorInfo),
|
|
7100
|
+
stringOrNull(params?.error?.code),
|
|
7101
|
+
].filter((part) => part !== null);
|
|
7102
|
+
if (parts.length > 0) {
|
|
7103
|
+
return parts.join(' ');
|
|
7104
|
+
}
|
|
7105
|
+
try {
|
|
7106
|
+
return JSON.stringify(params?.error ?? params ?? {});
|
|
7107
|
+
}
|
|
7108
|
+
catch {
|
|
7109
|
+
return String(params?.error ?? params ?? '');
|
|
7083
7110
|
}
|
|
7084
|
-
|
|
7111
|
+
}
|
|
7112
|
+
function formatKnownCodexAccessError(raw) {
|
|
7113
|
+
if (!isChatGptBackendAccessBlocked(raw)) {
|
|
7114
|
+
return null;
|
|
7115
|
+
}
|
|
7116
|
+
const ray = raw.match(/\bcf-ray:\s*([A-Za-z0-9-]+)/i)?.[1]
|
|
7117
|
+
?? raw.match(/\bRay ID:\s*([A-Za-z0-9-]+)/i)?.[1]
|
|
7118
|
+
?? null;
|
|
7119
|
+
return `ChatGPT backend 403 Forbidden: service network/proxy/IP is blocked, not necessarily auth.json invalid. Check HTTP_PROXY/HTTPS_PROXY in the FoxClaw env file and restart FoxClaw${ray ? ` (cf-ray: ${ray})` : ''}.`;
|
|
7120
|
+
}
|
|
7121
|
+
function isChatGptBackendAccessBlocked(raw) {
|
|
7122
|
+
const value = raw.toLowerCase();
|
|
7123
|
+
const targetsChatGptBackend = value.includes('chatgpt.com/backend-api')
|
|
7124
|
+
|| value.includes('wss://chatgpt.com/backend-api');
|
|
7125
|
+
const isForbidden = value.includes('403 forbidden')
|
|
7126
|
+
|| value.includes('status 403')
|
|
7127
|
+
|| value.includes('httpstatuscode":403')
|
|
7128
|
+
|| value.includes('httpstatuscode:403');
|
|
7129
|
+
const looksLikeHtmlBlock = value.includes('<html')
|
|
7130
|
+
|| value.includes('text/html')
|
|
7131
|
+
|| value.includes('cf-ray')
|
|
7132
|
+
|| value.includes('unable to load site')
|
|
7133
|
+
|| value.includes('if you are using a vpn');
|
|
7134
|
+
return targetsChatGptBackend && isForbidden && looksLikeHtmlBlock;
|
|
7135
|
+
}
|
|
7136
|
+
function cleanUserFacingError(raw) {
|
|
7137
|
+
return raw
|
|
7138
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
|
|
7139
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
|
|
7140
|
+
.replace(/<[^>]+>/g, ' ')
|
|
7141
|
+
.replace(/\s+/g, ' ')
|
|
7142
|
+
.trim();
|
|
7143
|
+
}
|
|
7144
|
+
function clipUserFacingError(raw, limit = 900) {
|
|
7145
|
+
const cleaned = raw.trim();
|
|
7146
|
+
if (cleaned.length <= limit) {
|
|
7147
|
+
return cleaned;
|
|
7148
|
+
}
|
|
7149
|
+
return `${cleaned.slice(0, limit - 3)}...`;
|
|
7085
7150
|
}
|
|
7086
7151
|
function parseUserInputQuestions(params) {
|
|
7087
7152
|
const rawQuestions = Array.isArray(params?.questions)
|
|
@@ -7484,7 +7549,12 @@ function formatLocalTimestamp(seconds) {
|
|
|
7484
7549
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
7485
7550
|
}
|
|
7486
7551
|
function formatShortStatusError(error) {
|
|
7487
|
-
const
|
|
7552
|
+
const raw = formatUserError(error);
|
|
7553
|
+
const known = formatKnownCodexAccessError(raw);
|
|
7554
|
+
if (known) {
|
|
7555
|
+
return known.length > 120 ? `${known.slice(0, 117)}...` : known;
|
|
7556
|
+
}
|
|
7557
|
+
const message = cleanUserFacingError(raw);
|
|
7488
7558
|
return message.length > 120 ? `${message.slice(0, 117)}...` : message;
|
|
7489
7559
|
}
|
|
7490
7560
|
function isThreadNotFoundError(error) {
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import process from 'node:process';
|
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, loadConfig, loadEnv, } from './config.js';
|
|
8
|
+
import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
|
|
9
9
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
10
10
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
11
11
|
const command = process.argv[2] || 'serve';
|
|
@@ -130,6 +130,9 @@ async function runServeCli() {
|
|
|
130
130
|
updatedAt: new Date().toISOString(),
|
|
131
131
|
channels: { telegram: false, weixin: false },
|
|
132
132
|
});
|
|
133
|
+
await app.stop({ terminateServer: true }).catch((error) => {
|
|
134
|
+
logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
|
|
135
|
+
});
|
|
133
136
|
store?.close();
|
|
134
137
|
processLock.release();
|
|
135
138
|
process.exit(0);
|
|
@@ -383,16 +386,14 @@ function installSystemd() {
|
|
|
383
386
|
const unitName = 'foxclaw.service';
|
|
384
387
|
const userSystemdDir = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user');
|
|
385
388
|
const unitPath = path.join(userSystemdDir, unitName);
|
|
386
|
-
const
|
|
389
|
+
const envPath = serviceEnvPath();
|
|
390
|
+
const configDir = path.dirname(envPath);
|
|
387
391
|
const nodeBin = process.execPath;
|
|
388
392
|
const nodeDir = path.dirname(nodeBin);
|
|
389
393
|
const pathValue = buildServicePath(nodeDir);
|
|
390
394
|
fs.mkdirSync(userSystemdDir, { recursive: true });
|
|
391
395
|
fs.mkdirSync(configDir, { recursive: true });
|
|
392
396
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
393
|
-
const foxclawEnvLine = process.env.FOXCLAW_ENV?.trim()
|
|
394
|
-
? `Environment=FOXCLAW_ENV=${systemdEscape(process.env.FOXCLAW_ENV.trim())}\n`
|
|
395
|
-
: '';
|
|
396
397
|
fs.writeFileSync(unitPath, `[Unit]
|
|
397
398
|
Description=FoxClaw local Codex execution bridge
|
|
398
399
|
Documentation=https://github.com/foxden-app/foxclaw
|
|
@@ -408,7 +409,8 @@ Environment=HOME=${systemdEscape(process.env.HOME || '')}
|
|
|
408
409
|
Environment=USER=${systemdEscape(process.env.USER || '')}
|
|
409
410
|
Environment=LOGNAME=${systemdEscape(process.env.LOGNAME || process.env.USER || '')}
|
|
410
411
|
Environment=PATH=${systemdEscape(pathValue)}
|
|
411
|
-
|
|
412
|
+
Environment=FOXCLAW_ENV=${systemdEscape(envPath)}
|
|
413
|
+
ExecStart=${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve
|
|
412
414
|
Restart=always
|
|
413
415
|
RestartSec=10
|
|
414
416
|
TimeoutStopSec=45
|
|
@@ -456,15 +458,11 @@ function installLaunchd() {
|
|
|
456
458
|
}
|
|
457
459
|
const home = process.env.HOME || '';
|
|
458
460
|
const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
|
|
459
|
-
const
|
|
461
|
+
const envPath = serviceEnvPath();
|
|
462
|
+
const configDir = path.dirname(envPath);
|
|
460
463
|
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
461
464
|
fs.mkdirSync(configDir, { recursive: true });
|
|
462
465
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
463
|
-
const foxclawEnvXml = process.env.FOXCLAW_ENV?.trim()
|
|
464
|
-
? ` <key>FOXCLAW_ENV</key>
|
|
465
|
-
<string>${xmlEscape(process.env.FOXCLAW_ENV.trim())}</string>
|
|
466
|
-
`
|
|
467
|
-
: '';
|
|
468
466
|
fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
|
|
469
467
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
470
468
|
<plist version="1.0">
|
|
@@ -489,7 +487,8 @@ function installLaunchd() {
|
|
|
489
487
|
<string>${xmlEscape(process.env.USER || '')}</string>
|
|
490
488
|
<key>LOGNAME</key>
|
|
491
489
|
<string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
|
|
492
|
-
|
|
490
|
+
<key>FOXCLAW_ENV</key>
|
|
491
|
+
<string>${xmlEscape(envPath)}</string>
|
|
493
492
|
</dict>
|
|
494
493
|
<key>RunAtLoad</key>
|
|
495
494
|
<true/>
|
|
@@ -532,6 +531,9 @@ function buildServicePath(nodeDir) {
|
|
|
532
531
|
];
|
|
533
532
|
return parts.filter((part, index) => part && parts.indexOf(part) === index).join(':');
|
|
534
533
|
}
|
|
534
|
+
function serviceEnvPath() {
|
|
535
|
+
return path.resolve(process.env.FOXCLAW_ENV?.trim() || getLoadedEnvPath() || DEFAULT_ENV_PATH);
|
|
536
|
+
}
|
|
535
537
|
function spawnChecked(commandName, args) {
|
|
536
538
|
const result = spawnSync(commandName, args, { stdio: 'inherit' });
|
|
537
539
|
if (result.status !== 0) {
|
package/docs/troubleshooting.md
CHANGED
|
@@ -182,6 +182,31 @@ Bridge logs are stored here:
|
|
|
182
182
|
tail -f ~/.foxclaw/logs/service.log
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
## ChatGPT Backend 403 Or Unable To Load Site
|
|
186
|
+
|
|
187
|
+
If Telegram shows `ChatGPT backend 403 Forbidden`, or the app-server log contains `Unable to load site`, `cf-ray`, or `chatgpt.com/backend-api`, the auth file is not necessarily broken. The service process is usually reaching ChatGPT with the wrong network/proxy/IP.
|
|
188
|
+
|
|
189
|
+
A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file. Check the env file installed into the service:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
systemctl --user cat foxclaw.service
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Make sure the file referenced by `Environment=FOXCLAW_ENV=...` contains your proxy variables, for example:
|
|
196
|
+
|
|
197
|
+
```dotenv
|
|
198
|
+
HTTP_PROXY=http://127.0.0.1:20171
|
|
199
|
+
HTTPS_PROXY=http://127.0.0.1:20171
|
|
200
|
+
ALL_PROXY=socks5://127.0.0.1:20170
|
|
201
|
+
NO_PROXY=127.0.0.1,localhost
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Restart FoxClaw after editing. The restart also restarts the managed Codex app-server so the new proxy environment takes effect:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
foxclaw restart
|
|
208
|
+
```
|
|
209
|
+
|
|
185
210
|
## Service Starts With The Wrong Node Version
|
|
186
211
|
|
|
187
212
|
The systemd installer captures the `node` binary from your current PATH. If you installed the service from a shell using Node 22 or older, reinstall it from a Node 24 shell:
|
|
@@ -183,6 +183,31 @@ Bridge 日志默认在:
|
|
|
183
183
|
tail -f ~/.foxclaw/logs/service.log
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
## ChatGPT 后端 403 或 Unable to load site
|
|
187
|
+
|
|
188
|
+
如果 Telegram 里看到 `ChatGPT backend 403 Forbidden`,或者 app-server 日志里出现 `Unable to load site`、`cf-ray`、`chatgpt.com/backend-api`,通常不是 `auth.json` 文件坏了,而是服务进程访问 ChatGPT 后端时没有走正确网络。
|
|
189
|
+
|
|
190
|
+
常见原因是:你在 shell 里配置了代理,或者项目 `.env` 里有代理,但 systemd/launchd 服务实际读的是另一个 env 文件。先看服务用的是哪个 env:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
systemctl --user cat foxclaw.service
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
确认 `Environment=FOXCLAW_ENV=...` 指向的文件里有你的代理配置,例如:
|
|
197
|
+
|
|
198
|
+
```dotenv
|
|
199
|
+
HTTP_PROXY=http://127.0.0.1:20171
|
|
200
|
+
HTTPS_PROXY=http://127.0.0.1:20171
|
|
201
|
+
ALL_PROXY=socks5://127.0.0.1:20170
|
|
202
|
+
NO_PROXY=127.0.0.1,localhost
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
改完后重启 FoxClaw。重启会同时重启托管的 Codex app-server,让新代理生效:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
foxclaw restart
|
|
209
|
+
```
|
|
210
|
+
|
|
186
211
|
## 服务用了错误的 Node 版本
|
|
187
212
|
|
|
188
213
|
systemd 安装脚本会记录当时 PATH 里的 `node`。如果你从 Node 22 或更旧版本的 shell 里安装过服务,请从 Node 24 的 shell 重新安装:
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ description: Publish npm packages safely, especially packages that require npm 2
|
|
|
5
5
|
|
|
6
6
|
# NPM Publish
|
|
7
7
|
|
|
8
|
-
Use this skill to publish an npm package from a repo
|
|
8
|
+
Use this skill to publish an npm package from a repo. Prefer the normal no-2FA publish path first. Fall back to npm web-auth only when npm explicitly prompts for it.
|
|
9
9
|
|
|
10
10
|
## Release Checklist
|
|
11
11
|
|
|
@@ -41,32 +41,44 @@ Use this skill to publish an npm package from a repo while preserving the exact
|
|
|
41
41
|
git push
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## Publish Flow
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
1. Start publish in a TTY and disable local browser opening:
|
|
46
|
+
1. Start publish in a TTY and disable local browser opening. This works both when npm publishes directly and when it asks for web auth:
|
|
49
47
|
```bash
|
|
50
48
|
BROWSER=true npm publish
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
2.
|
|
51
|
+
2. If npm publishes directly, it should finish with:
|
|
54
52
|
```text
|
|
55
|
-
|
|
56
|
-
https://www.npmjs.com/auth/cli/<auth-id>
|
|
57
|
-
Press ENTER to open in the browser...
|
|
53
|
+
+ <package-name>@<version>
|
|
58
54
|
```
|
|
55
|
+
Then verify:
|
|
56
|
+
```bash
|
|
57
|
+
npm view <package-name> version
|
|
58
|
+
git status --short --branch
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
3. If npm instead prints a web-auth prompt, use the fallback flow below.
|
|
62
|
+
|
|
63
|
+
## Web Auth Fallback
|
|
64
|
+
|
|
65
|
+
Use this only when npm prints:
|
|
66
|
+
```text
|
|
67
|
+
Authenticate your account at:
|
|
68
|
+
https://www.npmjs.com/auth/cli/<auth-id>
|
|
69
|
+
Press ENTER to open in the browser...
|
|
70
|
+
```
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
1. Send the URL to the user as a bare URL, not inside backticks or a code block. Bare URLs are easier to tap.
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
2. Do not press Enter immediately. Wait until the user says they clicked/confirmed the npm page.
|
|
63
75
|
|
|
64
|
-
|
|
76
|
+
3. After the user confirms, send Enter to the still-running TTY process. npm should retrieve the temporary token and finish:
|
|
65
77
|
```text
|
|
66
78
|
+ <package-name>@<version>
|
|
67
79
|
```
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
4. Verify the published version:
|
|
70
82
|
```bash
|
|
71
83
|
npm view <package-name> version
|
|
72
84
|
git status --short --branch
|
|
@@ -74,7 +86,7 @@ Use this exact flow when npm returns `EOTP` or when the account uses web auth fo
|
|
|
74
86
|
|
|
75
87
|
## Failure Modes
|
|
76
88
|
|
|
77
|
-
- If `npm publish` is run without a TTY, npm may print an `EOTP` error with the auth URL redacted as `***`. Stop that attempt and rerun with a TTY.
|
|
89
|
+
- If `npm publish` is run without a TTY, npm may print an `EOTP` error with the auth URL redacted as `***`. Stop that attempt and rerun with `BROWSER=true npm publish` in a TTY.
|
|
78
90
|
- If `xdg-open` fails because the environment has no browser, rerun with `BROWSER=true npm publish`.
|
|
79
91
|
- If the auth link expires or the publish process exits, rerun `BROWSER=true npm publish` to generate a new link.
|
|
80
92
|
- If the user provides a classic authenticator OTP instead of using the web link, publish can be retried with `npm publish --otp <code>`, but prefer web auth when the user asks for a clickable confirmation link.
|