rush-ai 0.16.1 → 0.17.0

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/README.md CHANGED
@@ -67,6 +67,30 @@ rush-ai task push
67
67
 
68
68
  `task push` 会自动把改动推到 Rush,preview URL 几秒后就会显示新版本。
69
69
 
70
+ ### 管理项目环境变量
71
+
72
+ 按 `preview` / `production` 分环境给项目配置环境变量(值加密存储、运行时注入,不写进代码仓库):
73
+
74
+ ```bash
75
+ # 交互式输入(不回显、不进 shell history),默认 preview
76
+ rush-ai task env set OPENAI_API_KEY
77
+
78
+ # 直接给值 / 指定 production
79
+ rush-ai task env set OPENAI_API_KEY sk-xxx --env production
80
+
81
+ # 从 stdin 读(CI 友好)
82
+ echo -n "$SECRET" | rush-ai task env set OPENAI_API_KEY --env production
83
+
84
+ # 查看(不传 --env 列全部两环境,值脱敏)
85
+ rush-ai task env list
86
+ rush-ai task env list --env production
87
+
88
+ # 删除
89
+ rush-ai task env rm OPENAI_API_KEY --env production
90
+ ```
91
+
92
+ 当前目录是 Rush 项目时自动识别项目,否则用 `--project <id>` 指定。
93
+
70
94
  ## Quick Start
71
95
 
72
96
  要求:Node.js >= 18。如果不想全局装,任何 `rush-ai` 命令都可以用 `npx rush-ai` 替代。
@@ -93,6 +117,7 @@ rush-ai task status <id> --json
93
117
  - **任务生命周期**:`task create` / `send` / `status` / `watch`(实时流)/ `result` / `files` / `cancel`
94
118
  - **本地同步**:`task push` —— 改完本地代码,Rush preview 立刻更新
95
119
  - **生产发布**:`task deploy <id>` —— web-builder 产物发 prod,支持自定义域名
120
+ - **环境变量**:`task env list/set/rm` —— 按 preview/production 管理项目环境变量(加密存储、运行时注入)
96
121
  - **agent shelf**:`agent list` / `agent info` —— 浏览和使用 Rush 平台的专家 agent
97
122
  - **MCP 集成**:作为 MCP stdio server 跑,或浏览平台上的 MCP server 和工具
98
123
  - **Skill 管理**:`skill install/list/publish` 代理 reskill,但复用 Rush 登录态和 registry
@@ -121,6 +146,9 @@ rush-ai task status <id> --json
121
146
  | `task deploy <id>` | 发布 web-builder 产物到 prod(`--domain` / `--version` / `--env`) |
122
147
  | `task versions <id>` | 列出 web-builder 任务的可发布版本 |
123
148
  | `task domain check <prefix>` | 校验自定义域名前缀是否可用(需 `--task <id>`) |
149
+ | `task env list` | 列出项目环境变量(脱敏);`--env preview\|production` 过滤,默认全部 |
150
+ | `task env set <KEY> [value]` | 新增/更新环境变量;默认 `preview`,值可省(走 stdin / 交互式输入) |
151
+ | `task env rm <KEY>` | 删除环境变量(`--env`,默认 `preview`) |
124
152
 
125
153
  > 命名迁移事实:0.10.0 起 `task init` 改名为 `task link`(对齐 `vercel link` / `netlify link` 语义);`task init` 作为 deprecated alias 在 0.13.0 移除。
126
154
  >
