rush-ai 0.13.0 → 0.14.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.
@@ -1,5 +1,68 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/commands/mcp/prompt-utils.ts
4
+ function readOneLine() {
5
+ return new Promise((resolve) => {
6
+ let acc = "";
7
+ const onData = (chunk) => {
8
+ acc += chunk.toString("utf-8");
9
+ const nlIdx = acc.indexOf("\n");
10
+ if (nlIdx !== -1) {
11
+ process.stdin.removeListener("data", onData);
12
+ process.stdin.pause();
13
+ resolve(acc.slice(0, nlIdx));
14
+ }
15
+ };
16
+ const onEnd = () => {
17
+ process.stdin.removeListener("data", onData);
18
+ resolve(acc);
19
+ };
20
+ process.stdin.resume();
21
+ process.stdin.on("data", onData);
22
+ process.stdin.once("end", onEnd);
23
+ });
24
+ }
25
+ async function promptCredentials(extraConfigMeta, options) {
26
+ const values = {};
27
+ const isTTY = Boolean(process.stdin.isTTY);
28
+ const skipPrompt = options?.yes || !isTTY;
29
+ for (const [key, meta] of Object.entries(extraConfigMeta)) {
30
+ if (!meta.required) {
31
+ if (meta.defaultValue) {
32
+ values[key] = meta.defaultValue;
33
+ }
34
+ continue;
35
+ }
36
+ if (skipPrompt) {
37
+ if (meta.defaultValue) {
38
+ values[key] = meta.defaultValue;
39
+ } else {
40
+ 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}` : "")
42
+ );
43
+ }
44
+ continue;
45
+ }
46
+ 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} `);
51
+ const input = await readOneLine();
52
+ const trimmed = input.trim();
53
+ if (trimmed) {
54
+ values[key] = trimmed;
55
+ } else if (meta.defaultValue) {
56
+ values[key] = meta.defaultValue;
57
+ } else {
58
+ throw new Error(
59
+ `Credential "${key}" is required but was not provided.` + (meta.helpUrl ? ` Help: ${meta.helpUrl}` : "")
60
+ );
61
+ }
62
+ }
63
+ return values;
64
+ }
65
+
3
66
  // src/installers/claude-code/atomic-json.ts
4
67
  import { randomUUID } from "crypto";
5
68
  import { constants as fsConstants } from "fs";
@@ -92,6 +155,7 @@ export {
92
155
  readJsonFile,
93
156
  writeJsonFile,
94
157
  AtomicJsonConflictError,
95
- pathExists
158
+ pathExists,
159
+ promptCredentials
96
160
  };
97
- //# sourceMappingURL=chunk-V7P2TZZ4.js.map
161
+ //# sourceMappingURL=chunk-GDSJUMK4.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 type?: 'text' | 'secret';\n required?: boolean;\n defaultValue?: string;\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?: { yes?: boolean }\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 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 throw new Error(\n `Credential \"${key}\" is required but no default value is available. ` +\n `Run interactively or provide credentials via the Rush web UI.` +\n (meta.helpUrl ? ` Help: ${meta.helpUrl}` : '')\n );\n }\n continue;\n }\n\n // Interactive prompt\n const typeHint = meta.type === 'secret' ? ' (secret)' : '';\n const defaultHint = meta.defaultValue ? ` [${meta.defaultValue}]` : '';\n const helpHint = meta.helpUrl ? `\\n Help: ${meta.helpUrl}` : '';\n process.stderr.write(` ${key}${typeHint}${defaultHint}:${helpHint} `);\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 \"${key}\" is required but was not provided.` +\n (meta.helpUrl ? ` Help: ${meta.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":[]}