@zhiman_innies/innies-codex 0.122.58 → 0.122.60

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
@@ -61,6 +61,80 @@ innies --version
61
61
 
62
62
  ---
63
63
 
64
+ ## First-Run Setup (重要 · N20)
65
+
66
+ > [!WARNING]
67
+ > **首次跑 `innies` 之前,必须先配置 base_url 和 api key,否则会立即报:**
68
+ >
69
+ > ```
70
+ > ERROR: Missing environment variable: ZHIMAN_BASE_URL (or [model_providers.zhiman_35b].base_url)
71
+ > ```
72
+ >
73
+ > 这是 N20 fresh-install 阻断 — 因为模板里 base_url 是注释占位符,而 Rust provider resolver 会在 env_key 缺失时**静默回退**到 `api.openai.com/v1`(见 [`docs/n20-fresh-install-base-url-fail.md`](docs/n20-fresh-install-base-url-fail.md))。
74
+
75
+ **三种配置方式任选其一(优先级从高到低):**
76
+
77
+ ### 1. 环境变量 (推荐用于 CI / 无交互场景)
78
+
79
+ ```bash
80
+ # 私有部署 (zhiman_35b / zhiman_27b 共享 ZHIMAN_API_KEY,见模板注释)
81
+ export ZHIMAN_API_KEY="sk-..."
82
+ export ZHIMAN_BASE_URL="http://your-private-deployment/v1" # 可选,覆盖 config.toml
83
+
84
+ # 阿里云 dashscope 公网
85
+ export DASHSCOPE_API_KEY="sk-..."
86
+ export DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" # 可选
87
+ ```
88
+
89
+ `export` 到 `~/.zshrc` 或 `~/.bashrc` 后 `source` 一下即持久化。
90
+
91
+ ### 2. 编辑 `~/.inniescoder/config.toml`
92
+
93
+ ```bash
94
+ # 第一次跑会自动创建 config.toml
95
+ innies 2>&1 | head -5 # 看到 [innies-onboarding] 横幅后 Ctrl+C 中断
96
+ # 然后编辑 ~/.inniescoder/config.toml,取消下面这种行的 # 号:
97
+ # # base_url = "http://your-private-deployment/v1" # FILL IN: ...
98
+ # # env_key = "ZHIMAN_API_KEY" # FILL IN: ...
99
+ # 取消注释 + 填值后保存。env_key 是"环境变量名"(不是 key 本身),然后确保那个
100
+ # env var 已经在你的 shell 里 export 过(或写在 rc 文件里)。
101
+ ```
102
+
103
+ ### 3. Interactive 引导 (默认首次跑触发)
104
+
105
+ ```bash
106
+ innies # 不要 pipe,保持 stdin 是 TTY
107
+ ```
108
+
109
+ 首次跑会打印:
110
+
111
+ ```
112
+ =============================================================================
113
+ [innies-onboarding] 首次运行检测 (N20 fresh-install 阻断防护)
114
+ config.toml: /Users/you/.inniescoder/config.toml
115
+
116
+ 检测到以下 provider 块的 base_url 仍是占位符(注释状态):
117
+ - [model_providers.zhiman_35b] base_url = "http://your-private-deployment/v1"
118
+ - [model_providers.zhiman_27b] base_url = "http://your-private-deployment/v1"
119
+ - [model_providers.dashscope] base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
120
+
121
+ 请选择配置方式:
122
+ 1. 现在交互式填入 base_url 和 api key (推荐) — 写回 config.toml
123
+ 2. 跳过,我会自己编辑 /Users/you/.inniescoder/config.toml
124
+ 3. 退出 innies
125
+ >
126
+ ```
127
+
128
+ - **选项 1**: 逐步输入每个 provider 的 `base_url` 和 env_key 名,自动写回 `config.toml`,然后继续启动 innies。
129
+ - **选项 2**: 跳过(接受 N20 会 fail)。之后自己编辑 `~/.inniescoder/config.toml` 填值。
130
+ - **选项 3**: 退出 innies(`process.exit(0)`),让你先手动配。
131
+
132
+ **跳过引导**: 设 `export INNIES_SKIP_ONBOARDING=1` 后跑 `innies`,永远不弹引导横幅。CI / smoke 测试用。
133
+
134
+ **详见 `~/.inniescoder/config.toml` 顶部的 `⚠️ FIRST-RUN SETUP REQUIRED` 横幅注释(模板在 [`codex-cli/bin/innies-config.js`](codex-cli/bin/innies-config.js) 的 `defaultInniesConfig` 里,首次 `innies` 启动时会写入)。**
135
+
136
+ ---
137
+
64
138
  ## Provider 配置
