@zhiman_innies/innies-codex 0.122.58 → 0.122.59
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 +74 -0
- package/bin/innies-config.js +49 -6
- package/bin/innies.js +309 -0
- package/package.json +8 -2
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]
|
package/bin/innies-config.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.122.59",
|
|
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.59-darwin-x64",
|
|
27
|
+
"@zhiman_innies/innies-codex-darwin-arm64": "0.122.59-darwin-arm64",
|
|
28
|
+
"@zhiman_innies/innies-codex-win32-x64": "0.122.59-win32-x64",
|
|
29
|
+
"@zhiman_innies/innies-codex-win32-arm64": "0.122.59-win32-arm64"
|
|
24
30
|
}
|
|
25
|
-
}
|
|
31
|
+
}
|