@@ -1,6 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/mcp/prompt-utils.ts
4
+ function sanitizeForTerminal(text) {
5
+ const normalized = text.replace(/\r\n?/g, "\n");
6
+ let out = "";
7
+ for (const ch of normalized) {
8
+ const code = ch.codePointAt(0) ?? 0;
9
+ const isAllowedWhitespace = code === 9 || code === 10;
10
+ const isControl = code <= 31 || code === 127 || code >= 128 && code <= 159;
11
+ if (isAllowedWhitespace || !isControl) {
12
+ out += ch;
13
+ }
14
+ }
15
+ return out;
16
+ }
17
+ function sanitizeInline(text) {
18
+ let out = "";
19
+ for (const ch of text) {
20
+ const code = ch.codePointAt(0) ?? 0;
21
+ const isControl = code <= 31 || code === 127 || code >= 128 && code <= 159;
22
+ if (!isControl) {
23
+ out += ch;
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ function indentLines(text, indent) {
29
+ return text.split("\n").map((line) => `${indent}${line}`).join("\n");
30
+ }
4
31
  function readOneLine() {
5
32
  return new Promise((resolve) => {
6
33
  let acc = "";
@@ -27,6 +54,10 @@ async function promptCredentials(extraConfigMeta, options) {
27
54
  const isTTY = Boolean(process.stdin.isTTY);
28
55
  const skipPrompt = options?.yes || !isTTY;
29
56
  for (const [key, meta] of Object.entries(extraConfigMeta)) {
57
+ const safeKey = sanitizeInline(key);
58
+ const description = meta.description ? sanitizeForTerminal(meta.description) : void 0;
59
+ const helpUrl = meta.helpUrl ? sanitizeInline(meta.helpUrl) : void 0;
60
+ const safeDefault = meta.defaultValue !== void 0 ? sanitizeInline(meta.defaultValue) : void 0;
30
61
  if (!meta.required) {
31
62
  if (meta.defaultValue) {
32
63
  values[key] = meta.defaultValue;
@@ -37,17 +68,30 @@ async function promptCredentials(extraConfigMeta, options) {
37
68
  if (meta.defaultValue) {
38
69
  values[key] = meta.defaultValue;
39
70
  } else {
71
+ const presetHint = options?.secretFlag ? `pass ${options.secretFlag} ${safeKey}=<value>, or ` : "provide it non-interactively, or ";
40
72
  throw new Error(
41
- `Credential "${key}" is required but no default value is available. Run interactively or provide credentials via the Rush web UI.` + (meta.helpUrl ? ` Help: ${meta.helpUrl}` : "")
73
+ `Credential "${safeKey}" is required but no default value is available. Run interactively, ${presetHint}set it via the Rush web UI.` + (description ? `
74
+ ${description}` : "") + (helpUrl ? `
75
+ Help: ${helpUrl}` : "")
42
76
  );
43
77
  }
44
78
  continue;
45
79
  }
46
80
  const typeHint = meta.type === "secret" ? " (secret)" : "";
47
- const defaultHint = meta.defaultValue ? ` [${meta.defaultValue}]` : "";
48
- const helpHint = meta.helpUrl ? `
49
- Help: ${meta.helpUrl}` : "";
50
- process.stderr.write(` ${key}${typeHint}${defaultHint}:${helpHint} `);
81
+ const defaultHint = safeDefault ? ` [${safeDefault}]` : "";
82
+ if (description) {
83
+ process.stderr.write(`${indentLines(description, " ")}
84
+ `);
85
+ }
86
+ if (helpUrl) {
87
+ process.stderr.write(` \u83B7\u53D6\u65B9\u5F0F\uFF1A${helpUrl}
88
+ `);
89
+ }
90
+ if (description || helpUrl) {
91
+ process.stderr.write(` \uFF08\u83B7\u53D6\u540E\u7C98\u8D34\u5230\u4E0B\u65B9\uFF09
92
+ `);
93
+ }
94
+ process.stderr.write(` ${safeKey}${typeHint}${defaultHint}: `);
51
95
  const input = await readOneLine();
52
96
  const trimmed = input.trim();
53
97
  if (trimmed) {
@@ -56,7 +100,7 @@ async function promptCredentials(extraConfigMeta, options) {
56
100
  values[key] = meta.defaultValue;
57
101
  } else {
58
102
  throw new Error(
59
- `Credential "${key}" is required but was not provided.` + (meta.helpUrl ? ` Help: ${meta.helpUrl}` : "")
103
+ `Credential "${safeKey}" is required but was not provided.` + (helpUrl ? ` Help: ${helpUrl}` : "")
60
104
  );
61
105
  }
62
106
  }
@@ -158,4 +202,4 @@ export {
158
202
  pathExists,
159
203
  promptCredentials
160
204
  };
161
- //# sourceMappingURL=chunk-GDSJUMK4.js.map
205
+ //# sourceMappingURL=chunk-YYQF6ZB6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/mcp/prompt-utils.ts","../src/installers/claude-code/atomic-json.ts"],"sourcesContent":["/**\n * 交互式凭证收集(raw stdin,无第三方依赖)。\n *\n * 复用 commands/task/deploy.ts:460-475 的 readOneLine 模式。\n */\n\nexport interface ExtraConfigFieldMeta {\n helpUrl?: string;\n /** 获取说明 / 步骤(多行纯文本),prompt 与报错时一并展示,引导用户如何拿到凭据 */\n description?: string;\n type?: 'text' | 'secret';\n required?: boolean;\n defaultValue?: string;\n}\n\nexport interface PromptCredentialsOptions {\n /** 跳过交互(--yes / 非 TTY),required 且无默认值时抛错 */\n yes?: boolean;\n /**\n * 非交互报错里提示的预置凭证命令行参数名。\n * `plugin install` 用 `--secret`,`mcp install` 用 `--set`;\n * 不传则给出通用措辞。promptCredentials 自身不知道被哪个命令调用,必须由调用方注入。\n */\n secretFlag?: string;\n}\n\n/**\n * 移除可能污染终端 / 伪造日志的控制字符(保留 \\n 与 \\t),并把 CRLF 归一为 LF。\n *\n * description / helpUrl 是定义者可控的内容,直写 stderr / Error 前必须 sanitize,\n * 防止 ANSI ESC、C0/C1 控制字符造成清屏、光标回退、日志伪造等问题。\n *\n * 用 codePoint 过滤而非内联控制字符的正则,避免源码里出现裸控制字符。\n */\nfunction sanitizeForTerminal(text: string): string {\n const normalized = text.replace(/\\r\\n?/g, '\\n');\n let out = '';\n for (const ch of normalized) {\n const code = ch.codePointAt(0) ?? 0;\n const isAllowedWhitespace = code === 0x09 || code === 0x0a; // \\t \\n\n const isControl =\n code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f);\n if (isAllowedWhitespace || !isControl) {\n out += ch;\n }\n }\n return out;\n}\n\n/**\n * 单行字段(key / helpUrl / defaultValue)的清洗:剔除**所有**控制字符(含 \\n / \\t),\n * 防止换行/制表造成多行输出伪造。这些字段都是单行语义,不应包含任何空白控制符。\n */\nfunction sanitizeInline(text: string): string {\n let out = '';\n for (const ch of text) {\n const code = ch.codePointAt(0) ?? 0;\n const isControl =\n code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f);\n if (!isControl) {\n out += ch;\n }\n }\n return out;\n}\n\n/** 把多行文本按统一缩进格式化,用于 stderr 引导输出 */\nfunction indentLines(text: string, indent: string): string {\n return text\n .split('\\n')\n .map((line) => `${indent}${line}`)\n .join('\\n');\n}\n\n/**\n * 从 stdin 读取一行。非 TTY 时如果 stdin 已关闭则 resolve 空字符串。\n */\nfunction readOneLine(): Promise<string> {\n return new Promise((resolve) => {\n let acc = '';\n const onData = (chunk: Buffer) => {\n acc += chunk.toString('utf-8');\n const nlIdx = acc.indexOf('\\n');\n if (nlIdx !== -1) {\n process.stdin.removeListener('data', onData);\n process.stdin.pause();\n resolve(acc.slice(0, nlIdx));\n }\n };\n const onEnd = () => {\n process.stdin.removeListener('data', onData);\n resolve(acc);\n };\n process.stdin.resume();\n process.stdin.on('data', onData);\n process.stdin.once('end', onEnd);\n });\n}\n\n/**\n * 交互式收集凭证。\n *\n * - TTY: 提示用户输入每个 required 字段\n * - 非 TTY / --yes: 使用 defaultValue,required 且无 default 的字段 → 抛错\n */\nexport async function promptCredentials(\n extraConfigMeta: Record<string, ExtraConfigFieldMeta>,\n options?: PromptCredentialsOptions\n): Promise<Record<string, string>> {\n const values: Record<string, string> = {};\n const isTTY = Boolean(process.stdin.isTTY);\n const skipPrompt = options?.yes || !isTTY;\n\n for (const [key, meta] of Object.entries(extraConfigMeta)) {\n // 全部定义者可控内容展示前都要 sanitize(返回值仍用原始 key 以正确映射配置):\n // key / helpUrl / defaultValue 是单行语义 → 连 \\n\\t 一并剔除;description 多行 → 保留 \\n\\t\n const safeKey = sanitizeInline(key);\n const description = meta.description\n ? sanitizeForTerminal(meta.description)\n : undefined;\n const helpUrl = meta.helpUrl ? sanitizeInline(meta.helpUrl) : undefined;\n const safeDefault =\n meta.defaultValue !== undefined\n ? sanitizeInline(meta.defaultValue)\n : undefined;\n\n if (!meta.required) {\n if (meta.defaultValue) {\n values[key] = meta.defaultValue;\n }\n continue;\n }\n\n if (skipPrompt) {\n if (meta.defaultValue) {\n values[key] = meta.defaultValue;\n } else {\n const presetHint = options?.secretFlag\n ? `pass ${options.secretFlag} ${safeKey}=<value>, or `\n : 'provide it non-interactively, or ';\n throw new Error(\n `Credential \"${safeKey}\" is required but no default value is available. ` +\n `Run interactively, ${presetHint}set it via the Rush web UI.` +\n (description ? `\\n${description}` : '') +\n (helpUrl ? `\\nHelp: ${helpUrl}` : '')\n );\n }\n continue;\n }\n\n // Interactive prompt:先展示获取说明与链接,再提示输入\n const typeHint = meta.type === 'secret' ? ' (secret)' : '';\n const defaultHint = safeDefault ? ` [${safeDefault}]` : '';\n if (description) {\n process.stderr.write(`${indentLines(description, ' ')}\\n`);\n }\n if (helpUrl) {\n process.stderr.write(` 获取方式:${helpUrl}\\n`);\n }\n if (description || helpUrl) {\n process.stderr.write(` (获取后粘贴到下方)\\n`);\n }\n process.stderr.write(` ${safeKey}${typeHint}${defaultHint}: `);\n\n const input = await readOneLine();\n const trimmed = input.trim();\n\n if (trimmed) {\n values[key] = trimmed;\n } else if (meta.defaultValue) {\n values[key] = meta.defaultValue;\n } else {\n throw new Error(\n `Credential \"${safeKey}\" is required but was not provided.` +\n (helpUrl ? ` Help: ${helpUrl}` : '')\n );\n }\n }\n\n return values;\n}\n","/**\n * 通用的原子 JSON 读-改-写 helper(task-6 产物)。\n *\n * 契约(对齐 spec §2.1 / §3.3):\n * - 读不到 / 文件不存在 → 视为空(返回调用方传入的 `defaults`),mtime = null\n * - JSON 损坏 / 顶层 shape 非 object → 抛 `AtomicJsonCorruptError`(**不尝试修复**)\n * - 写入走 write-.tmp → rename 原子替换(POSIX 同文件系统原子)\n * - mtime 冲突检测留给调用方(Installer 在 rollback 逻辑里自己处理)——本 helper\n * 是纯磁盘 IO,不持 mtime state\n *\n * 为什么不复用 `registry.ts` 里的 `atomicWrite`:\n * - 那个函数是模块内 private(未 export)\n * - 它耦合了 `RushRegistryData` schema(load 会做 schemaVersion assert)\n * - task-6 需要写 3 个不同 schema 的 JSON(known_marketplaces / installed_plugins /\n * settings),独立 helper 更适配\n *\n * 为什么不 depend on 第三方库(如 `atomically`):\n * - rush-ai 已有极简依赖集,新增 dep 需要独立评估\n * - 语义简单到 20 行能 cover 所有场景\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { constants as fsConstants } from 'node:fs';\nimport {\n access,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Error 类\n// ---------------------------------------------------------------------------\n\nexport class AtomicJsonError extends Error {\n constructor(\n message: string,\n public readonly filePath: string\n ) {\n super(message);\n this.name = 'AtomicJsonError';\n }\n}\n\n/** JSON 损坏或顶层非 object。spec §5.2:不尝试修复,让用户处理。 */\nexport class AtomicJsonCorruptError extends AtomicJsonError {\n constructor(\n filePath: string,\n public readonly cause: unknown\n ) {\n super(\n `${filePath} 解析失败,拒绝读取。请手工修复或备份后删除。原因:${String(\n (cause as Error | undefined)?.message ?? cause\n )}`,\n filePath\n );\n this.name = 'AtomicJsonCorruptError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// 读取\n// ---------------------------------------------------------------------------\n\nexport interface ReadJsonResult<T> {\n data: T;\n /** 文件 mtime(ms);文件不存在时为 `null`(视为首次写入) */\n mtimeMs: number | null;\n /** 文件是否在磁盘上存在(区分\"空 registry 视图 vs 真实空文件\") */\n existed: boolean;\n}\n\n/**\n * 读 JSON 文件。不存在 → `{ data: defaults(), mtimeMs: null, existed: false }`。\n *\n * `defaults` 是函数而不是值——避免默认对象被多次共享引用。\n *\n * 不做任何 shape 校验(只 gate JSON 语法 + 顶层必须是 object);调用方在拿到 `data`\n * 后自行做更严格的 schema 校验。\n */\nexport async function readJsonFile<T>(\n filePath: string,\n defaults: () => T\n): Promise<ReadJsonResult<T>> {\n if (!(await pathExists(filePath))) {\n return { data: defaults(), mtimeMs: null, existed: false };\n }\n const stats = await stat(filePath);\n const raw = await readFile(filePath, 'utf8');\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new AtomicJsonCorruptError(filePath, err);\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new AtomicJsonCorruptError(\n filePath,\n new Error('顶层必须是 JSON object,不允许数组或基本类型')\n );\n }\n return {\n data: parsed as T,\n mtimeMs: stats.mtimeMs,\n existed: true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// 写入\n// ---------------------------------------------------------------------------\n\nexport interface WriteJsonOptions {\n /** 缩进空格数,默认 2(方便人工审查) */\n indent?: number;\n /** 末尾是否加换行,默认 true(符合 POSIX 文本规范) */\n trailingNewline?: boolean;\n}\n\n/**\n * 原子写 JSON 文件。\n *\n * 步骤:\n * 1. `mkdir -p` 确保父目录存在\n * 2. 写入 `<filePath>.<uuid>.tmp`\n * 3. `rename(tmp, filePath)` 原子替换\n *\n * 失败时 best-effort 清 tmp 文件(不覆盖原始错误)。\n *\n * **不做**:\n * - mtime 比对(留给调用方)\n * - JSON shape 校验(调用方已经知道自己在写什么)\n */\nexport async function writeJsonFile(\n filePath: string,\n data: unknown,\n options: WriteJsonOptions = {}\n): Promise<void> {\n const { indent = 2, trailingNewline = true } = options;\n await mkdir(dirname(filePath), { recursive: true });\n const payload =\n JSON.stringify(data, null, indent) + (trailingNewline ? '\\n' : '');\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, payload, { encoding: 'utf8', flag: 'w' });\n await rename(tmp, filePath);\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {});\n throw err;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Read-modify-write(便于 installer 用法)\n// ---------------------------------------------------------------------------\n\n/**\n * Read-modify-write 组合操作:原子读 → 同步 mutate → 原子写。\n *\n * 典型场景:Installer 更新 known_marketplaces.json 的某个 key,不碰其他字段。\n *\n * **mtime 保护策略**:\n * - 读时记 mtimeMs\n * - 写前再 stat 比对;如果 mtime 变了 → 抛 `AtomicJsonConflictError`\n * - 调用方(Installer)可选择 retry(本 helper 不做 retry,避免隐藏状态)\n *\n * **注意**:若 `mutator` 返回 `undefined`(表示\"没变化\"),仍会写回——caller 需要\n * 自己判断是否需要写(fast path 省略 write 是 caller 的优化空间)。\n */\nexport async function updateJsonFile<T>(\n filePath: string,\n defaults: () => T,\n mutator: (data: T) => T | Promise<T>,\n options: WriteJsonOptions = {}\n): Promise<void> {\n const { data, mtimeMs } = await readJsonFile(filePath, defaults);\n const next = await mutator(data);\n\n // 写前 mtime 检测\n if (mtimeMs !== null) {\n let currentMtime: number | null = null;\n try {\n const s = await stat(filePath);\n currentMtime = s.mtimeMs;\n } catch {\n currentMtime = null;\n }\n if (currentMtime !== null && currentMtime !== mtimeMs) {\n throw new AtomicJsonConflictError(filePath);\n }\n }\n\n await writeJsonFile(filePath, next, options);\n}\n\n/** 写前 mtime 变化——caller 可 catch + retry。 */\nexport class AtomicJsonConflictError extends AtomicJsonError {\n constructor(filePath: string) {\n super(\n `${filePath} 在 read-modify-write 期间被其他进程修改。请重试。`,\n filePath\n );\n this.name = 'AtomicJsonConflictError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nexport async function pathExists(p: string): Promise<boolean> {\n try {\n await access(p, fsConstants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  promptCredentials,
9
9
  readJsonFile,
10
10
  writeJsonFile
11
- } from "./chunk-GDSJUMK4.js";
11
+ } from "./chunk-YYQF6ZB6.js";
12
12
  import {
13
13
  ApiError,
14
14
  AuthError,
@@ -403,7 +403,7 @@ function getApiBaseUrl() {
403
403
  async function loginViaBrowser(jsonMode) {
404
404
  const baseUrl = getApiBaseUrl();
405
405
  const state = randomBytes(16).toString("hex");
406
- return new Promise((resolve23, reject) => {
406
+ return new Promise((resolve24, reject) => {
407
407
  let authSaved = false;
408
408
  const rejectLogin = (error) => {
409
409
  clearAuthConfig();
@@ -506,7 +506,7 @@ async function loginViaBrowser(jsonMode) {
506
506
  );
507
507
  }
508
508
  }
509
- resolve23();
509
+ resolve24();
510
510
  } catch (err) {
511
511
  server.close();
512
512
  const authError = err instanceof AuthError ? err : new AuthError(
@@ -4207,7 +4207,7 @@ function registerMcpCommand(program) {
4207
4207
  "Set credential values (e.g. --set TOKEN=xxx KEY=yyy)"
4208
4208
  ).action(async (mcpId, opts) => {
4209
4209
  const format = resolveFormat(program.opts());
4210
- const { runMcpInstall, printMcpInstallSummary } = await import("./install-KY7BF54H.js");
4210
+ const { runMcpInstall, printMcpInstallSummary } = await import("./install-IH2VPYXH.js");
4211
4211
  let setValues;
4212
4212
  if (opts.set) {
4213
4213
  setValues = {};
@@ -4268,7 +4268,7 @@ function registerMcpCommand(program) {
4268
4268
  "Comma-separated targets: claude-desktop,claude-code (default: both)"
4269
4269
  ).action(async (mcpId, opts) => {
4270
4270
  const format = resolveFormat(program.opts());
4271
- const { runMcpUninstall, printMcpUninstallSummary } = await import("./install-KY7BF54H.js");
4271
+ const { runMcpUninstall, printMcpUninstallSummary } = await import("./install-IH2VPYXH.js");
4272
4272
  try {
4273
4273
  const result = await runMcpUninstall({
4274
4274
  mcpId,
@@ -9096,11 +9096,12 @@ async function runInstall(input) {
9096
9096
  missingMeta[s.key] = {
9097
9097
  type: isSecret ? "secret" : "text",
9098
9098
  required: true,
9099
- helpUrl: s.helpUrl
9099
+ helpUrl: s.helpUrl,
9100
+ description: s.description
9100
9101
  };
9101
9102
  }
9102
9103
  }
9103
- const prompted = Object.keys(missingMeta).length > 0 ? await promptCredentials(missingMeta) : {};
9104
+ const prompted = Object.keys(missingMeta).length > 0 ? await promptCredentials(missingMeta, { secretFlag: "--secret" }) : {};
9104
9105
  secrets = { ...prompted, ...presetSecrets };
9105
9106
  }
9106
9107
  try {
@@ -10045,7 +10046,7 @@ function getReskillInvocation() {
10045
10046
  };
10046
10047
  }
10047
10048
  function runReskill(args) {
10048
- return new Promise((resolve23, reject) => {
10049
+ return new Promise((resolve24, reject) => {
10049
10050
  const reskill = getReskillInvocation();
10050
10051
  const child = spawn2(reskill.command, [...reskill.args, ...args], {
10051
10052
  env: createReskillEnv(),
@@ -10055,10 +10056,10 @@ function runReskill(args) {
10055
10056
  child.on("close", (code, signal) => {
10056
10057
  if (signal) {
10057
10058
  output.error(`reskill exited from signal ${signal}`);
10058
- resolve23(1);
10059
+ resolve24(1);
10059
10060
  return;
10060
10061
  }
10061
- resolve23(code ?? 1);
10062
+ resolve24(code ?? 1);
10062
10063
  });
10063
10064
  });
10064
10065
  }
@@ -10158,13 +10159,13 @@ async function readStdinIfPiped() {
10158
10159
  if (process.stdin.isTTY) {
10159
10160
  return null;
10160
10161
  }
10161
- return new Promise((resolve23, reject) => {
10162
+ return new Promise((resolve24, reject) => {
10162
10163
  const chunks = [];
10163
10164
  process.stdin.on("data", (chunk) => {
10164
10165
  chunks.push(chunk);
10165
10166
  });
10166
10167
  process.stdin.on("end", () => {
10167
- resolve23(Buffer.concat(chunks).toString("utf-8").trim());
10168
+ resolve24(Buffer.concat(chunks).toString("utf-8").trim());
10168
10169
  });
10169
10170
  process.stdin.on("error", reject);
10170
10171
  });
@@ -10940,7 +10941,7 @@ async function confirmPublish(args) {
10940
10941
  return answer.trim().toLowerCase().startsWith("y");
10941
10942
  }
10942
10943
  function readOneLine() {
10943
- return new Promise((resolve23) => {
10944
+ return new Promise((resolve24) => {
10944
10945
  let acc = "";
10945
10946
  const onData = (chunk) => {
10946
10947
  acc += chunk.toString("utf-8");
@@ -10948,7 +10949,7 @@ function readOneLine() {
10948
10949
  if (nlIdx !== -1) {
10949
10950
  process.stdin.removeListener("data", onData);
10950
10951
  process.stdin.pause();
10951
- resolve23(acc.slice(0, nlIdx));
10952
+ resolve24(acc.slice(0, nlIdx));
10952
10953
  }
10953
10954
  };
10954
10955
  process.stdin.resume();
@@ -11133,6 +11134,202 @@ function registerDomainSubcommand(task, program) {
11133
11134
  });
11134
11135
  }
11135
11136
 
11137
+ // src/commands/task/env.ts
11138
+ import { resolve as resolve23 } from "path";
11139
+
11140
+ // src/util/prompt.ts
11141
+ import { createInterface } from "readline";
11142
+ function promptSecret(message) {
11143
+ return new Promise((resolve24, reject) => {
11144
+ const rl = createInterface({
11145
+ input: process.stdin,
11146
+ output: process.stdout,
11147
+ terminal: true
11148
+ });
11149
+ process.stdout.write(message);
11150
+ const rlAny = rl;
11151
+ rlAny._writeToOutput = () => {
11152
+ };
11153
+ rl.question("", (answer) => {
11154
+ rl.close();
11155
+ process.stdout.write("\n");
11156
+ resolve24(answer);
11157
+ });
11158
+ rl.on("SIGINT", () => {
11159
+ rl.close();
11160
+ process.stdout.write("\n");
11161
+ reject(new Error("Aborted"));
11162
+ });
11163
+ });
11164
+ }
11165
+
11166
+ // src/commands/task/env.ts
11167
+ var ENV_NAMES = ["preview", "production"];
11168
+ var ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]{0,127}$/;
11169
+ function isValidEnvKey(key) {
11170
+ return ENV_KEY_REGEX.test(key);
11171
+ }
11172
+ var NO_PROJECT_HINT2 = [
11173
+ "\u65E0\u6CD5\u8BC6\u522B\u5F53\u524D\u76EE\u5F55\u5BF9\u5E94\u7684 Rush \u9879\u76EE\u3002\u8BF7\u4EFB\u9009\u5176\u4E00:",
11174
+ " rush-ai task env <cmd> --project <id> \u663E\u5F0F\u6307\u5B9A\u9879\u76EE",
11175
+ " rush-ai task link \u5173\u8054\u5F53\u524D\u76EE\u5F55\u5230\u4E00\u4E2A\u9879\u76EE"
11176
+ ].join("\n");
11177
+ function resolveProjectId(opts) {
11178
+ if (opts.project) return Promise.resolve(opts.project);
11179
+ const projectPath = resolve23(opts.path ?? ".");
11180
+ const remoteUrl = isGitRepo(projectPath) ? getRemoteUrl(projectPath) : null;
11181
+ const idFromRemote = remoteUrl ? extractRushProjectId(remoteUrl) : null;
11182
+ if (idFromRemote) return Promise.resolve(idFromRemote);
11183
+ const fromEnvFile = readEnvFile(projectPath).PROJECT_ID;
11184
+ if (fromEnvFile) return Promise.resolve(fromEnvFile);
11185
+ return Promise.reject(
11186
+ new RushError(NO_PROJECT_HINT2, {}, "NO_RUSH_PROJECT", 1)
11187
+ );
11188
+ }
11189
+ function parseEnvOption(value, fallback) {
11190
+ if (value === void 0) {
11191
+ if (fallback) return fallback;
11192
+ throw new RushError("\u5185\u90E8\u9519\u8BEF:\u7F3A\u5C11 --env \u9ED8\u8BA4\u503C");
11193
+ }
11194
+ if (!ENV_NAMES.includes(value)) {
11195
+ throw new RushError(
11196
+ `Invalid --env: "${value}". \u4EC5\u652F\u6301 preview \u6216 production\u3002`,
11197
+ { value },
11198
+ "INVALID_ENV"
11199
+ );
11200
+ }
11201
+ return value;
11202
+ }
11203
+ function statusLabel(env, rev, hasVars) {
11204
+ if (!rev) return "\u672A\u914D\u7F6E";
11205
+ if (rev.lastSyncError) return "\u540C\u6B65\u5931\u8D25";
11206
+ if (env === "production" && rev.pending) return "\u5F85\u90E8\u7F72";
11207
+ if (rev.pending) return "\u5F85\u540C\u6B65";
11208
+ if (rev.appliedAt && hasVars) return "\u5DF2\u751F\u6548";
11209
+ return "\u672A\u914D\u7F6E";
11210
+ }
11211
+ function syncMessage(sync) {
11212
+ switch (sync?.status) {
11213
+ case "applied":
11214
+ return "\u5DF2\u751F\u6548";
11215
+ case "applying":
11216
+ return "\u5DF2\u4FDD\u5B58,\u6B63\u5728\u5E94\u7528\u5230\u9884\u89C8\u2026";
11217
+ case "pending":
11218
+ return "\u5DF2\u4FDD\u5B58,\u5C06\u5728\u4E0B\u6B21\u7EBF\u4E0A\u90E8\u7F72\u751F\u6548";
11219
+ case "failed":
11220
+ return `\u5DF2\u4FDD\u5B58,\u9884\u89C8\u540C\u6B65\u5931\u8D25${sync.error ? `:${sync.error}` : ""}`;
11221
+ default:
11222
+ return "\u5DF2\u4FDD\u5B58";
11223
+ }
11224
+ }
11225
+ function renderSection(env, vars, rev, format) {
11226
+ const keys = Object.keys(vars);
11227
+ const label = statusLabel(env, rev, keys.length > 0);
11228
+ output.log(`${env.toUpperCase()} (${label})`);
11229
+ if (keys.length === 0) {
11230
+ output.log(" (\u6682\u65E0\u73AF\u5883\u53D8\u91CF)");
11231
+ output.newline();
11232
+ return;
11233
+ }
11234
+ const rows = keys.map((key) => ({
11235
+ KEY: key,
11236
+ VALUE: vars[key].maskedValue || "(empty)",
11237
+ UPDATED: vars[key].updatedAt ?? ""
11238
+ }));
11239
+ output.log(formatOutput(rows, format));
11240
+ output.newline();
11241
+ }
11242
+ async function setValue(argValue, key) {
11243
+ if (argValue !== void 0) return argValue;
11244
+ const piped = await readStdinIfPiped();
11245
+ if (piped !== null) return piped;
11246
+ return promptSecret(`Enter value for ${key}: `);
11247
+ }
11248
+ function registerEnvSubcommand(task, program) {
11249
+ const env = task.command("env").description("\u7BA1\u7406\u9879\u76EE\u73AF\u5883\u53D8\u91CF(preview / production)");
11250
+ env.command("list").alias("ls").description("\u5217\u51FA\u9879\u76EE\u73AF\u5883\u53D8\u91CF(\u8131\u654F)").option("--env <env>", "\u53EA\u770B\u67D0\u4E2A\u73AF\u5883:preview | production(\u9ED8\u8BA4\u5168\u90E8)").option("--project <id>", "\u663E\u5F0F\u6307\u5B9A\u9879\u76EE(\u9ED8\u8BA4\u81EA\u52A8\u8BC6\u522B)").action(async (opts) => {
11251
+ requireAuth();
11252
+ const format = resolveFormat(program.opts());
11253
+ const projectId = await resolveProjectId(opts);
11254
+ const client = createClient();
11255
+ const { data } = await client.get(`/api/projects/${encodeURIComponent(projectId)}/env-vars`);
11256
+ const overview = data.data;
11257
+ if (format === "json") {
11258
+ output.log(JSON.stringify(overview, null, 2));
11259
+ return;
11260
+ }
11261
+ const filter = opts.env ? parseEnvOption(opts.env) : void 0;
11262
+ const tableFormat = format === "csv" ? "csv" : "table";
11263
+ for (const name of ENV_NAMES) {
11264
+ if (filter && filter !== name) continue;
11265
+ renderSection(
11266
+ name,
11267
+ overview[name] ?? {},
11268
+ overview.revisions?.[name],
11269
+ tableFormat
11270
+ );
11271
+ }
11272
+ });
11273
+ env.command("set").description("\u65B0\u589E/\u66F4\u65B0\u4E00\u4E2A\u73AF\u5883\u53D8\u91CF").argument("<KEY>", "\u53D8\u91CF\u540D(UPPER_SNAKE_CASE)").argument("[value]", "\u53D8\u91CF\u503C(\u7701\u7565\u5219\u4ECE stdin \u6216\u4EA4\u4E92\u5F0F\u8BFB\u53D6)").option("--env <env>", "\u76EE\u6807\u73AF\u5883:preview | production", "preview").option("--project <id>", "\u663E\u5F0F\u6307\u5B9A\u9879\u76EE(\u9ED8\u8BA4\u81EA\u52A8\u8BC6\u522B)").action(
11274
+ async (key, value, opts) => {
11275
+ requireAuth();
11276
+ const format = resolveFormat(program.opts());
11277
+ const environment = parseEnvOption(opts.env, "preview");
11278
+ if (!isValidEnvKey(key)) {
11279
+ throw new RushError(
11280
+ `Invalid key: "${key}". \u9700\u5339\u914D ^[A-Z][A-Z0-9_]{0,127}$(\u5927\u5199\u5B57\u6BCD\u5F00\u5934,\u5927\u5199/\u6570\u5B57/\u4E0B\u5212\u7EBF)\u3002`,
11281
+ { key },
11282
+ "INVALID_ENV_KEY"
11283
+ );
11284
+ }
11285
+ const resolved = await setValue(value, key);
11286
+ if (!resolved) {
11287
+ throw new RushError("\u503C\u4E0D\u80FD\u4E3A\u7A7A", { key }, "EMPTY_ENV_VALUE");
11288
+ }
11289
+ const projectId = await resolveProjectId(opts);
11290
+ const client = createClient();
11291
+ const { data } = await client.put(`/api/projects/${encodeURIComponent(projectId)}/env-vars`, {
11292
+ environment,
11293
+ set: { [key]: resolved }
11294
+ });
11295
+ if (format === "json") {
11296
+ output.log(
11297
+ JSON.stringify({ success: true, sync: data.sync }, null, 2)
11298
+ );
11299
+ return;
11300
+ }
11301
+ output.success(
11302
+ `${key} \u5DF2\u4FDD\u5B58(${environment})\u3002${syncMessage(data.sync)}`
11303
+ );
11304
+ }
11305
+ );
11306
+ env.command("rm").alias("delete").description("\u5220\u9664\u4E00\u4E2A\u73AF\u5883\u53D8\u91CF").argument("<KEY>", "\u53D8\u91CF\u540D").option("--env <env>", "\u76EE\u6807\u73AF\u5883:preview | production", "preview").option("--project <id>", "\u663E\u5F0F\u6307\u5B9A\u9879\u76EE(\u9ED8\u8BA4\u81EA\u52A8\u8BC6\u522B)").action(async (key, opts) => {
11307
+ requireAuth();
11308
+ const format = resolveFormat(program.opts());
11309
+ const environment = parseEnvOption(opts.env, "preview");
11310
+ if (!isValidEnvKey(key)) {
11311
+ throw new RushError(
11312
+ `Invalid key: "${key}".`,
11313
+ { key },
11314
+ "INVALID_ENV_KEY"
11315
+ );
11316
+ }
11317
+ const projectId = await resolveProjectId(opts);
11318
+ const client = createClient();
11319
+ const { data } = await client.put(`/api/projects/${encodeURIComponent(projectId)}/env-vars`, {
11320
+ environment,
11321
+ delete: [key]
11322
+ });
11323
+ if (format === "json") {
11324
+ output.log(JSON.stringify({ success: true, sync: data.sync }, null, 2));
11325
+ return;
11326
+ }
11327
+ output.success(
11328
+ `\u5DF2\u5220\u9664 ${key}(${environment})\u3002${syncMessage(data.sync)}`
11329
+ );
11330
+ });
11331
+ }
11332
+
11136
11333
  // src/commands/task/link.ts
11137
11334
  import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync } from "fs";
11138
11335
  import path2 from "path";
@@ -11745,6 +11942,7 @@ function registerTaskCommand(program) {
11745
11942
  registerDeploySubcommand(task, program);
11746
11943
  registerVersionsSubcommand(task, program);
11747
11944
  registerDomainSubcommand(task, program);
11945
+ registerEnvSubcommand(task, program);
11748
11946
  task.command("create").description("Create a task asynchronously").option(
11749
11947
  "-a, --agent <name>",
11750
11948
  "Agent name to execute the task (defaults to `rush`)",