65
139
 
66
140
  > [!IMPORTANT]
@@ -316,7 +316,50 @@ function defaultInniesConfig(catalogPath, managedDefault) {
316
316
  `# model_reasoning_effort = "low" # thinking toggle: low | medium | high | xhigh (or omit for model default)`,
317
317
  `# model_verbosity = "low" # optional: GPT-5 only (low | medium | high)`,
318
318
  ];
319
+ // N20 (2026-06-15): first-run base_url warning header. Fresh
320
+ // installs hit `ERROR: Missing environment variable: ZHIMAN_BASE_URL`
321
+ // on the very first `innies` run because the provider blocks
322
+ // below emit commented placeholders and the Rust provider resolver
323
+ // silently falls back to api.openai.com when neither env_key nor
324
+ // base_url is set. This block is intentionally a COMMENT block at
325
+ // the very top of the file (before any active `key = ...` lines)
326
+ // so it is bytewise-equal across re-inits (only the user can
327
+ // change it; re-init strips active root lines but preserves
328
+ // comment lines verbatim). The text is short enough that pasting
329
+ // it back into the TOML parser yields no errors; we verified
330
+ // TOML 1.0 compliance of the "# ..." prefix lines.
331
+ const firstRunWarningHeader = [
332
+ `# ============================================================================`,
333
+ `# ⚠️ FIRST-RUN SETUP REQUIRED (N20 / 2026-06-15)`,
334
+ `# ----------------------------------------------------------------------------`,
335
+ `# 首次跑 innies 之前,必须先配置 base_url 和 api key,否则会立即报错:`,
336
+ `# ERROR: Missing environment variable: ZHIMAN_BASE_URL (or [model_providers.zhiman_35b].base_url)`,
337
+ `#`,
338
+ `# 三种配置方式任选其一 (优先级从高到低):`,
339
+ `#`,
340
+ `# 1. 环境变量 (推荐用于 CI / 无交互场景):`,
341
+ `# export ZHIMAN_API_KEY="sk-..." # 私有 7380 + 7382 共享 ZHIMAN_API_KEY`,
342
+ `# export DASHSCOPE_API_KEY="sk-..." # 阿里云 dashscope 公网`,
343
+ `# export ZHIMAN_BASE_URL="http://your-private-deployment/v1" # 可选,优于 config.toml`,
344
+ `# export DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" # 可选`,
345
+ `#`,
346
+ `# 2. 编辑本文件:`,
347
+ `# 取消下面 [model_providers.*] 块里 base_url 和 env_key 行首的 # 号,`,
348
+ `# 填入你的实际部署地址和 api key 环境变量名(不是 key 本身)。`,
349
+ `# env_key 是"环境变量名"(例如 "ZHIMAN_API_KEY"),不是 api key 的值。`,
350
+ `#`,
351
+ `# 3. Interactive 引导 (首次跑自动触发):`,
352
+ `# 跑 \`innies\` 时会问:"检测到未填的 base_url,选 1 交互式填入"。`,
353
+ `# 选项 1: 逐步输入 base_url 和 api key,自动写回本文件;`,
354
+ `# 选项 2: 跳过,自己编辑本文件(接受 N20 会 fail);`,
355
+ `# 选项 3: 退出。`,
356
+ `#`,
357
+ `# 详见 README.md 的 "First-Run Setup" 节。`,
358
+ `# ============================================================================`,
359
+ ];
319
360
  return [
361
+ ...firstRunWarningHeader,
362
+ "",
320
363
  ...lines,
321
364
  "",
322
365
  ...rootDocumentation,
@@ -723,8 +766,8 @@ function defaultZhiman35bProviderBlock() {
723
766
  return [
724
767
  ZHIMAN_35B_PROVIDER_HEADER,
725
768
  'name = "zhiman_35b"',
726
- `# base_url = "http://your-private-deployment/v1" # FILL IN: private vLLM / OpenAI-compatible endpoint`,
727
- `# env_key = "ZHIMAN_API_KEY" # FILL IN: name of the env var holding your API key`,
769
+ `# base_url = "http://your-private-deployment/v1" # FILL IN: your private vLLM / OpenAI-compatible endpoint — do NOT keep this placeholder`,
770
+ `# env_key = "ZHIMAN_API_KEY" # FILL IN: the env var NAME holding your API key (NOT the key itself) — see file header`,
728
771
  `wire_api = "${DEFAULT_PROVIDER_WIRE_API}"`,
729
772
  `# http_headers = { "X-Custom" = "value" } # optional: literal per-request headers`,
730
773
  `# env_http_headers = { "X-Tenant" = "TENANT_ID_ENV" } # optional: headers from env-var indirection`,
@@ -750,8 +793,8 @@ function defaultZhiman27bProviderBlock() {
750
793
  return [
751
794
  ZHIMAN_27B_PROVIDER_HEADER,
752
795
  'name = "zhiman_27b"',
753
- `# base_url = "http://your-private-deployment/v1" # FILL IN: private vLLM / OpenAI-compatible endpoint`,
754
- `# env_key = "ZHIMAN_API_KEY" # FILL IN: name of the env var holding your API key`,
796
+ `# base_url = "http://your-private-deployment/v1" # FILL IN: your private vLLM / OpenAI-compatible endpoint — do NOT keep this placeholder`,
797
+ `# env_key = "ZHIMAN_API_KEY" # FILL IN: the env var NAME holding your API key (NOT the key itself) — see file header`,
755
798
  `wire_api = "${DEFAULT_PROVIDER_WIRE_API}"`,
756
799
  `# http_headers = { "X-Custom" = "value" } # optional: literal per-request headers`,
757
800
  `# env_http_headers = { "X-Tenant" = "TENANT_ID_ENV" } # optional: headers from env-var indirection`,
@@ -794,8 +837,8 @@ function defaultDashscopeProviderBlock() {
794
837
  return [
795
838
  DASHSCOPE_PROVIDER_HEADER,
796
839
  'name = "bailian"',
797
- `# base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" # FILL IN: DashScope OpenAI-compatible endpoint`,
798
- `# env_key = "DASHSCOPE_API_KEY" # FILL IN: name of the env var holding your DashScope API key`,
840
+ `# base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" # FILL IN: DashScope OpenAI-compatible endpoint — keep this default if you use the public cloud`,
841
+ `# env_key = "DASHSCOPE_API_KEY" # FILL IN: the env var NAME holding your DashScope API key (NOT the key itself) — see file header`,
799
842
  `wire_api = "${DEFAULT_PROVIDER_WIRE_API}"`,
800
843
  `# http_headers = { "X-Custom" = "value" } # optional: literal per-request headers`,
801
844
  `# env_http_headers = { "X-Tenant" = "TENANT_ID_ENV" } # optional: headers from env-var indirection`,
package/bin/innies.js CHANGED
@@ -2,11 +2,38 @@
2
2
 
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import readline from "node:readline";
5
6
 
6
7
  import { maybeRunInniesCodingRuntime } from "./innies-coding-runtime.js";
7
8
  import { ensureInniesHomeDefaults, resolveInniesHome } from "./innies-config.js";
8
9
 
9
10
  const INNIES_HOME_ENV_VAR = "INNIES_HOME";
11
+ // N20 (2026-06-15): onboarding flow skip knob. CI / smoke tests can
12
+ // set this to "1" to suppress the interactive first-run prompt. The
13
+ // default behaviour (unset or "0") is to prompt the user. We treat
14
+ // "1" / "true" / "yes" as "skip" (case-insensitive), matching the
15
+ // convention used by `shouldInstallInniesSuperpowers` in
16
+ // `innies-config.js`.
17
+ const ONBOARDING_SKIP_ENV_VAR = "INNIES_SKIP_ONBOARDING";
18
+
19
+ // N20 (2026-06-15): known base_url placeholder values that signal
20
+ // an unfilled provider block. Declared at the top of the file so it
21
+ // is initialised BEFORE the top-level `await` at line ~40 runs (the
22
+ // `await` triggers `maybeRunOnboardingFlow` which calls
23
+ // `detectPlaceholders` which references this const — TDZ would
24
+ // throw if it were declared further down).
25
+ const KNOWN_BASE_URL_PLACEHOLDERS = Object.freeze({
26
+ "http://your-private-deployment/v1": {
27
+ providerLabel: "zhiman private deployment",
28
+ defaultEnvKey: "ZHIMAN_API_KEY",
29
+ defaultBaseUrlHint: "http://your-host:port/v1",
30
+ },
31
+ "https://dashscope.aliyuncs.com/compatible-mode/v1": {
32
+ providerLabel: "dashscope public cloud",
33
+ defaultEnvKey: "DASHSCOPE_API_KEY",
34
+ defaultBaseUrlHint: "https://dashscope.aliyuncs.com/compatible-mode/v1",
35
+ },
36
+ });
10
37
 
11
38
  function isVersionRequest(args) {
12
39
  return args.length === 1 && (args[0] === "--version" || args[0] === "-V");
@@ -29,6 +56,8 @@ fs.mkdirSync(codexHome, { recursive: true });
29
56
 
30
57
  ensureInniesHomeDefaults(codexHome);
31
58
 
59
+ await maybeRunOnboardingFlow(codexHome);
60
+
32
61
  if (await maybeRunInniesCodingRuntime()) {
33
62
  process.exit(0);
34
63
  }
@@ -75,3 +104,283 @@ function writeExternalRuntimeSmokeResult(runtimeVersion) {
75
104
  "utf8",
76
105
  );
77
106
  }
107
+
108
+ // N20 (2026-06-15): interactive first-run onboarding flow.
109
+ //
110
+ // On a fresh install, `innies-config.js` writes a config.toml whose
111
+ // provider blocks contain commented `# base_url = "<placeholder>"`
112
+ // lines (zhiman_35b, zhiman_27b, dashscope). If the user then runs
113
+ // `innies` without configuring those placeholders, the Rust binary
114
+ // emits:
115
+ // ERROR: Missing environment variable: ZHIMAN_BASE_URL (or [model_providers.zhiman_35b].base_url)
116
+ // because the provider resolver silently falls back to api.openai.com
117
+ // when env_key is unset. The user reads this as a confusing "config
118
+ // broken" failure.
119
+ //
120
+ // This flow detects unfilled placeholders and offers a 3-option
121
+ // interactive prompt BEFORE the binary starts, so the user can
122
+ // either (1) fill them in via prompts, (2) skip and edit the file
123
+ // by hand, or (3) bail out. We deliberately do NOT touch any Rust
124
+ // code path here — A+C+D's whole purpose is to make the user never
125
+ // trigger the N20 path.
126
+ //
127
+ // Detection: scan the just-written config.toml for lines matching
128
+ // `/^\s*#\s*base_url\s*=\s*"<known-placeholder>"`. The placeholder
129
+ // strings are the literal values emitted by
130
+ // `defaultZhiman35bProviderBlock`, `defaultZhiman27bProviderBlock`,
131
+ // and `defaultDashscopeProviderBlock` in `innies-config.js`. We
132
+ // detect by placeholder VALUE (not by provider name) because the
133
+ // user might delete the whole block and re-add a custom one — the
134
+ // placeholder detection must remain robust to block re-ordering.
135
+ //
136
+ // Flow:
137
+ // - detectPlaceholders(configPath) -> [{providerHeader, lineNo,
138
+ // placeholder, envKeyLine, envKeyPlaceholder}] (sorted by lineNo)
139
+ // - If empty: no-op return (already configured)
140
+ // - Print banner + 3-option prompt
141
+ // - 1 (interactive fill): for each placeholder, prompt for
142
+ // base_url and api-key env-var NAME; rewrite lines in place
143
+ // - 2 (skip): print warning, return; binary will then N20-fail
144
+ // (this is the documented behaviour; user accepts it)
145
+ // - 3 (quit): process.exit(0)
146
+ //
147
+ // Re-entrancy: this flow runs on EVERY `innies` invocation but is
148
+ // effectively a no-op when no placeholders are present (the user
149
+ // already configured things, or removed the placeholder block, or
150
+ // is on a run where every provider was already filled). Re-detect
151
+ // every time is idempotent and cheap (one read of config.toml).
152
+ //
153
+ // Skipping: setting `INNIES_SKIP_ONBOARDING=1` (or "true" / "yes")
154
+ // suppresses the prompt entirely. CI / smoke tests rely on this.
155
+ //
156
+ // Failure modes: if stdin is not a TTY (e.g. running under
157
+ // `echo ... | innies`), we fall back to option 2 (skip) with a
158
+ // warning — otherwise we'd block on readline forever. The
159
+ // detection itself is purely synchronous and never throws.
160
+
161
+ function detectPlaceholders(configPath) {
162
+ if (!fs.existsSync(configPath)) {
163
+ return [];
164
+ }
165
+ const lines = fs.readFileSync(configPath, "utf8").split(/\r?\n/);
166
+ const placeholders = [];
167
+ let currentProviderHeader = null;
168
+ // Each placeholder record tracks both the commented `base_url` line
169
+ // and the matching commented `env_key` line (if any) so we can
170
+ // rewrite them in one pass.
171
+ for (let i = 0; i < lines.length; i++) {
172
+ const line = lines[i];
173
+ const trimmed = line.trim();
174
+ const headerMatch = trimmed.match(/^\[model_providers\.([^\]]+)\]$/);
175
+ if (headerMatch) {
176
+ currentProviderHeader = headerMatch[1];
177
+ continue;
178
+ }
179
+ const commentedBaseUrl = line.match(/^(\s*)#\s*base_url\s*=\s*"([^"]+)"\s*(#.*)?$/);
180
+ if (commentedBaseUrl) {
181
+ const [, indent, value, trailingComment] = commentedBaseUrl;
182
+ if (Object.prototype.hasOwnProperty.call(KNOWN_BASE_URL_PLACEHOLDERS, value)) {
183
+ placeholders.push({
184
+ lineNo: i,
185
+ providerKey: currentProviderHeader,
186
+ placeholderValue: value,
187
+ meta: KNOWN_BASE_URL_PLACEHOLDERS[value],
188
+ baseUrlIndent: indent,
189
+ baseUrlTrailingComment: trailingComment ?? "",
190
+ });
191
+ }
192
+ }
193
+ }
194
+ return placeholders;
195
+ }
196
+
197
+ function shouldSkipOnboarding() {
198
+ const v = process.env[ONBOARDING_SKIP_ENV_VAR]?.trim().toLowerCase();
199
+ return v === "1" || v === "true" || v === "yes";
200
+ }
201
+
202
+ async function promptOnce(rl, question) {
203
+ return new Promise((resolve) => {
204
+ rl.question(question, (answer) => resolve(answer ?? ""));
205
+ });
206
+ }
207
+
208
+ async function maybeRunOnboardingFlow(codexHome) {
209
+ const configPath = path.join(codexHome, "config.toml");
210
+ const placeholders = detectPlaceholders(configPath);
211
+
212
+ if (placeholders.length === 0) {
213
+ // No unfilled placeholders — user already configured (or is on a
214
+ // fresh install with env vars set). Nothing to do.
215
+ return { prompted: false, filled: 0 };
216
+ }
217
+
218
+ // Non-TTY stdin (CI, piped input, smoke tests): fall back to skip
219
+ // with a loud warning rather than blocking on readline forever.
220
+ if (!process.stdin.isTTY) {
221
+ if (shouldSkipOnboarding()) {
222
+ // Honour INNIES_SKIP_ONBOARDING — silent skip.
223
+ return { prompted: false, filled: 0 };
224
+ }
225
+ console.warn(
226
+ "[innies-onboarding] 检测到 config.toml 有未填的 base_url 占位符 " +
227
+ "(N20 fresh-install 阻断)。" +
228
+ " stdin 不是 TTY,无法交互式引导 — 继续执行(可能 N20 fail)。" +
229
+ " 三种解决方式: (1) 设置 INNIES_SKIP_ONBOARDING=1 显式跳过;" +
230
+ " (2) 编辑 " + configPath + " 填值;" +
231
+ " (3) 跑交互式 `innies` (无 pipe)。"
232
+ );
233
+ return { prompted: false, filled: 0 };
234
+ }
235
+
236
+ if (shouldSkipOnboarding()) {
237
+ return { prompted: false, filled: 0 };
238
+ }
239
+
240
+ console.log("");
241
+ console.log("=============================================================================");
242
+ console.log("[innies-onboarding] 首次运行检测 (N20 fresh-install 阻断防护)");
243
+ console.log("-----------------------------------------------------------------------------");
244
+ console.log(`config.toml: ${configPath}`);
245
+ console.log("");
246
+ console.log("检测到以下 provider 块的 base_url 仍是占位符(注释状态):");
247
+ for (const p of placeholders) {
248
+ console.log(` - [model_providers.${p.providerKey}] base_url = "${p.placeholderValue}"`);
249
+ }
250
+ console.log("");
251
+ console.log("如果直接继续执行,innies 会立即报:");
252
+ console.log(' ERROR: Missing environment variable: ZHIMAN_BASE_URL (or [model_providers.zhiman_35b].base_url)');
253
+ console.log("");
254
+ console.log("请选择配置方式:");
255
+ console.log(" 1. 现在交互式填入 base_url 和 api key (推荐) — 写回 config.toml");
256
+ console.log(" 2. 跳过,我会自己编辑 " + configPath);
257
+ console.log(" 3. 退出 innies");
258
+ console.log("");
259
+
260
+ const rl = readline.createInterface({
261
+ input: process.stdin,
262
+ output: process.stdout,
263
+ });
264
+
265
+ let choice = "";
266
+ while (choice !== "1" && choice !== "2" && choice !== "3") {
267
+ choice = (await promptOnce(rl, "> ")).trim();
268
+ if (choice !== "1" && choice !== "2" && choice !== "3") {
269
+ console.log(' 请输入 1、2 或 3。');
270
+ }
271
+ }
272
+
273
+ if (choice === "3") {
274
+ rl.close();
275
+ console.log("[innies-onboarding] 用户选择退出,不再继续 innies。");
276
+ process.exit(0);
277
+ }
278
+
279
+ if (choice === "2") {
280
+ rl.close();
281
+ console.log("[innies-onboarding] 用户选择跳过 — 继续执行 innies (可能 N20 fail)。");
282
+ return { prompted: true, filled: 0 };
283
+ }
284
+
285
+ // choice === "1": interactively fill each placeholder.
286
+ console.log("");
287
+ console.log("[innies-onboarding] 开始交互式填入。输入 'skip' 保留占位符,空行重新提示。");
288
+ console.log("");
289
+
290
+ const rewrites = [];
291
+ for (const p of placeholders) {
292
+ const label = p.meta.providerLabel;
293
+ console.log(`--- [model_providers.${p.providerKey}] (${label}) ---`);
294
+ console.log(` 提示: ${label} 默认 base_url = ${p.meta.defaultBaseUrlHint}`);
295
+ console.log(` 提示: ${label} 默认 env_key 名 = ${p.meta.defaultEnvKey}`);
296
+ console.log(` (输入 'skip' 跳过本块,留空会重新提示)`);
297
+
298
+ const baseUrl = await promptUntilFilledOrSkip(
299
+ rl,
300
+ ` Enter base_url: `,
301
+ { allowSkip: true }
302
+ );
303
+ if (baseUrl === "__SKIP__") {
304
+ console.log(` -> 保留占位符 (${p.placeholderValue})`);
305
+ continue;
306
+ }
307
+
308
+ const envKeyName = await promptUntilFilledOrSkip(
309
+ rl,
310
+ ` Enter env var name holding API key: `,
311
+ { allowSkip: true }
312
+ );
313
+ if (envKeyName === "__SKIP__") {
314
+ console.log(` -> 保留 base_url 占位符`);
315
+ continue;
316
+ }
317
+
318
+ rewrites.push({
319
+ lineNo: p.lineNo,
320
+ indent: p.baseUrlIndent,
321
+ baseUrl,
322
+ envKey: envKeyName,
323
+ trailingComment: p.baseUrlTrailingComment,
324
+ });
325
+ console.log(` -> base_url = "${baseUrl}", env_key = "${envKeyName}"`);
326
+ }
327
+
328
+ rl.close();
329
+
330
+ if (rewrites.length === 0) {
331
+ console.log("");
332
+ console.log("[innies-onboarding] 全部跳过 — 继续执行 innies (可能 N20 fail)。");
333
+ return { prompted: true, filled: 0 };
334
+ }
335
+
336
+ // Atomic write: rewrite the file in one fs.writeFileSync call. We
337
+ // re-read the file (the placeholders are line-anchored so concurrent
338
+ // edits are vanishingly unlikely in a single-user onboarding flow)
339
+ // and replace each commented base_url line with an active `key = "..."`
340
+ // line + an active env_key line immediately after.
341
+ const original = fs.readFileSync(configPath, "utf8");
342
+ const originalLines = original.split(/\r?\n/);
343
+ // Sort rewrites in DESCENDING lineNo so splice() doesn't shift the
344
+ // remaining indexes.
345
+ rewrites.sort((a, b) => b.lineNo - a.lineNo);
346
+ for (const r of rewrites) {
347
+ // The detection regex captures `# FILL IN: ...` WITHOUT the
348
+ // leading space (the regex is `^(\s*)#\s*base_url\s*=...` and the
349
+ // trailing-comment group `(#.*)?$` matches from the `#` itself).
350
+ // Re-insert the separator so we don't produce lines like
351
+ // `base_url = "x"# FILL IN: ...` — always exactly one space.
352
+ const trailingSep = r.trailingComment && !r.trailingComment.startsWith(" ") ? " " : "";
353
+ const baseUrlLine = `${r.indent}base_url = ${JSON.stringify(r.baseUrl)}${trailingSep}${r.trailingComment}`;
354
+ const envKeyLine = `${r.indent}env_key = ${JSON.stringify(r.envKey)}${trailingSep}${r.trailingComment}`;
355
+ originalLines.splice(r.lineNo, 1, baseUrlLine, envKeyLine);
356
+ }
357
+ const updated = originalLines.join("\n");
358
+ // Atomic write: write to a temp file then rename. `rename` is
359
+ // atomic on POSIX when src and dst are on the same filesystem,
360
+ // which they are here (both inside `codexHome`).
361
+ const tmpPath = `${configPath}.tmp-${process.pid}`;
362
+ fs.writeFileSync(tmpPath, updated, "utf8");
363
+ fs.renameSync(tmpPath, configPath);
364
+
365
+ console.log("");
366
+ console.log(`[innies-onboarding] 已写回 ${rewrites.length} 个 provider 配置到 ${configPath}`);
367
+ console.log("[innies-onboarding] 继续启动 innies...");
368
+ return { prompted: true, filled: rewrites.length };
369
+ }
370
+
371
+ async function promptUntilFilledOrSkip(rl, question, { allowSkip, defaultValue }) {
372
+ while (true) {
373
+ const answer = (await promptOnce(rl, question)).trim();
374
+ if (allowSkip && answer.toLowerCase() === "skip") {
375
+ return "__SKIP__";
376
+ }
377
+ if (answer === "" && defaultValue) {
378
+ return defaultValue;
379
+ }
380
+ if (answer === "") {
381
+ console.log(" (不允许空,重新输入,或 'skip' 跳过)");
382
+ continue;
383
+ }
384
+ return answer;
385
+ }
386
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhiman_innies/innies-codex",
3
- "version": "0.122.58",
3
+ "version": "0.122.60",
4
4
  "license": "Apache-2.0",
5
5
  "bin": {
6
6
  "innies": "bin/innies.js"
@@ -21,5 +21,11 @@
21
21
  "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc",
22
22
  "scripts": {
23
23
  "postinstall": "node bin/innies-init.js"
24
+ },
25
+ "optionalDependencies": {
26
+ "@zhiman_innies/innies-codex-darwin-x64": "0.122.60-darwin-x64",
27
+ "@zhiman_innies/innies-codex-darwin-arm64": "0.122.60-darwin-arm64",
28
+ "@zhiman_innies/innies-codex-win32-x64": "0.122.60-win32-x64",
29
+ "@zhiman_innies/innies-codex-win32-arm64": "0.122.60-win32-arm64"
24
30
  }
25
- }
31
+ }