@remixmate/cli 0.9.72 → 0.9.74

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.
Files changed (179) hide show
  1. package/LICENSE +45 -21
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/argv.js +1 -51
  5. package/dist/auth/auth-lock.js +1 -100
  6. package/dist/auth/auto-login.js +7 -75
  7. package/dist/auth/commands.js +40 -290
  8. package/dist/auth/credential-store.js +1 -143
  9. package/dist/auth/device-flow-runner.js +13 -110
  10. package/dist/auth/device-flow.js +1 -132
  11. package/dist/auth/ensure.js +1 -62
  12. package/dist/auth/environment.js +1 -69
  13. package/dist/auth/pending-store.js +1 -45
  14. package/dist/auth/resolve.js +1 -44
  15. package/dist/billing.js +3 -76
  16. package/dist/capabilities.js +1 -75
  17. package/dist/cli.js +39 -211
  18. package/dist/diagnostics.js +2 -70
  19. package/dist/doctor.js +9 -188
  20. package/dist/errors.js +1 -33
  21. package/dist/exec.js +4 -47
  22. package/dist/handlers/gen-digital-human.js +33 -264
  23. package/dist/handlers/gen-image.js +9 -113
  24. package/dist/handlers/gen-post.js +15 -192
  25. package/dist/handlers/gen-video.js +7 -185
  26. package/dist/handlers/gen-voice.js +19 -133
  27. package/dist/handlers/index.js +1 -20
  28. package/dist/handlers/shared.js +1 -66
  29. package/dist/handlers/template-init.js +1 -21
  30. package/dist/http.js +3 -155
  31. package/dist/index.js +1 -8
  32. package/dist/install.js +40 -448
  33. package/dist/manifest.json +44 -3
  34. package/dist/progress.js +2 -15
  35. package/dist/project/commands.js +34 -156
  36. package/dist/project/host.js +1 -151
  37. package/dist/project/resolve.js +1 -65
  38. package/dist/project/store.js +2 -97
  39. package/dist/project/take.js +1 -106
  40. package/dist/project/trace.js +5 -278
  41. package/dist/registry.js +3 -70
  42. package/dist/runner.js +7 -225
  43. package/dist/skill-schema.js +1 -194
  44. package/dist/template/args.js +1 -69
  45. package/dist/template/check.js +2 -30
  46. package/dist/template/commands.js +34 -163
  47. package/dist/template/constants.js +1 -46
  48. package/dist/template/init.js +5 -133
  49. package/dist/template/list.js +1 -46
  50. package/dist/template/preview.js +7 -318
  51. package/dist/template/proc.js +1 -43
  52. package/dist/template/project.js +6 -161
  53. package/dist/template/publish.js +5 -185
  54. package/dist/template/render-client.js +2 -79
  55. package/dist/template/upgrade.js +4 -123
  56. package/dist/text.js +1 -15
  57. package/package.json +2 -35
  58. package/skills/doc-parse/scripts/extract_via_render.py +32 -32
  59. package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
  60. package/skills/gen-script/SKILL.md +23 -1
  61. package/skills/gen-script/scripts/gen_script.py +644 -551
  62. package/skills/gen-script/skill.json +12 -0
  63. package/skills/gen-script/version.json +1 -1
  64. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
  65. package/skills/render-video/scripts/_chrome_vendor.py +17 -17
  66. package/skills/render-video/scripts/_digital_human_segments.py +34 -34
  67. package/skills/render-video/scripts/_video_probe.py +33 -33
  68. package/skills/render-video/scripts/_vod_polling.py +13 -13
  69. package/skills/render-video/scripts/remote_renderer_client.py +42 -42
  70. package/skills/render-video/scripts/render_video.py +639 -639
  71. package/skills/render-video/scripts/required_props.py +37 -37
  72. package/skills/render-video/scripts/upload_video.py +35 -35
  73. package/skills/render-video/version.json +1 -1
  74. package/skills/template-registry/SKILL.md +16 -0
  75. package/skills/template-registry/scripts/countdown_spec.py +92 -92
  76. package/skills/template-registry/scripts/doc_extract.py +40 -40
  77. package/skills/template-registry/scripts/list_templates.py +303 -191
  78. package/skills/template-registry/scripts/log_diagnostics.py +1 -1
  79. package/skills/template-registry/scripts/match_template.py +87 -87
  80. package/skills/template-registry/scripts/payload_hydrator.py +89 -89
  81. package/skills/template-registry/scripts/registry_loader.py +159 -159
  82. package/skills/template-registry/scripts/render_job_client.py +75 -75
  83. package/skills/template-registry/scripts/showcase_loader.py +44 -44
  84. package/skills/template-registry/skill.json +4 -1
  85. package/skills/template-registry/version.json +1 -1
  86. package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
  87. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
  88. package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
  89. package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
  90. package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
  91. package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
  92. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
  93. package/skills/video-parser/scripts/analyze_video.py +173 -173
  94. package/skills/video-parser/scripts/deconstruct_video.py +91 -91
  95. package/skills/video-parser/scripts/parse_via_render.py +21 -21
  96. package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
  97. package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
  98. package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
  99. package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
  100. package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
  101. package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
  102. package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
  103. package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
  104. package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
  105. package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
  106. package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
  107. package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
  108. package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
  109. package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
  110. package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
  111. package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
  112. package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
  113. package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
  114. package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
  115. package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
  116. package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
  117. package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
  118. package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
  119. package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
  120. package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
  121. package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
  122. package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
  123. package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
  124. package/skills/web-screenshot/scripts/_media_upload.py +33 -33
  125. package/skills/web-screenshot/scripts/read_page.py +15 -15
  126. package/skills/web-screenshot/scripts/record.py +26 -26
  127. package/skills/web-screenshot/scripts/screenshot.py +5 -5
  128. package/dist/argv.d.ts +0 -15
  129. package/dist/auth/auth-lock.d.ts +0 -26
  130. package/dist/auth/auto-login.d.ts +0 -20
  131. package/dist/auth/commands.d.ts +0 -8
  132. package/dist/auth/credential-store.d.ts +0 -60
  133. package/dist/auth/device-flow-runner.d.ts +0 -58
  134. package/dist/auth/device-flow.d.ts +0 -52
  135. package/dist/auth/ensure.d.ts +0 -55
  136. package/dist/auth/environment.d.ts +0 -38
  137. package/dist/auth/pending-store.d.ts +0 -33
  138. package/dist/auth/resolve.d.ts +0 -30
  139. package/dist/billing.d.ts +0 -44
  140. package/dist/capabilities.d.ts +0 -63
  141. package/dist/cli.d.ts +0 -13
  142. package/dist/diagnostics.d.ts +0 -24
  143. package/dist/doctor.d.ts +0 -22
  144. package/dist/errors.d.ts +0 -29
  145. package/dist/exec.d.ts +0 -18
  146. package/dist/handlers/gen-digital-human.d.ts +0 -17
  147. package/dist/handlers/gen-image.d.ts +0 -20
  148. package/dist/handlers/gen-post.d.ts +0 -22
  149. package/dist/handlers/gen-video.d.ts +0 -19
  150. package/dist/handlers/gen-voice.d.ts +0 -26
  151. package/dist/handlers/index.d.ts +0 -20
  152. package/dist/handlers/shared.d.ts +0 -26
  153. package/dist/handlers/template-init.d.ts +0 -10
  154. package/dist/http.d.ts +0 -72
  155. package/dist/index.d.ts +0 -9
  156. package/dist/install.d.ts +0 -71
  157. package/dist/progress.d.ts +0 -23
  158. package/dist/project/commands.d.ts +0 -19
  159. package/dist/project/host.d.ts +0 -91
  160. package/dist/project/resolve.d.ts +0 -43
  161. package/dist/project/store.d.ts +0 -31
  162. package/dist/project/take.d.ts +0 -50
  163. package/dist/project/trace.d.ts +0 -98
  164. package/dist/registry.d.ts +0 -30
  165. package/dist/runner.d.ts +0 -39
  166. package/dist/skill-schema.d.ts +0 -138
  167. package/dist/template/args.d.ts +0 -25
  168. package/dist/template/check.d.ts +0 -23
  169. package/dist/template/commands.d.ts +0 -25
  170. package/dist/template/constants.d.ts +0 -39
  171. package/dist/template/init.d.ts +0 -28
  172. package/dist/template/list.d.ts +0 -23
  173. package/dist/template/preview.d.ts +0 -60
  174. package/dist/template/proc.d.ts +0 -26
  175. package/dist/template/project.d.ts +0 -47
  176. package/dist/template/publish.d.ts +0 -43
  177. package/dist/template/render-client.d.ts +0 -39
  178. package/dist/template/upgrade.d.ts +0 -44
  179. package/dist/text.d.ts +0 -8
