@remixmate/cli 0.9.71 → 0.9.73
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/LICENSE +45 -21
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/argv.js +1 -51
- package/dist/auth/auth-lock.js +1 -100
- package/dist/auth/auto-login.js +7 -75
- package/dist/auth/commands.js +40 -290
- package/dist/auth/credential-store.js +1 -143
- package/dist/auth/device-flow-runner.js +13 -110
- package/dist/auth/device-flow.js +1 -132
- package/dist/auth/ensure.js +1 -62
- package/dist/auth/environment.js +1 -69
- package/dist/auth/pending-store.js +1 -45
- package/dist/auth/resolve.js +1 -44
- package/dist/billing.js +3 -76
- package/dist/capabilities.js +1 -75
- package/dist/cli.js +39 -211
- package/dist/diagnostics.js +2 -70
- package/dist/doctor.js +9 -188
- package/dist/errors.js +1 -33
- package/dist/exec.js +4 -47
- package/dist/handlers/gen-digital-human.js +33 -264
- package/dist/handlers/gen-image.js +9 -113
- package/dist/handlers/gen-post.js +15 -188
- package/dist/handlers/gen-video.js +7 -185
- package/dist/handlers/gen-voice.js +19 -133
- package/dist/handlers/index.js +1 -20
- package/dist/handlers/shared.js +1 -66
- package/dist/handlers/template-init.js +1 -21
- package/dist/http.js +3 -155
- package/dist/index.js +1 -8
- package/dist/install.js +40 -448
- package/dist/manifest.json +53 -13
- package/dist/progress.js +2 -15
- package/dist/project/commands.js +34 -156
- package/dist/project/host.js +1 -151
- package/dist/project/resolve.js +1 -65
- package/dist/project/store.js +2 -97
- package/dist/project/take.js +1 -106
- package/dist/project/trace.js +5 -278
- package/dist/registry.js +3 -70
- package/dist/runner.js +7 -225
- package/dist/skill-schema.js +1 -194
- package/dist/template/args.js +1 -69
- package/dist/template/check.js +2 -30
- package/dist/template/commands.js +34 -163
- package/dist/template/constants.js +1 -46
- package/dist/template/init.js +5 -133
- package/dist/template/list.js +1 -46
- package/dist/template/preview.js +7 -318
- package/dist/template/proc.js +1 -43
- package/dist/template/project.js +6 -161
- package/dist/template/publish.js +5 -185
- package/dist/template/render-client.js +2 -79
- package/dist/template/upgrade.js +4 -123
- package/dist/text.js +1 -15
- package/package.json +2 -35
- package/skills/doc-parse/scripts/extract_via_render.py +32 -32
- package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
- package/skills/gen-post/SKILL.md +23 -13
- package/skills/gen-post/skill.json +9 -10
- package/skills/gen-script/SKILL.md +23 -1
- package/skills/gen-script/scripts/gen_script.py +644 -551
- package/skills/gen-script/skill.json +12 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
- package/skills/render-video/scripts/_chrome_vendor.py +17 -17
- package/skills/render-video/scripts/_digital_human_segments.py +34 -34
- package/skills/render-video/scripts/_video_probe.py +33 -33
- package/skills/render-video/scripts/_vod_polling.py +13 -13
- package/skills/render-video/scripts/remote_renderer_client.py +42 -42
- package/skills/render-video/scripts/render_video.py +639 -639
- package/skills/render-video/scripts/required_props.py +37 -37
- package/skills/render-video/scripts/upload_video.py +35 -35
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +16 -0
- package/skills/template-registry/scripts/countdown_spec.py +92 -92
- package/skills/template-registry/scripts/doc_extract.py +40 -40
- package/skills/template-registry/scripts/list_templates.py +303 -191
- package/skills/template-registry/scripts/log_diagnostics.py +1 -1
- package/skills/template-registry/scripts/match_template.py +87 -87
- package/skills/template-registry/scripts/payload_hydrator.py +89 -89
- package/skills/template-registry/scripts/registry_loader.py +159 -159
- package/skills/template-registry/scripts/render_job_client.py +75 -75
- package/skills/template-registry/scripts/showcase_loader.py +44 -44
- package/skills/template-registry/skill.json +4 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
- package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
- package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
- package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
- package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
- package/skills/video-parser/scripts/analyze_video.py +173 -173
- package/skills/video-parser/scripts/deconstruct_video.py +91 -91
- package/skills/video-parser/scripts/parse_via_render.py +21 -21
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
- package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
- package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
- package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
- package/skills/web-screenshot/scripts/_media_upload.py +33 -33
- package/skills/web-screenshot/scripts/read_page.py +15 -15
- package/skills/web-screenshot/scripts/record.py +26 -26
- package/skills/web-screenshot/scripts/screenshot.py +5 -5
- package/dist/argv.d.ts +0 -15
- package/dist/auth/auth-lock.d.ts +0 -26
- package/dist/auth/auto-login.d.ts +0 -20
- package/dist/auth/commands.d.ts +0 -8
- package/dist/auth/credential-store.d.ts +0 -60
- package/dist/auth/device-flow-runner.d.ts +0 -58
- package/dist/auth/device-flow.d.ts +0 -52
- package/dist/auth/ensure.d.ts +0 -55
- package/dist/auth/environment.d.ts +0 -38
- package/dist/auth/pending-store.d.ts +0 -33
- package/dist/auth/resolve.d.ts +0 -30
- package/dist/billing.d.ts +0 -44
- package/dist/capabilities.d.ts +0 -63
- package/dist/cli.d.ts +0 -13
- package/dist/diagnostics.d.ts +0 -24
- package/dist/doctor.d.ts +0 -22
- package/dist/errors.d.ts +0 -29
- package/dist/exec.d.ts +0 -18
- package/dist/handlers/gen-digital-human.d.ts +0 -17
- package/dist/handlers/gen-image.d.ts +0 -20
- package/dist/handlers/gen-post.d.ts +0 -21
- package/dist/handlers/gen-video.d.ts +0 -19
- package/dist/handlers/gen-voice.d.ts +0 -26
- package/dist/handlers/index.d.ts +0 -20
- package/dist/handlers/shared.d.ts +0 -26
- package/dist/handlers/template-init.d.ts +0 -10
- package/dist/http.d.ts +0 -72
- package/dist/index.d.ts +0 -9
- package/dist/install.d.ts +0 -71
- package/dist/progress.d.ts +0 -23
- package/dist/project/commands.d.ts +0 -19
- package/dist/project/host.d.ts +0 -91
- package/dist/project/resolve.d.ts +0 -43
- package/dist/project/store.d.ts +0 -31
- package/dist/project/take.d.ts +0 -50
- package/dist/project/trace.d.ts +0 -98
- package/dist/registry.d.ts +0 -30
- package/dist/runner.d.ts +0 -39
- package/dist/skill-schema.d.ts +0 -138
- package/dist/template/args.d.ts +0 -25
- package/dist/template/check.d.ts +0 -23
- package/dist/template/commands.d.ts +0 -25
- package/dist/template/constants.d.ts +0 -39
- package/dist/template/init.d.ts +0 -28
- package/dist/template/list.d.ts +0 -23
- package/dist/template/preview.d.ts +0 -60
- package/dist/template/proc.d.ts +0 -26
- package/dist/template/project.d.ts +0 -47
- package/dist/template/publish.d.ts +0 -43
- package/dist/template/render-client.d.ts +0 -39
- package/dist/template/upgrade.d.ts +0 -44
- package/dist/text.d.ts +0 -8
package/dist/auth/commands.js
CHANGED
|
@@ -1,290 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Env token short-circuits the device flow (CI / agent-embedded hosts).
|
|
42
|
-
if ((process.env.PRIV_TOKEN ?? '').trim()) {
|
|
43
|
-
process.stdout.write('ℹ️ PRIV_TOKEN 环境变量已设置,CLI 将直接使用它,无需登录。\n');
|
|
44
|
-
return EXIT.OK;
|
|
45
|
-
}
|
|
46
|
-
// The device flow reads no keyboard input, so a TTY is not required — an
|
|
47
|
-
// agent host can run it and relay the link into chat. What IS required is a
|
|
48
|
-
// way to reach the user: canAutoAuth() rejects CI / headless, where a browser
|
|
49
|
-
// could never be opened and blocking would just hang the job.
|
|
50
|
-
if (!canAutoAuth()) {
|
|
51
|
-
process.stderr.write('❌ 当前环境无法完成浏览器授权(CI / 无桌面会话 / 已通过 REMIXMATE_NO_BROWSER_AUTH 关闭)。\n' +
|
|
52
|
-
' 请改用 PRIV_TOKEN 环境变量。\n');
|
|
53
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
54
|
-
}
|
|
55
|
-
// Already authenticated for this backend?
|
|
56
|
-
const existing = await getCredential(apiBaseUrl);
|
|
57
|
-
if (existing) {
|
|
58
|
-
process.stdout.write(`✅ 已登录(${existing.userLabel ?? 'unknown'})。如需切换账号请先运行 remixmate logout。\n`);
|
|
59
|
-
return EXIT.OK;
|
|
60
|
-
}
|
|
61
|
-
// A human at a terminal can wait out the device code's full lifetime; an
|
|
62
|
-
// agent tool call cannot (see AGENT_LOGIN_WAIT_MS). Progress goes to stdout.
|
|
63
|
-
const outcome = await runDeviceFlow(apiBaseUrl, {
|
|
64
|
-
write: (msg) => process.stdout.write(msg),
|
|
65
|
-
maxWaitMs: isAgentHost() ? AGENT_LOGIN_WAIT_MS : undefined,
|
|
66
|
-
});
|
|
67
|
-
switch (outcome.status) {
|
|
68
|
-
case 'approved':
|
|
69
|
-
process.stdout.write(`\n✅ 登录成功:${outcome.userLabel ?? ''}\n`);
|
|
70
|
-
process.stdout.write('🔒 安全提示:若你此前曾把 PrivToken 粘贴到聊天框或写进 shell 历史,建议在控制台轮换该 token。\n');
|
|
71
|
-
return EXIT.OK;
|
|
72
|
-
case 'denied':
|
|
73
|
-
process.stderr.write('❌ 授权被拒绝。\n');
|
|
74
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
75
|
-
case 'error':
|
|
76
|
-
process.stderr.write(`❌ 登录失败:${outcome.message}\n`);
|
|
77
|
-
return EXIT.ERROR;
|
|
78
|
-
case 'timeout':
|
|
79
|
-
// The wait window closed, not the device code. Authorization may still be
|
|
80
|
-
// pending in the browser — re-running picks up the approval immediately.
|
|
81
|
-
process.stderr.write('⏳ 已打开授权页但未在等待窗口内完成。在浏览器点击确认后,重跑 remixmate login(或直接重跑原命令)即可。\n');
|
|
82
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
83
|
-
case 'expired':
|
|
84
|
-
default:
|
|
85
|
-
process.stderr.write('❌ 设备码已过期或无效,请重新运行 remixmate login。\n');
|
|
86
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/** Default bound for one `login --wait` call, overridable with --timeout <seconds>. */
|
|
90
|
-
const DEFAULT_WAIT_SECONDS = 60;
|
|
91
|
-
/**
|
|
92
|
-
* `login --start` — request a device code, persist it, print the link, return.
|
|
93
|
-
*
|
|
94
|
-
* Deliberately does NOT poll: the caller (an agent, or a script) gets the link
|
|
95
|
-
* immediately and can surface it while the user takes as long as they need.
|
|
96
|
-
* Approval is collected later by `login --wait`.
|
|
97
|
-
*/
|
|
98
|
-
async function loginStart(apiBaseUrl, jsonOutput) {
|
|
99
|
-
const clientLabel = `remixmate-cli (${process.platform} ${process.arch})`;
|
|
100
|
-
let code;
|
|
101
|
-
try {
|
|
102
|
-
code = await requestDeviceCode(apiBaseUrl, clientLabel);
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
const msg = err instanceof DeviceFlowError ? err.message : err.message;
|
|
106
|
-
process.stderr.write(`❌ 无法获取设备码:${msg}\n`);
|
|
107
|
-
return EXIT.BACKEND_UNREACHABLE;
|
|
108
|
-
}
|
|
109
|
-
await setPending({
|
|
110
|
-
apiBaseUrl,
|
|
111
|
-
deviceCode: code.deviceCode,
|
|
112
|
-
userCode: code.userCode,
|
|
113
|
-
verificationUri: code.verificationUri,
|
|
114
|
-
verificationUriComplete: code.verificationUriComplete,
|
|
115
|
-
interval: code.interval,
|
|
116
|
-
expiresAt: Date.now() + code.expiresIn * 1000,
|
|
117
|
-
});
|
|
118
|
-
if (jsonOutput) {
|
|
119
|
-
process.stdout.write(JSON.stringify({
|
|
120
|
-
status: 'pending',
|
|
121
|
-
apiBaseUrl,
|
|
122
|
-
verificationUri: code.verificationUri,
|
|
123
|
-
verificationUriComplete: code.verificationUriComplete ?? code.verificationUri,
|
|
124
|
-
userCode: code.userCode,
|
|
125
|
-
expiresIn: code.expiresIn,
|
|
126
|
-
// The device_code itself is intentionally omitted — `login --wait` reads
|
|
127
|
-
// it from the pending file, so it never has to travel through a log or
|
|
128
|
-
// an agent's context.
|
|
129
|
-
}) + '\n');
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
writeAuthPrompt((m) => process.stdout.write(m), apiBaseUrl, code);
|
|
133
|
-
openBrowser(code.verificationUriComplete ?? code.verificationUri);
|
|
134
|
-
process.stdout.write('完成授权后运行 remixmate login --wait 收取凭证。\n');
|
|
135
|
-
}
|
|
136
|
-
return EXIT.OK;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* `login --wait [--timeout N]` — poll a previously started authorization for a
|
|
140
|
-
* bounded window. Safe to call repeatedly until it reports approved or expired.
|
|
141
|
-
*/
|
|
142
|
-
async function loginWait(apiBaseUrl, timeoutSeconds, jsonOutput) {
|
|
143
|
-
const emit = (payload, human, code) => {
|
|
144
|
-
if (jsonOutput)
|
|
145
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
146
|
-
else
|
|
147
|
-
process.stdout.write(human);
|
|
148
|
-
return code;
|
|
149
|
-
};
|
|
150
|
-
const pending = await getPending(apiBaseUrl);
|
|
151
|
-
if (!pending) {
|
|
152
|
-
if (jsonOutput)
|
|
153
|
-
process.stdout.write(JSON.stringify({ status: 'no_pending', apiBaseUrl }) + '\n');
|
|
154
|
-
else
|
|
155
|
-
process.stderr.write(`❌ 没有进行中的授权(或已过期 / 属于别的后端)。请先运行 remixmate login --start。\n`);
|
|
156
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
157
|
-
}
|
|
158
|
-
// Rebuild the shape pollForToken expects; expiresIn is the REMAINING lifetime
|
|
159
|
-
// so a resumed wait can never poll past the device code's real expiry.
|
|
160
|
-
const remainingSec = Math.max(1, Math.ceil((pending.expiresAt - Date.now()) / 1000));
|
|
161
|
-
let result;
|
|
162
|
-
try {
|
|
163
|
-
result = await pollForToken(apiBaseUrl, {
|
|
164
|
-
deviceCode: pending.deviceCode,
|
|
165
|
-
userCode: pending.userCode,
|
|
166
|
-
verificationUri: pending.verificationUri,
|
|
167
|
-
verificationUriComplete: pending.verificationUriComplete,
|
|
168
|
-
interval: pending.interval,
|
|
169
|
-
expiresIn: remainingSec,
|
|
170
|
-
}, { maxWaitMs: timeoutSeconds * 1000 });
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
const msg = err instanceof DeviceFlowError ? err.message : err.message;
|
|
174
|
-
if (jsonOutput)
|
|
175
|
-
process.stdout.write(JSON.stringify({ status: 'error', message: msg }) + '\n');
|
|
176
|
-
else
|
|
177
|
-
process.stderr.write(`❌ 轮询失败:${msg}\n`);
|
|
178
|
-
return EXIT.BACKEND_UNREACHABLE;
|
|
179
|
-
}
|
|
180
|
-
if (result.status === 'approved' && result.privToken) {
|
|
181
|
-
await persistApproval(apiBaseUrl, result.privToken, result.userLabel, (m) => process.stdout.write(m));
|
|
182
|
-
await clearPending();
|
|
183
|
-
return emit({ status: 'approved', userLabel: result.userLabel, apiBaseUrl }, `✅ 登录成功:${result.userLabel ?? ''}\n`, EXIT.OK);
|
|
184
|
-
}
|
|
185
|
-
if (result.status === 'access_denied') {
|
|
186
|
-
await clearPending();
|
|
187
|
-
return emit({ status: 'denied' }, '❌ 授权被拒绝。\n', EXIT.NOT_AUTHENTICATED);
|
|
188
|
-
}
|
|
189
|
-
if (result.timedOut) {
|
|
190
|
-
// The wait window closed, not the device code — the pending state stays put
|
|
191
|
-
// so the caller can simply poll again.
|
|
192
|
-
return emit({ status: 'pending', apiBaseUrl, userCode: pending.userCode }, '⏳ 尚未完成授权。在浏览器确认后再次运行 remixmate login --wait。\n', EXIT.NOT_AUTHENTICATED);
|
|
193
|
-
}
|
|
194
|
-
await clearPending();
|
|
195
|
-
return emit({ status: 'expired' }, '❌ 设备码已过期,请重新运行 remixmate login --start。\n', EXIT.NOT_AUTHENTICATED);
|
|
196
|
-
}
|
|
197
|
-
async function logout(apiBaseUrl) {
|
|
198
|
-
// Drop any half-finished authorization too, so a later `--wait` can't redeem
|
|
199
|
-
// a device code the user just logged out of.
|
|
200
|
-
await clearPending();
|
|
201
|
-
const res = await removeCredential(apiBaseUrl);
|
|
202
|
-
const localNote = 'ℹ️ logout 仅清除本地凭证,不会在服务端吊销 PrivToken;如需服务端失效请轮换 token。\n';
|
|
203
|
-
if (!res.removedFile && !res.removedKeychain && !res.fileError && !res.keychainError) {
|
|
204
|
-
process.stdout.write('ℹ️ 未找到已存储的凭证。\n');
|
|
205
|
-
return 0;
|
|
206
|
-
}
|
|
207
|
-
// Partial failure: keychain removed but file not cleared (Req 8.4), or vice versa.
|
|
208
|
-
if (res.fileError || res.keychainError) {
|
|
209
|
-
const which = res.fileError ? '本地文件' : '系统钥匙串';
|
|
210
|
-
process.stderr.write(`⚠️ 凭证未完全清除:${which}清除失败(${res.fileError ?? res.keychainError})。\n`);
|
|
211
|
-
process.stdout.write(localNote);
|
|
212
|
-
return 1;
|
|
213
|
-
}
|
|
214
|
-
process.stdout.write('✅ 已清除本地存储的凭证。\n');
|
|
215
|
-
process.stdout.write(localNote);
|
|
216
|
-
return 0;
|
|
217
|
-
}
|
|
218
|
-
/** Probe token validity against an existing authenticated endpoint (10s timeout). */
|
|
219
|
-
async function verifyToken(apiBaseUrl, token) {
|
|
220
|
-
const controller = new AbortController();
|
|
221
|
-
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
222
|
-
try {
|
|
223
|
-
const resp = await fetch(`${apiBaseUrl}/remotionTemplate/registry`, {
|
|
224
|
-
method: 'POST',
|
|
225
|
-
headers: { 'Content-Type': 'application/json', 'X-Priv-Token': token },
|
|
226
|
-
body: '{}',
|
|
227
|
-
signal: controller.signal,
|
|
228
|
-
});
|
|
229
|
-
const parsed = JSON.parse(await resp.text());
|
|
230
|
-
if (parsed.code === 0)
|
|
231
|
-
return 'valid';
|
|
232
|
-
if (parsed.code === 401)
|
|
233
|
-
return 'invalid';
|
|
234
|
-
return 'invalid';
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
return 'unreachable';
|
|
238
|
-
}
|
|
239
|
-
finally {
|
|
240
|
-
clearTimeout(timer);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
async function whoami(apiBaseUrl, flagToken) {
|
|
244
|
-
const resolved = await resolvePrivToken({ flag: flagToken, apiBaseUrl });
|
|
245
|
-
if (!resolved) {
|
|
246
|
-
process.stderr.write('❌ 未登录。运行 remixmate login,或设置 PRIV_TOKEN 环境变量。\n');
|
|
247
|
-
const hint = await describeOtherBackends(apiBaseUrl);
|
|
248
|
-
if (hint)
|
|
249
|
-
process.stderr.write(hint);
|
|
250
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
251
|
-
}
|
|
252
|
-
const verdict = await verifyToken(apiBaseUrl, resolved.privToken);
|
|
253
|
-
if (verdict === 'unreachable') {
|
|
254
|
-
process.stderr.write('❌ 无法验证身份:后端不可达或超时(本地凭证未改动)。\n');
|
|
255
|
-
return EXIT.BACKEND_UNREACHABLE;
|
|
256
|
-
}
|
|
257
|
-
if (verdict === 'invalid') {
|
|
258
|
-
process.stderr.write(`❌ 凭证无效或已被吊销。来源:${resolved.source}\n`);
|
|
259
|
-
return EXIT.NOT_AUTHENTICATED;
|
|
260
|
-
}
|
|
261
|
-
const label = resolved.userLabel ?? '(已认证)';
|
|
262
|
-
process.stdout.write(`已认证:${label}\n来源:${resolved.source}\n后端:${apiBaseUrl}\n`);
|
|
263
|
-
return EXIT.OK;
|
|
264
|
-
}
|
|
265
|
-
/** Dispatch an auth verb. Returns a process exit code. */
|
|
266
|
-
export async function runAuthCommand(verb, rest) {
|
|
267
|
-
const args = parseArgv(rest);
|
|
268
|
-
const apiBaseUrl = resolveApiBaseUrl(args.api_base_url);
|
|
269
|
-
const flagToken = args.token;
|
|
270
|
-
switch (verb) {
|
|
271
|
-
case 'login': {
|
|
272
|
-
const jsonOutput = args.json_output === true || args.json === true;
|
|
273
|
-
if (args.start === true)
|
|
274
|
-
return loginStart(apiBaseUrl, jsonOutput);
|
|
275
|
-
if (args.wait === true || typeof args.wait === 'string') {
|
|
276
|
-
const raw = Number(args.timeout);
|
|
277
|
-
const timeout = Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_WAIT_SECONDS;
|
|
278
|
-
return loginWait(apiBaseUrl, timeout, jsonOutput);
|
|
279
|
-
}
|
|
280
|
-
return login(apiBaseUrl);
|
|
281
|
-
}
|
|
282
|
-
case 'logout':
|
|
283
|
-
return logout(apiBaseUrl);
|
|
284
|
-
case 'whoami':
|
|
285
|
-
return whoami(apiBaseUrl, flagToken);
|
|
286
|
-
default:
|
|
287
|
-
process.stderr.write(`❌ unknown auth command: ${verb}\n`);
|
|
288
|
-
return EXIT.USAGE;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
1
|
+
var f=Object.defineProperty;var c=(t,i)=>f(t,"name",{value:i,configurable:!0});import{parseArgv as w}from"../argv.js";import{resolveApiBaseUrl as T}from"../http.js";import{EXIT as n}from"../errors.js";import{getCredential as E,listCredentials as v,removeCredential as g}from"./credential-store.js";import{resolvePrivToken as A}from"./resolve.js";import{persistApproval as C,runDeviceFlow as N,writeAuthPrompt as _}from"./device-flow-runner.js";import{openBrowser as O,pollForToken as h,requestDeviceCode as I,DeviceFlowError as p}from"./device-flow.js";import{clearPending as l,getPending as y,setPending as x}from"./pending-store.js";import{canAutoAuth as U,isAgentHost as $}from"./environment.js";const b=9e4;async function D(t){const i=(await v()).filter(e=>e.apiBaseUrl!==t);if(i.length===0)return"";const r=i.map(e=>` - ${e.apiBaseUrl}${e.userLabel?`(${e.userLabel})`:""}`);return`ℹ️ 当前后端:${t}(无凭证)
|
|
2
|
+
已存储凭证的后端:
|
|
3
|
+
${r.join(`
|
|
4
|
+
`)}
|
|
5
|
+
要使用其中之一,设置 MM_API_BASE_URL=<该地址>,或加 --api-base-url <该地址>。
|
|
6
|
+
`}c(D,"describeOtherBackends");async function k(t){if((process.env.PRIV_TOKEN??"").trim())return process.stdout.write(`ℹ️ PRIV_TOKEN 环境变量已设置,CLI 将直接使用它,无需登录。
|
|
7
|
+
`),n.OK;if(!U())return process.stderr.write(`❌ 当前环境无法完成浏览器授权(CI / 无桌面会话 / 已通过 REMIXMATE_NO_BROWSER_AUTH 关闭)。
|
|
8
|
+
请改用 PRIV_TOKEN 环境变量。
|
|
9
|
+
`),n.NOT_AUTHENTICATED;const i=await E(t);if(i)return process.stdout.write(`✅ 已登录(${i.userLabel??"unknown"})。如需切换账号请先运行 remixmate logout。
|
|
10
|
+
`),n.OK;const r=await N(t,{write:c(e=>process.stdout.write(e),"write"),maxWaitMs:$()?b:void 0});switch(r.status){case"approved":return process.stdout.write(`
|
|
11
|
+
✅ 登录成功:${r.userLabel??""}
|
|
12
|
+
`),process.stdout.write(`🔒 安全提示:若你此前曾把 PrivToken 粘贴到聊天框或写进 shell 历史,建议在控制台轮换该 token。
|
|
13
|
+
`),n.OK;case"denied":return process.stderr.write(`❌ 授权被拒绝。
|
|
14
|
+
`),n.NOT_AUTHENTICATED;case"error":return process.stderr.write(`❌ 登录失败:${r.message}
|
|
15
|
+
`),n.ERROR;case"timeout":return process.stderr.write(`⏳ 已打开授权页但未在等待窗口内完成。在浏览器点击确认后,重跑 remixmate login(或直接重跑原命令)即可。
|
|
16
|
+
`),n.NOT_AUTHENTICATED;default:return process.stderr.write(`❌ 设备码已过期或无效,请重新运行 remixmate login。
|
|
17
|
+
`),n.NOT_AUTHENTICATED}}c(k,"login");const L=60;async function H(t,i){const r=`remixmate-cli (${process.platform} ${process.arch})`;let e;try{e=await I(t,r)}catch(s){const a=(s instanceof p,s.message);return process.stderr.write(`❌ 无法获取设备码:${a}
|
|
18
|
+
`),n.BACKEND_UNREACHABLE}return await x({apiBaseUrl:t,deviceCode:e.deviceCode,userCode:e.userCode,verificationUri:e.verificationUri,verificationUriComplete:e.verificationUriComplete,interval:e.interval,expiresAt:Date.now()+e.expiresIn*1e3}),i?process.stdout.write(JSON.stringify({status:"pending",apiBaseUrl:t,verificationUri:e.verificationUri,verificationUriComplete:e.verificationUriComplete??e.verificationUri,userCode:e.userCode,expiresIn:e.expiresIn})+`
|
|
19
|
+
`):(_(s=>process.stdout.write(s),t,e),O(e.verificationUriComplete??e.verificationUri),process.stdout.write(`完成授权后运行 remixmate login --wait 收取凭证。
|
|
20
|
+
`)),n.OK}c(H,"loginStart");async function K(t,i,r){const e=c((u,d,m)=>(r?process.stdout.write(JSON.stringify(u)+`
|
|
21
|
+
`):process.stdout.write(d),m),"emit"),s=await y(t);if(!s)return r?process.stdout.write(JSON.stringify({status:"no_pending",apiBaseUrl:t})+`
|
|
22
|
+
`):process.stderr.write(`❌ 没有进行中的授权(或已过期 / 属于别的后端)。请先运行 remixmate login --start。
|
|
23
|
+
`),n.NOT_AUTHENTICATED;const a=Math.max(1,Math.ceil((s.expiresAt-Date.now())/1e3));let o;try{o=await h(t,{deviceCode:s.deviceCode,userCode:s.userCode,verificationUri:s.verificationUri,verificationUriComplete:s.verificationUriComplete,interval:s.interval,expiresIn:a},{maxWaitMs:i*1e3})}catch(u){const d=(u instanceof p,u.message);return r?process.stdout.write(JSON.stringify({status:"error",message:d})+`
|
|
24
|
+
`):process.stderr.write(`❌ 轮询失败:${d}
|
|
25
|
+
`),n.BACKEND_UNREACHABLE}return o.status==="approved"&&o.privToken?(await C(t,o.privToken,o.userLabel,u=>process.stdout.write(u)),await l(),e({status:"approved",userLabel:o.userLabel,apiBaseUrl:t},`✅ 登录成功:${o.userLabel??""}
|
|
26
|
+
`,n.OK)):o.status==="access_denied"?(await l(),e({status:"denied"},`❌ 授权被拒绝。
|
|
27
|
+
`,n.NOT_AUTHENTICATED)):o.timedOut?e({status:"pending",apiBaseUrl:t,userCode:s.userCode},`⏳ 尚未完成授权。在浏览器确认后再次运行 remixmate login --wait。
|
|
28
|
+
`,n.NOT_AUTHENTICATED):(await l(),e({status:"expired"},`❌ 设备码已过期,请重新运行 remixmate login --start。
|
|
29
|
+
`,n.NOT_AUTHENTICATED))}c(K,"loginWait");async function P(t){await l();const i=await g(t),r=`ℹ️ logout 仅清除本地凭证,不会在服务端吊销 PrivToken;如需服务端失效请轮换 token。
|
|
30
|
+
`;if(!i.removedFile&&!i.removedKeychain&&!i.fileError&&!i.keychainError)return process.stdout.write(`ℹ️ 未找到已存储的凭证。
|
|
31
|
+
`),0;if(i.fileError||i.keychainError){const e=i.fileError?"本地文件":"系统钥匙串";return process.stderr.write(`⚠️ 凭证未完全清除:${e}清除失败(${i.fileError??i.keychainError})。
|
|
32
|
+
`),process.stdout.write(r),1}return process.stdout.write(`✅ 已清除本地存储的凭证。
|
|
33
|
+
`),process.stdout.write(r),0}c(P,"logout");async function R(t,i){const r=new AbortController,e=setTimeout(()=>r.abort(),1e4);try{const s=await fetch(`${t}/remotionTemplate/registry`,{method:"POST",headers:{"Content-Type":"application/json","X-Priv-Token":i},body:"{}",signal:r.signal}),a=JSON.parse(await s.text());return a.code===0?"valid":(a.code===401,"invalid")}catch{return"unreachable"}finally{clearTimeout(e)}}c(R,"verifyToken");async function S(t,i){const r=await A({flag:i,apiBaseUrl:t});if(!r){process.stderr.write(`❌ 未登录。运行 remixmate login,或设置 PRIV_TOKEN 环境变量。
|
|
34
|
+
`);const a=await D(t);return a&&process.stderr.write(a),n.NOT_AUTHENTICATED}const e=await R(t,r.privToken);if(e==="unreachable")return process.stderr.write(`❌ 无法验证身份:后端不可达或超时(本地凭证未改动)。
|
|
35
|
+
`),n.BACKEND_UNREACHABLE;if(e==="invalid")return process.stderr.write(`❌ 凭证无效或已被吊销。来源:${r.source}
|
|
36
|
+
`),n.NOT_AUTHENTICATED;const s=r.userLabel??"(已认证)";return process.stdout.write(`已认证:${s}
|
|
37
|
+
来源:${r.source}
|
|
38
|
+
后端:${t}
|
|
39
|
+
`),n.OK}c(S,"whoami");async function z(t,i){const r=w(i),e=T(r.api_base_url),s=r.token;switch(t){case"login":{const a=r.json_output===!0||r.json===!0;if(r.start===!0)return H(e,a);if(r.wait===!0||typeof r.wait=="string"){const o=Number(r.timeout),u=Number.isFinite(o)&&o>0?o:L;return K(e,u,a)}return k(e)}case"logout":return P(e);case"whoami":return S(e,s);default:return process.stderr.write(`❌ unknown auth command: ${t}
|
|
40
|
+
`),n.USAGE}}c(z,"runAuthCommand");export{z as runAuthCommand};
|
|
@@ -1,143 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Local credential store for the remixmate CLI.
|
|
3
|
-
*
|
|
4
|
-
* Design (see .kiro/specs/cli-device-login/design.md):
|
|
5
|
-
* - File store at ~/.config/remixmate/credentials.json (mode 0600) is the
|
|
6
|
-
* REQUIRED, always-present implementation. Entries are keyed by API base
|
|
7
|
-
* URL so multiple accounts / backends coexist without overwriting.
|
|
8
|
-
* - The OS keychain is an OPTIONAL enhancement loaded via dynamic import().
|
|
9
|
-
* When a keychain module is available we keep the secret PrivToken there
|
|
10
|
-
* and store only non-secret metadata (+ a location marker) in the file.
|
|
11
|
-
* When it is absent or fails at runtime, the PrivToken is stored in the
|
|
12
|
-
* file instead. Either way the PrivToken value is never logged.
|
|
13
|
-
*
|
|
14
|
-
* No new runtime dependency is added: keychain support activates only if a
|
|
15
|
-
* compatible module (`keytar`) is already importable in the host environment.
|
|
16
|
-
*/
|
|
17
|
-
import { homedir } from 'node:os';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
import { promises as fs } from 'node:fs';
|
|
20
|
-
export const CRED_DIR = join(homedir(), '.config', 'remixmate');
|
|
21
|
-
export const CRED_FILE = join(CRED_DIR, 'credentials.json');
|
|
22
|
-
const KEYCHAIN_SERVICE = 'remixmate-cli';
|
|
23
|
-
let keychainProbe;
|
|
24
|
-
/** Lazily attempt to load an optional keychain module; cache the (null) result. */
|
|
25
|
-
async function loadKeychain() {
|
|
26
|
-
if (keychainProbe === undefined) {
|
|
27
|
-
keychainProbe = import('keytar')
|
|
28
|
-
.then((m) => (m.default ?? m))
|
|
29
|
-
.catch(() => null);
|
|
30
|
-
}
|
|
31
|
-
return keychainProbe;
|
|
32
|
-
}
|
|
33
|
-
async function readFileStore() {
|
|
34
|
-
try {
|
|
35
|
-
const raw = await fs.readFile(CRED_FILE, 'utf8');
|
|
36
|
-
const parsed = JSON.parse(raw);
|
|
37
|
-
if (parsed && parsed.version === 1 && parsed.credentials) {
|
|
38
|
-
return { version: 1, credentials: parsed.credentials };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// Missing or unreadable/corrupt file → treat as empty store.
|
|
43
|
-
}
|
|
44
|
-
return { version: 1, credentials: {} };
|
|
45
|
-
}
|
|
46
|
-
async function writeFileStore(store) {
|
|
47
|
-
await fs.mkdir(CRED_DIR, { recursive: true, mode: 0o700 });
|
|
48
|
-
// Write with 0600 from the start, then chmod defensively in case the file
|
|
49
|
-
// pre-existed with looser permissions.
|
|
50
|
-
await fs.writeFile(CRED_FILE, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
51
|
-
await fs.chmod(CRED_FILE, 0o600);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Persist a credential for `apiBaseUrl`. Prefers the keychain for the secret;
|
|
55
|
-
* falls back to the file on absence or runtime failure. Returns whether the
|
|
56
|
-
* keychain was used so callers can surface "keychain unavailable, used file".
|
|
57
|
-
*/
|
|
58
|
-
export async function setCredential(apiBaseUrl, cred) {
|
|
59
|
-
const store = await readFileStore();
|
|
60
|
-
const entry = {
|
|
61
|
-
userLabel: cred.userLabel,
|
|
62
|
-
createdAt: cred.createdAt,
|
|
63
|
-
secret: 'file',
|
|
64
|
-
privToken: cred.privToken,
|
|
65
|
-
};
|
|
66
|
-
const keychain = await loadKeychain();
|
|
67
|
-
let usedKeychain = false;
|
|
68
|
-
let keychainError;
|
|
69
|
-
if (keychain) {
|
|
70
|
-
try {
|
|
71
|
-
await keychain.setPassword(KEYCHAIN_SERVICE, apiBaseUrl, cred.privToken);
|
|
72
|
-
usedKeychain = true;
|
|
73
|
-
entry.secret = 'keychain';
|
|
74
|
-
delete entry.privToken; // keep the secret out of the file
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
keychainError = err.message;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
store.credentials[apiBaseUrl] = entry;
|
|
81
|
-
await writeFileStore(store);
|
|
82
|
-
return { usedKeychain, keychainError };
|
|
83
|
-
}
|
|
84
|
-
/** Read a credential for `apiBaseUrl`, or null when none is stored. */
|
|
85
|
-
export async function getCredential(apiBaseUrl) {
|
|
86
|
-
const store = await readFileStore();
|
|
87
|
-
const entry = store.credentials[apiBaseUrl];
|
|
88
|
-
if (!entry)
|
|
89
|
-
return null;
|
|
90
|
-
let privToken = entry.privToken;
|
|
91
|
-
if (entry.secret === 'keychain') {
|
|
92
|
-
const keychain = await loadKeychain();
|
|
93
|
-
const secret = keychain ? await keychain.getPassword(KEYCHAIN_SERVICE, apiBaseUrl) : null;
|
|
94
|
-
if (!secret)
|
|
95
|
-
return null; // secret expected in keychain but unavailable
|
|
96
|
-
privToken = secret;
|
|
97
|
-
}
|
|
98
|
-
if (!privToken)
|
|
99
|
-
return null;
|
|
100
|
-
return { privToken, userLabel: entry.userLabel, createdAt: entry.createdAt };
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* List which backends have a stored credential, WITHOUT reading any secret.
|
|
104
|
-
*
|
|
105
|
-
* Credentials are keyed by API base URL, so "logged in" is per-backend. Without
|
|
106
|
-
* this, a credential stored for a local dev backend while the CLI defaults to
|
|
107
|
-
* production produced a bare "not logged in" — technically true, actively
|
|
108
|
-
* misleading, and the exact confusion that motivated this work.
|
|
109
|
-
*/
|
|
110
|
-
export async function listCredentials() {
|
|
111
|
-
const store = await readFileStore();
|
|
112
|
-
return Object.entries(store.credentials).map(([apiBaseUrl, entry]) => ({
|
|
113
|
-
apiBaseUrl,
|
|
114
|
-
userLabel: entry.userLabel,
|
|
115
|
-
createdAt: entry.createdAt,
|
|
116
|
-
secret: entry.secret,
|
|
117
|
-
}));
|
|
118
|
-
}
|
|
119
|
-
/** Remove a credential from both stores; reports per-store outcome. */
|
|
120
|
-
export async function removeCredential(apiBaseUrl) {
|
|
121
|
-
const result = { removedFile: false, removedKeychain: false };
|
|
122
|
-
const keychain = await loadKeychain();
|
|
123
|
-
if (keychain) {
|
|
124
|
-
try {
|
|
125
|
-
result.removedKeychain = await keychain.deletePassword(KEYCHAIN_SERVICE, apiBaseUrl);
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
result.keychainError = err.message;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const store = await readFileStore();
|
|
132
|
-
if (store.credentials[apiBaseUrl]) {
|
|
133
|
-
delete store.credentials[apiBaseUrl];
|
|
134
|
-
try {
|
|
135
|
-
await writeFileStore(store);
|
|
136
|
-
result.removedFile = true;
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
result.fileError = err.message;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
1
|
+
var k=Object.defineProperty;var i=(e,t)=>k(e,"name",{value:t,configurable:!0});import{homedir as v}from"node:os";import{join as y}from"node:path";import{promises as c}from"node:fs";const w=y(v(),".config","remixmate"),l=y(w,"credentials.json"),d="remixmate-cli";let u;async function f(){return u===void 0&&(u=import("keytar").then(e=>e.default??e).catch(()=>null)),u}i(f,"loadKeychain");async function o(){try{const e=await c.readFile(l,"utf8"),t=JSON.parse(e);if(t&&t.version===1&&t.credentials)return{version:1,credentials:t.credentials}}catch{}return{version:1,credentials:{}}}i(o,"readFileStore");async function h(e){await c.mkdir(w,{recursive:!0,mode:448}),await c.writeFile(l,JSON.stringify(e,null,2),{mode:384}),await c.chmod(l,384)}i(h,"writeFileStore");async function g(e,t){const r=await o(),n={userLabel:t.userLabel,createdAt:t.createdAt,secret:"file",privToken:t.privToken},a=await f();let s=!1,m;if(a)try{await a.setPassword(d,e,t.privToken),s=!0,n.secret="keychain",delete n.privToken}catch(p){m=p.message}return r.credentials[e]=n,await h(r),{usedKeychain:s,keychainError:m}}i(g,"setCredential");async function A(e){const r=(await o()).credentials[e];if(!r)return null;let n=r.privToken;if(r.secret==="keychain"){const a=await f(),s=a?await a.getPassword(d,e):null;if(!s)return null;n=s}return n?{privToken:n,userLabel:r.userLabel,createdAt:r.createdAt}:null}i(A,"getCredential");async function F(){const e=await o();return Object.entries(e.credentials).map(([t,r])=>({apiBaseUrl:t,userLabel:r.userLabel,createdAt:r.createdAt,secret:r.secret}))}i(F,"listCredentials");async function L(e){const t={removedFile:!1,removedKeychain:!1},r=await f();if(r)try{t.removedKeychain=await r.deletePassword(d,e)}catch(a){t.keychainError=a.message}const n=await o();if(n.credentials[e]){delete n.credentials[e];try{await h(n),t.removedFile=!0}catch(a){t.fileError=a.message}}return t}i(L,"removeCredential");export{w as CRED_DIR,l as CRED_FILE,A as getCredential,F as listCredentials,L as removeCredential,g as setCredential};
|
|
@@ -1,110 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { isAgentHost } from './environment.js';
|
|
15
|
-
/**
|
|
16
|
-
* Machine-readable authorization event, emitted alongside the human text when
|
|
17
|
-
* an agent host is driving the CLI. Delimited by markers (rather than a bare
|
|
18
|
-
* JSON line) because a skill's own stdout/stderr is interleaved on the same
|
|
19
|
-
* stream — the host greps for the block, never for prose that may be reworded.
|
|
20
|
-
*/
|
|
21
|
-
const AUTH_BLOCK_OPEN = '<<REMIXMATE_AUTH_REQUIRED';
|
|
22
|
-
const AUTH_BLOCK_CLOSE = 'REMIXMATE_AUTH_REQUIRED>>';
|
|
23
|
-
export function renderAuthBlock(apiBaseUrl, code) {
|
|
24
|
-
const payload = {
|
|
25
|
-
apiBaseUrl,
|
|
26
|
-
verificationUri: code.verificationUri,
|
|
27
|
-
verificationUriComplete: code.verificationUriComplete ?? code.verificationUri,
|
|
28
|
-
userCode: code.userCode,
|
|
29
|
-
expiresIn: code.expiresIn,
|
|
30
|
-
};
|
|
31
|
-
return `${AUTH_BLOCK_OPEN}\n${JSON.stringify(payload)}\n${AUTH_BLOCK_CLOSE}\n`;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Emit the authorization prompt: a one-click link (device code pre-filled),
|
|
35
|
-
* the manual fallback, and — for agent hosts — the machine-readable block.
|
|
36
|
-
* Shared by `runDeviceFlow` and `login --start` so both entry points present
|
|
37
|
-
* authorization identically.
|
|
38
|
-
*/
|
|
39
|
-
export function writeAuthPrompt(write, apiBaseUrl, code) {
|
|
40
|
-
const openUrl = code.verificationUriComplete ?? code.verificationUri;
|
|
41
|
-
write(`\n🔐 需要授权 remixmate CLI,请在浏览器中打开:\n ${openUrl}\n`);
|
|
42
|
-
if (code.verificationUriComplete) {
|
|
43
|
-
write(`(若链接无法打开:访问 ${code.verificationUri} 并输入设备码 ${code.userCode})\n`);
|
|
44
|
-
}
|
|
45
|
-
write('\n');
|
|
46
|
-
if (isAgentHost())
|
|
47
|
-
write(renderAuthBlock(apiBaseUrl, code));
|
|
48
|
-
}
|
|
49
|
-
function nowSec() {
|
|
50
|
-
return Math.floor(Date.now() / 1000);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Persist an approved PrivToken. Shared by `runDeviceFlow` and `login --wait`
|
|
54
|
-
* so a credential written by either is indistinguishable. The token value is
|
|
55
|
-
* never written to `write`.
|
|
56
|
-
*/
|
|
57
|
-
export async function persistApproval(apiBaseUrl, privToken, userLabel, write) {
|
|
58
|
-
const { usedKeychain, keychainError } = await setCredential(apiBaseUrl, {
|
|
59
|
-
privToken,
|
|
60
|
-
userLabel,
|
|
61
|
-
createdAt: nowSec(),
|
|
62
|
-
});
|
|
63
|
-
if (!usedKeychain && keychainError) {
|
|
64
|
-
write('(系统钥匙串不可用,凭证已写入受保护的本地文件 ~/.config/remixmate/credentials.json)\n');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Run one full Device Flow against `apiBaseUrl`. On approval the PrivToken is
|
|
69
|
-
* persisted via setCredential (keychain or file, same as `login`) and only the
|
|
70
|
-
* non-secret outcome is returned.
|
|
71
|
-
*/
|
|
72
|
-
export async function runDeviceFlow(apiBaseUrl, opts = {}) {
|
|
73
|
-
const write = opts.write ?? ((msg) => process.stderr.write(msg));
|
|
74
|
-
const clientLabel = opts.clientLabel ?? `remixmate-cli (${process.platform} ${process.arch})`;
|
|
75
|
-
try {
|
|
76
|
-
const code = await requestDeviceCode(apiBaseUrl, clientLabel);
|
|
77
|
-
// The backend pre-fills the user code into verificationUriComplete, so the
|
|
78
|
-
// user only has to click and confirm. Lead with that link — asking someone
|
|
79
|
-
// to open a bare URL and transcribe a device code is a different (much
|
|
80
|
-
// higher) difficulty tier, and the whole point is that a non-technical user
|
|
81
|
-
// never has to touch a terminal.
|
|
82
|
-
const openUrl = code.verificationUriComplete ?? code.verificationUri;
|
|
83
|
-
// Always print the link, even when the browser auto-opens, so a host
|
|
84
|
-
// (Claude Code / Codex) can relay it into the chat.
|
|
85
|
-
writeAuthPrompt(write, apiBaseUrl, code);
|
|
86
|
-
if (openBrowser(openUrl)) {
|
|
87
|
-
write('已尝试自动打开浏览器…\n');
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
write('无法自动打开浏览器,请手动复制上面的链接。\n');
|
|
91
|
-
}
|
|
92
|
-
write('等待授权中(可在浏览器确认)…\n');
|
|
93
|
-
const result = await pollForToken(apiBaseUrl, code, { maxWaitMs: opts.maxWaitMs });
|
|
94
|
-
if (result.status === 'approved' && result.privToken) {
|
|
95
|
-
await persistApproval(apiBaseUrl, result.privToken, result.userLabel, write);
|
|
96
|
-
return { status: 'approved', userLabel: result.userLabel };
|
|
97
|
-
}
|
|
98
|
-
if (result.status === 'access_denied')
|
|
99
|
-
return { status: 'denied' };
|
|
100
|
-
if (result.timedOut)
|
|
101
|
-
return { status: 'timeout' };
|
|
102
|
-
return { status: 'expired' };
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
if (err instanceof DeviceFlowError) {
|
|
106
|
-
return { status: 'error', message: err.message };
|
|
107
|
-
}
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
1
|
+
var c=Object.defineProperty;var o=(r,t)=>c(r,"name",{value:t,configurable:!0});import{DeviceFlowError as u,openBrowser as f,pollForToken as p,requestDeviceCode as l}from"./device-flow.js";import{setCredential as m}from"./credential-store.js";import{isAgentHost as v}from"./environment.js";const U="<<REMIXMATE_AUTH_REQUIRED",C="REMIXMATE_AUTH_REQUIRED>>";function x(r,t){const e={apiBaseUrl:r,verificationUri:t.verificationUri,verificationUriComplete:t.verificationUriComplete??t.verificationUri,userCode:t.userCode,expiresIn:t.expiresIn};return`${U}
|
|
2
|
+
${JSON.stringify(e)}
|
|
3
|
+
${C}
|
|
4
|
+
`}o(x,"renderAuthBlock");function E(r,t,e){const s=e.verificationUriComplete??e.verificationUri;r(`
|
|
5
|
+
🔐 需要授权 remixmate CLI,请在浏览器中打开:
|
|
6
|
+
${s}
|
|
7
|
+
`),e.verificationUriComplete&&r(`(若链接无法打开:访问 ${e.verificationUri} 并输入设备码 ${e.userCode})
|
|
8
|
+
`),r(`
|
|
9
|
+
`),v()&&r(x(t,e))}o(E,"writeAuthPrompt");function d(){return Math.floor(Date.now()/1e3)}o(d,"nowSec");async function w(r,t,e,s){const{usedKeychain:i,keychainError:a}=await m(r,{privToken:t,userLabel:e,createdAt:d()});!i&&a&&s(`(系统钥匙串不可用,凭证已写入受保护的本地文件 ~/.config/remixmate/credentials.json)
|
|
10
|
+
`)}o(w,"persistApproval");async function h(r,t={}){const e=t.write??(i=>process.stderr.write(i)),s=t.clientLabel??`remixmate-cli (${process.platform} ${process.arch})`;try{const i=await l(r,s),a=i.verificationUriComplete??i.verificationUri;E(e,r,i),f(a)?e(`已尝试自动打开浏览器…
|
|
11
|
+
`):e(`无法自动打开浏览器,请手动复制上面的链接。
|
|
12
|
+
`),e(`等待授权中(可在浏览器确认)…
|
|
13
|
+
`);const n=await p(r,i,{maxWaitMs:t.maxWaitMs});return n.status==="approved"&&n.privToken?(await w(r,n.privToken,n.userLabel,e),{status:"approved",userLabel:n.userLabel}):n.status==="access_denied"?{status:"denied"}:n.timedOut?{status:"timeout"}:{status:"expired"}}catch(i){if(i instanceof u)return{status:"error",message:i.message};throw i}}o(h,"runDeviceFlow");export{w as persistApproval,x as renderAuthBlock,h as runDeviceFlow,E as writeAuthPrompt};
|