@@ -1,290 +1,40 @@
1
- /**
2
- * `remixmate login | logout | whoami` command implementations.
3
- *
4
- * Security: the PrivToken value is never printed. Identity is shown by label only.
5
- * Env/CI behavior: when PRIV_TOKEN is set, `login` does not start the device flow.
6
- */
7
- import { parseArgv } from '../argv.js';
8
- import { resolveApiBaseUrl } from '../http.js';
9
- import { EXIT } from '../errors.js';
10
- import { getCredential, listCredentials, removeCredential } from './credential-store.js';
11
- import { resolvePrivToken } from './resolve.js';
12
- import { persistApproval, runDeviceFlow, writeAuthPrompt } from './device-flow-runner.js';
13
- import { openBrowser, pollForToken, requestDeviceCode, DeviceFlowError } from './device-flow.js';
14
- import { clearPending, getPending, setPending } from './pending-store.js';
15
- import { canAutoAuth, isAgentHost } from './environment.js';
16
- /**
17
- * Bounded wait used when an agent host runs `login` on the user's behalf. The
18
- * device code lives ~10 minutes, but an agent's tool call does not — blocking
19
- * that long guarantees the call is killed with nothing persisted. Stop early
20
- * and tell the caller it can simply re-run.
21
- */
22
- const AGENT_LOGIN_WAIT_MS = 90_000;
23
- /**
24
- * Explain, when the active backend has no credential, which backends DO have
25
- * one. Returns '' when there is nothing useful to add.
26
- *
27
- * Credentials are keyed by API base URL, so being "logged in" to
28
- * http://localhost:2999/api tells you nothing about production — but the bare
29
- * message ("not logged in") reads as if `login` had never been run.
30
- */
31
- async function describeOtherBackends(activeApiBaseUrl) {
32
- const others = (await listCredentials()).filter((c) => c.apiBaseUrl !== activeApiBaseUrl);
33
- if (others.length === 0)
34
- return '';
35
- const lines = others.map((c) => ` - ${c.apiBaseUrl}${c.userLabel ? `(${c.userLabel})` : ''}`);
36
- return (`ℹ️ 当前后端:${activeApiBaseUrl}(无凭证)\n` +
37
- ` 已存储凭证的后端:\n${lines.join('\n')}\n` +
38
- ` 要使用其中之一,设置 MM_API_BASE_URL=<该地址>,或加 --api-base-url <该地址>。\n`);
39
- }
40
- async function login(apiBaseUrl) {
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
- * Shared Device Flow orchestration used by BOTH `remixmate login` (interactive,
3
- * full device-code lifetime) and the automatic browser auth that kicks in when
4
- * a skill call finds no token (bounded by a short wait window).
5
- *
6
- * Centralizing the request → open browser → poll → persist sequence keeps the
7
- * two entry points byte-for-byte consistent (Requirement 4.1/4.2) and means the
8
- * credential written by auto-login is indistinguishable from `login`'s.
9
- *
10
- * Security: the PrivToken value is NEVER written to the provided sink.
11
- */
12
- import { DeviceFlowError, openBrowser, pollForToken, requestDeviceCode, } from './device-flow.js';
13
- import { setCredential } from './credential-store.js';
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};