@z0ne/installer 0.1.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/LICENSE +21 -0
- package/README.md +57 -0
- package/bin/cli.js +9 -0
- package/lib/api.js +59 -0
- package/lib/fsutil.js +88 -0
- package/lib/main.js +281 -0
- package/lib/toml.js +114 -0
- package/lib/tools/claude.js +112 -0
- package/lib/tools/cline.js +60 -0
- package/lib/tools/codex.js +136 -0
- package/lib/tools/hermes.js +71 -0
- package/lib/tools/openclaw.js +117 -0
- package/lib/tools/opencode.js +76 -0
- package/lib/ui.js +208 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 z0ne.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @z0ne/installer
|
|
2
|
+
|
|
3
|
+
Point coding agents at [z0ne.ai](https://z0ne.ai) — one API key, every model.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx @z0ne/installer
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The wizard validates your key against the live gateway, detects which tools
|
|
10
|
+
you have installed, lets you pick models from the live catalog, and writes
|
|
11
|
+
each tool's config — merging with what you already have, never clobbering it.
|
|
12
|
+
|
|
13
|
+
## Supported tools
|
|
14
|
+
|
|
15
|
+
| Tool | What gets written |
|
|
16
|
+
| ----------- | ------------------------------------------------------------- |
|
|
17
|
+
| Claude Code | `~/.claude/settings.json` (Anthropic wire format, model slots)|
|
|
18
|
+
| Codex CLI | `~/.codex/config.toml` + `auth.json` + token helper |
|
|
19
|
+
| OpenCode | `~/.config/opencode/opencode.json` (openai-compatible provider)|
|
|
20
|
+
| Cline | `~/.cline/data/globalState.json` + `secrets.json` |
|
|
21
|
+
| Hermes TaskCreate| `~/.hermes/config.yaml` + `~/.hermes/.env` |
|
|
22
|
+
| OpenClaw | `~/.openclaw/openclaw.json` (+ per-agent `models.json`) |
|
|
23
|
+
|
|
24
|
+
Optionally installs a **statusline for Claude Code** (model · session cost ·
|
|
25
|
+
project dir) — rendered from local session data only, no network calls.
|
|
26
|
+
|
|
27
|
+
## Non-interactive use
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx @z0ne/installer --key sk-... --tools claude,codex --yes
|
|
31
|
+
npx @z0ne/installer --tools codex --model gpt-5.6-sol --yes # key from Z0NE_API_KEY
|
|
32
|
+
npx @z0ne/installer uninstall --tools claude,codex --yes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run `npx @z0ne/installer --help` for all flags.
|
|
36
|
+
|
|
37
|
+
## Safety
|
|
38
|
+
|
|
39
|
+
- Every file the installer changes is backed up first to
|
|
40
|
+
`<file>.bak.<timestamp>` next to the original.
|
|
41
|
+
- Configs are merged, not replaced: your other providers, comments
|
|
42
|
+
(Codex TOML), and unrelated settings survive.
|
|
43
|
+
- The API key is stored only where each tool itself expects it, with
|
|
44
|
+
owner-only permissions where the platform supports it.
|
|
45
|
+
- `uninstall` removes exactly what `install` added.
|
|
46
|
+
|
|
47
|
+
## Windows note (Codex)
|
|
48
|
+
|
|
49
|
+
Codex on Windows can only spawn `.cmd`/`.exe` auth helpers — a `.sh` helper
|
|
50
|
+
fails with os error 193 and Codex silently skips fetching the live model
|
|
51
|
+
catalog, leaving only its bundled models in the picker. The installer writes
|
|
52
|
+
a `.cmd` token helper with absolute paths baked in, so the full z0ne.ai
|
|
53
|
+
catalog shows up in the Codex model picker out of the box.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT © z0ne.ai
|
package/bin/cli.js
ADDED
package/lib/api.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// z0ne.ai gateway API: key validation + live model catalog.
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BASE = "https://api.z0ne.ai";
|
|
5
|
+
|
|
6
|
+
// Normalize a user-supplied base URL to the bare origin form (no trailing
|
|
7
|
+
// slash, no /v1 suffix). Tool modules append what each agent expects.
|
|
8
|
+
function normalizeBase(raw) {
|
|
9
|
+
let base = (raw || DEFAULT_BASE).trim().replace(/\/+$/, "");
|
|
10
|
+
base = base.replace(/\/v1$/, "");
|
|
11
|
+
if (!/^https?:\/\//.test(base)) base = "https://" + base;
|
|
12
|
+
return base;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// GET /v1/models with the pasted key. Success doubles as key validation.
|
|
16
|
+
// Returns the enriched data rows: {id, vendor, context_length, input_modalities, ...}
|
|
17
|
+
async function fetchModels(base, key) {
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await fetch(base + "/v1/models", {
|
|
21
|
+
headers: { Authorization: "Bearer " + key, "User-Agent": "z0ne-installer" },
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error("could not reach " + base + " (" + error.message + ")");
|
|
25
|
+
}
|
|
26
|
+
if (res.status === 401 || res.status === 403) {
|
|
27
|
+
throw new Error("API key rejected (HTTP " + res.status + "). Get a key at https://console.z0ne.ai/keys");
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) throw new Error("gateway returned HTTP " + res.status);
|
|
30
|
+
const body = await res.json();
|
|
31
|
+
const rows = Array.isArray(body.data) ? body.data : [];
|
|
32
|
+
if (!rows.length) throw new Error("gateway returned an empty model list");
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// First id matching any pattern, in order of preference.
|
|
37
|
+
function pickDefault(ids, patterns, fallback) {
|
|
38
|
+
for (const p of patterns) {
|
|
39
|
+
const hit = ids.find((id) => p.test(id));
|
|
40
|
+
if (hit) return hit;
|
|
41
|
+
}
|
|
42
|
+
return fallback || ids[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultModels(ids) {
|
|
46
|
+
return {
|
|
47
|
+
model: pickDefault(ids, [/^gpt-5\.6-sol$/, /^gpt-5\.6/, /^gpt-/]),
|
|
48
|
+
opus: pickDefault(ids, [/^claude-opus-5$/, /^claude-fable-5$/, /^claude-opus-4-8$/, /^claude-opus/]),
|
|
49
|
+
sonnet: pickDefault(ids, [/^claude-sonnet-5$/, /^claude-sonnet-4-6$/, /^claude-sonnet/]),
|
|
50
|
+
haiku: pickDefault(ids, [/^claude-haiku-4-5$/, /^claude-haiku/]),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fmtCtx = (n) => {
|
|
55
|
+
if (!n || typeof n !== "number") return "";
|
|
56
|
+
return n >= 1000 ? Math.round(n / 1000) + "k ctx" : n + " ctx";
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = { DEFAULT_BASE, normalizeBase, fetchModels, pickDefault, defaultModels, fmtCtx };
|
package/lib/fsutil.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Filesystem + detection helpers shared by all tool modules.
|
|
3
|
+
// Every file we modify is backed up once per run to <file>.bak.<timestamp>.
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
|
|
10
|
+
const isWindows = os.platform() === "win32";
|
|
11
|
+
const home = () => os.homedir();
|
|
12
|
+
|
|
13
|
+
const runStamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
14
|
+
const backedUp = new Set();
|
|
15
|
+
|
|
16
|
+
// Copy <file> to <file>.bak.<stamp> before the first write of this run.
|
|
17
|
+
// Returns the backup path, or null when there was nothing to back up.
|
|
18
|
+
function backupFile(file) {
|
|
19
|
+
if (backedUp.has(file)) return null;
|
|
20
|
+
backedUp.add(file);
|
|
21
|
+
if (!fs.existsSync(file)) return null;
|
|
22
|
+
const bak = file + ".bak." + runStamp;
|
|
23
|
+
fs.copyFileSync(file, bak);
|
|
24
|
+
return bak;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureDir(dir) {
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readText(file) {
|
|
32
|
+
try {
|
|
33
|
+
return fs.readFileSync(file, "utf8");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === "ENOENT") return null;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse errors abort on purpose: merging on top of a file we could not parse
|
|
41
|
+
// would silently destroy the user's own configuration.
|
|
42
|
+
function readJson(file) {
|
|
43
|
+
const content = readText(file);
|
|
44
|
+
if (content === null) return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(content);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(file + " exists but is not valid JSON — fix or remove it, then re-run.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeText(file, content) {
|
|
53
|
+
ensureDir(path.dirname(file));
|
|
54
|
+
const bak = backupFile(file);
|
|
55
|
+
fs.writeFileSync(file, content.endsWith("\n") ? content : content + "\n");
|
|
56
|
+
return bak;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeJson(file, obj) {
|
|
60
|
+
return writeText(file, JSON.stringify(obj, null, 2));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `which`/`where` with %APPDATA%\npm injected on Windows so npm globals resolve.
|
|
64
|
+
function commandExists(cmd) {
|
|
65
|
+
try {
|
|
66
|
+
const probe = isWindows ? "where " + cmd : "which " + cmd;
|
|
67
|
+
const env = isWindows
|
|
68
|
+
? { ...process.env, PATH: process.env.APPDATA + "\\npm;" + process.env.PATH }
|
|
69
|
+
: process.env;
|
|
70
|
+
execSync(probe, { stdio: "pipe", windowsHide: true, env });
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
isWindows,
|
|
79
|
+
home,
|
|
80
|
+
runStamp,
|
|
81
|
+
backupFile,
|
|
82
|
+
ensureDir,
|
|
83
|
+
readText,
|
|
84
|
+
readJson,
|
|
85
|
+
writeText,
|
|
86
|
+
writeJson,
|
|
87
|
+
commandExists,
|
|
88
|
+
};
|
package/lib/main.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @z0ne/installer — wizard entry point.
|
|
3
|
+
// install (default): validate key -> pick tools -> pick models -> write configs.
|
|
4
|
+
// uninstall: remove z0ne.ai settings from selected tools.
|
|
5
|
+
|
|
6
|
+
const ui = require("./ui");
|
|
7
|
+
const { fmt } = ui;
|
|
8
|
+
const api = require("./api");
|
|
9
|
+
const { home } = require("./fsutil");
|
|
10
|
+
|
|
11
|
+
const TOOLS = [
|
|
12
|
+
require("./tools/claude"),
|
|
13
|
+
require("./tools/codex"),
|
|
14
|
+
require("./tools/opencode"),
|
|
15
|
+
require("./tools/cline"),
|
|
16
|
+
require("./tools/hermes"),
|
|
17
|
+
require("./tools/openclaw"),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const VERSION = require("../package.json").version;
|
|
21
|
+
const byId = (id) => TOOLS.find((t) => t.id === id);
|
|
22
|
+
const shortHome = (p) => p.replace(home(), "~").replace(/\\/g, "/");
|
|
23
|
+
|
|
24
|
+
const HELP = `
|
|
25
|
+
npx @z0ne/installer [uninstall] [options]
|
|
26
|
+
|
|
27
|
+
Points coding agents at the z0ne.ai gateway. Interactive by default;
|
|
28
|
+
every flag below makes one step non-interactive.
|
|
29
|
+
|
|
30
|
+
options
|
|
31
|
+
--key <sk-...> z0ne.ai API key (or env Z0NE_API_KEY)
|
|
32
|
+
--base-url <url> gateway base (default https://api.z0ne.ai)
|
|
33
|
+
--tools <a,b,c> ${TOOLS.map((t) => t.id).join(", ")}
|
|
34
|
+
--model <id> default model for OpenAI-format tools
|
|
35
|
+
--opus <id> Claude Code opus-slot model
|
|
36
|
+
--sonnet <id> Claude Code sonnet-slot model
|
|
37
|
+
--haiku <id> Claude Code haiku-slot model
|
|
38
|
+
--statusline install the Claude Code statusline without asking
|
|
39
|
+
--no-statusline skip the Claude Code statusline without asking
|
|
40
|
+
--yes skip the final confirmation
|
|
41
|
+
--help, --version
|
|
42
|
+
|
|
43
|
+
Every file the installer changes is backed up first to
|
|
44
|
+
<file>.bak.<timestamp>. Undo anytime: npx @z0ne/installer uninstall
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const VALUED_FLAGS = ["key", "k", "base-url", "tools", "model", "opus", "sonnet", "haiku"];
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const flags = { _: [] };
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const a = argv[i];
|
|
53
|
+
if (!a.startsWith("-")) {
|
|
54
|
+
flags._.push(a);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const eq = a.indexOf("=");
|
|
58
|
+
const name = (eq === -1 ? a : a.slice(0, eq)).replace(/^--?/, "");
|
|
59
|
+
let val = eq === -1 ? true : a.slice(eq + 1);
|
|
60
|
+
if (val === true && VALUED_FLAGS.includes(name) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("-")) {
|
|
61
|
+
val = argv[++i];
|
|
62
|
+
}
|
|
63
|
+
flags[name] = val;
|
|
64
|
+
}
|
|
65
|
+
return flags;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function banner() {
|
|
69
|
+
ui.log();
|
|
70
|
+
ui.log(" " + fmt.bold("Z0NE.AI") + fmt.gray(" installer v" + VERSION));
|
|
71
|
+
ui.log(fmt.gray(" one key, every model - api.z0ne.ai"));
|
|
72
|
+
ui.log();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hintOf = (row) => {
|
|
76
|
+
if (!row) return "";
|
|
77
|
+
const bits = [];
|
|
78
|
+
if (row.vendor) bits.push(row.vendor);
|
|
79
|
+
const ctx = api.fmtCtx(row.context_length);
|
|
80
|
+
if (ctx) bits.push(ctx);
|
|
81
|
+
return bits.join(" - ");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function parseToolsFlag(raw) {
|
|
85
|
+
const toolIds = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
86
|
+
const bad = toolIds.filter((id) => !byId(id));
|
|
87
|
+
if (bad.length) {
|
|
88
|
+
throw new Error("unknown tool(s): " + bad.join(", ") + " (valid: " + TOOLS.map((t) => t.id).join(", ") + ")");
|
|
89
|
+
}
|
|
90
|
+
return toolIds;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveKeyAndModels(flags) {
|
|
94
|
+
const base = api.normalizeBase(
|
|
95
|
+
(typeof flags["base-url"] === "string" && flags["base-url"]) || process.env.Z0NE_BASE_URL
|
|
96
|
+
);
|
|
97
|
+
let key =
|
|
98
|
+
(typeof flags.key === "string" && flags.key) ||
|
|
99
|
+
(typeof flags.k === "string" && flags.k) ||
|
|
100
|
+
process.env.Z0NE_API_KEY ||
|
|
101
|
+
"";
|
|
102
|
+
|
|
103
|
+
for (let attempt = 0; ; attempt++) {
|
|
104
|
+
if (!key) {
|
|
105
|
+
key = await ui.askHidden("Paste your z0ne.ai API key (from https://console.z0ne.ai/keys):");
|
|
106
|
+
if (!key) throw new Error("no API key provided (use --key or Z0NE_API_KEY)");
|
|
107
|
+
}
|
|
108
|
+
ui.info("validating key against " + base + " ...");
|
|
109
|
+
try {
|
|
110
|
+
const rows = await api.fetchModels(base, key);
|
|
111
|
+
ui.ok("API key valid - " + rows.length + " models available");
|
|
112
|
+
return { base, key, rows };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
ui.err(error.message);
|
|
115
|
+
if (!ui.isInteractive() || attempt >= 2) throw new Error("could not validate the API key");
|
|
116
|
+
key = "";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function install(flags) {
|
|
122
|
+
const { base, key, rows } = await resolveKeyAndModels(flags);
|
|
123
|
+
const ids = rows.map((r) => r.id);
|
|
124
|
+
const rowsById = Object.fromEntries(rows.map((r) => [r.id, r]));
|
|
125
|
+
const defs = api.defaultModels(ids);
|
|
126
|
+
const modelItems = ids.map((id) => ({ value: id, label: id, hint: hintOf(rowsById[id]) }));
|
|
127
|
+
|
|
128
|
+
// 1. which tools
|
|
129
|
+
let toolIds;
|
|
130
|
+
if (typeof flags.tools === "string") {
|
|
131
|
+
toolIds = parseToolsFlag(flags.tools);
|
|
132
|
+
} else {
|
|
133
|
+
const detected = TOOLS.filter((t) => t.detect()).map((t) => t.id);
|
|
134
|
+
const items = TOOLS.map((t) => ({
|
|
135
|
+
value: t.id,
|
|
136
|
+
label: t.title,
|
|
137
|
+
hint: (detected.includes(t.id) ? "detected - " : "") + t.hint,
|
|
138
|
+
}));
|
|
139
|
+
ui.log();
|
|
140
|
+
toolIds = await ui.selectMany(
|
|
141
|
+
"Which tools should use z0ne.ai?",
|
|
142
|
+
items,
|
|
143
|
+
detected.length ? detected : TOOLS.map((t) => t.id)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (!toolIds.length) {
|
|
147
|
+
ui.warn("nothing selected - exiting.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. default model for OpenAI-format tools
|
|
152
|
+
const openaiTools = toolIds.filter((id) => id !== "claude");
|
|
153
|
+
let model = typeof flags.model === "string" ? flags.model : defs.model;
|
|
154
|
+
if (openaiTools.length && typeof flags.model !== "string" && ui.isInteractive()) {
|
|
155
|
+
ui.log();
|
|
156
|
+
model = await ui.selectOne("Default model (" + openaiTools.join(", ") + ")", modelItems, defs.model);
|
|
157
|
+
}
|
|
158
|
+
if (model && !ids.includes(model)) {
|
|
159
|
+
ui.warn('model "' + model + '" is not in the live catalog - using it anyway');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 3. Claude Code model slots
|
|
163
|
+
let opus = typeof flags.opus === "string" ? flags.opus : defs.opus;
|
|
164
|
+
let sonnet = typeof flags.sonnet === "string" ? flags.sonnet : defs.sonnet;
|
|
165
|
+
let haiku = typeof flags.haiku === "string" ? flags.haiku : defs.haiku;
|
|
166
|
+
const slotsFlagged = ["opus", "sonnet", "haiku"].some((f) => typeof flags[f] === "string");
|
|
167
|
+
if (toolIds.includes("claude") && !slotsFlagged && ui.isInteractive()) {
|
|
168
|
+
ui.log();
|
|
169
|
+
const keep = await ui.confirm(
|
|
170
|
+
"Claude Code slots: opus=" + opus + ", sonnet=" + sonnet + ", haiku=" + haiku + ". Use these?",
|
|
171
|
+
true
|
|
172
|
+
);
|
|
173
|
+
if (!keep) {
|
|
174
|
+
opus = await ui.selectOne("Opus-slot model", modelItems, opus);
|
|
175
|
+
sonnet = await ui.selectOne("Sonnet-slot model", modelItems, sonnet);
|
|
176
|
+
haiku = await ui.selectOne("Haiku-slot model", modelItems, haiku);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 4. statusline (Claude Code only)
|
|
181
|
+
let statusline = false;
|
|
182
|
+
if (toolIds.includes("claude")) {
|
|
183
|
+
if (flags.statusline === true) statusline = true;
|
|
184
|
+
else if (flags["no-statusline"] === true) statusline = false;
|
|
185
|
+
else statusline = await ui.confirm("Install the z0ne statusline for Claude Code?", true);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 5. summary + confirm
|
|
189
|
+
ui.log();
|
|
190
|
+
ui.log(" " + fmt.bold("About to configure"));
|
|
191
|
+
for (const id of toolIds) {
|
|
192
|
+
const t = byId(id);
|
|
193
|
+
const detail =
|
|
194
|
+
id === "claude"
|
|
195
|
+
? "opus=" + opus + " sonnet=" + sonnet + " haiku=" + haiku + (statusline ? " +statusline" : "")
|
|
196
|
+
: "model=" + model;
|
|
197
|
+
ui.info(t.title + " " + fmt.gray(t.hint + " - " + detail));
|
|
198
|
+
}
|
|
199
|
+
ui.info("gateway " + base + fmt.gray(" - changed files are backed up to *.bak.<timestamp>"));
|
|
200
|
+
if (flags.yes !== true && !(await ui.confirm("Proceed?", true))) {
|
|
201
|
+
ui.warn("aborted - nothing written.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 6. apply
|
|
206
|
+
const ctx = { base, key, model, opus, sonnet, haiku, statusline, rowsById };
|
|
207
|
+
ui.log();
|
|
208
|
+
let failed = 0;
|
|
209
|
+
for (const id of toolIds) {
|
|
210
|
+
const t = byId(id);
|
|
211
|
+
try {
|
|
212
|
+
const res = t.apply(ctx);
|
|
213
|
+
ui.ok(t.title + " -> " + res.paths.map(shortHome).join(", "));
|
|
214
|
+
for (const n of res.notes) ui.warn(t.title + ": " + n);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
failed++;
|
|
217
|
+
ui.err(t.title + ": " + error.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
ui.log();
|
|
222
|
+
if (failed) ui.warn(failed + " tool(s) failed - see above. The rest are configured.");
|
|
223
|
+
else ui.ok(fmt.bold("Done.") + " Open a new terminal so fresh configs are picked up.");
|
|
224
|
+
const tryCmds = toolIds
|
|
225
|
+
.map((id) => ({ claude: "claude", codex: "codex", opencode: "opencode", cline: "cline", hermes: "hermes", openclaw: "openclaw" })[id])
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
if (tryCmds.length) ui.info("try it: " + tryCmds.join(" / "));
|
|
228
|
+
ui.info("undo anytime: npx @z0ne/installer uninstall");
|
|
229
|
+
if (failed) process.exitCode = 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function uninstall(flags) {
|
|
233
|
+
let toolIds;
|
|
234
|
+
if (typeof flags.tools === "string") {
|
|
235
|
+
toolIds = parseToolsFlag(flags.tools);
|
|
236
|
+
} else {
|
|
237
|
+
const detected = TOOLS.filter((t) => t.detect()).map((t) => t.id);
|
|
238
|
+
const items = TOOLS.map((t) => ({
|
|
239
|
+
value: t.id,
|
|
240
|
+
label: t.title,
|
|
241
|
+
hint: (detected.includes(t.id) ? "detected - " : "") + t.hint,
|
|
242
|
+
}));
|
|
243
|
+
toolIds = await ui.selectMany("Remove z0ne.ai settings from:", items, detected);
|
|
244
|
+
}
|
|
245
|
+
if (!toolIds.length) {
|
|
246
|
+
ui.warn("nothing selected - exiting.");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (flags.yes !== true && !(await ui.confirm("Remove z0ne.ai settings from " + toolIds.join(", ") + "?", true))) {
|
|
250
|
+
ui.warn("aborted - nothing changed.");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
ui.log();
|
|
254
|
+
for (const id of toolIds) {
|
|
255
|
+
const t = byId(id);
|
|
256
|
+
try {
|
|
257
|
+
const res = t.remove();
|
|
258
|
+
ui.ok(t.title + (res.paths.length ? " -> cleaned " + res.paths.map(shortHome).join(", ") : ""));
|
|
259
|
+
for (const n of res.notes) ui.info(t.title + ": " + n);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
ui.err(t.title + ": " + error.message);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
ui.log();
|
|
266
|
+
ui.ok("Uninstall finished. Backups (*.bak.<timestamp>) were left in place.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function run(argv) {
|
|
270
|
+
const flags = parseArgs(argv);
|
|
271
|
+
if (flags.help === true || flags.h === true) return ui.log(HELP);
|
|
272
|
+
if (flags.version === true || flags.v === true) return ui.log(VERSION);
|
|
273
|
+
|
|
274
|
+
const cmd = flags._[0] || "install";
|
|
275
|
+
banner();
|
|
276
|
+
if (cmd === "install") return install(flags);
|
|
277
|
+
if (cmd === "uninstall") return uninstall(flags);
|
|
278
|
+
throw new Error('unknown command "' + cmd + '" (try --help)');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { run };
|
package/lib/toml.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Line-based TOML merge helpers for Codex's config.toml.
|
|
3
|
+
// A full TOML round-trip would drop the user's comments and formatting, so we
|
|
4
|
+
// edit lines in place instead: replace a key inside its section when present,
|
|
5
|
+
// append it otherwise, and never touch unrelated lines.
|
|
6
|
+
|
|
7
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8
|
+
|
|
9
|
+
// TOML basic-string literal (paths are pre-normalized to forward slashes).
|
|
10
|
+
const tstr = (s) => '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
11
|
+
|
|
12
|
+
// Set `key = value` inside [section] (or top level when section is null).
|
|
13
|
+
// The section range ends at the NEXT bracket header of any kind, so inserted
|
|
14
|
+
// keys can never leak into a following subsection.
|
|
15
|
+
function setTomlKey(text, key, value, section) {
|
|
16
|
+
const lines = text ? text.split(/\r?\n/) : [];
|
|
17
|
+
let start = null;
|
|
18
|
+
let end = lines.length;
|
|
19
|
+
|
|
20
|
+
if (section) {
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const stripped = lines[i].trim();
|
|
23
|
+
if (start === null) {
|
|
24
|
+
if (stripped === "[" + section + "]") start = i + 1;
|
|
25
|
+
} else if (stripped.startsWith("[")) {
|
|
26
|
+
end = i;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (start === null) {
|
|
31
|
+
if (lines.length && lines[lines.length - 1].trim() !== "") lines.push("");
|
|
32
|
+
lines.push("[" + section + "]");
|
|
33
|
+
start = lines.length;
|
|
34
|
+
end = lines.length;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
start = 0;
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
if (lines[i].trim().startsWith("[")) {
|
|
40
|
+
end = i;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const keyRe = new RegExp("^\\s*" + escapeRe(key) + "\\s*=");
|
|
47
|
+
for (let i = start; i < end; i++) {
|
|
48
|
+
if (keyRe.test(lines[i])) {
|
|
49
|
+
lines[i] = key + " = " + value;
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.splice(end, 0, key + " = " + value);
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read `key` from [section] (or top level). Returns the raw value string
|
|
58
|
+
// (quotes stripped for basic strings) or null.
|
|
59
|
+
function getTomlKey(text, key, section) {
|
|
60
|
+
if (!text) return null;
|
|
61
|
+
const lines = text.split(/\r?\n/);
|
|
62
|
+
let inSection = section === null || section === undefined;
|
|
63
|
+
const keyRe = new RegExp("^\\s*" + escapeRe(key) + "\\s*=\\s*(.+?)\\s*$");
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const stripped = line.trim();
|
|
66
|
+
if (stripped.startsWith("[")) {
|
|
67
|
+
if (section) inSection = stripped === "[" + section + "]";
|
|
68
|
+
else inSection = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!inSection) continue;
|
|
72
|
+
const m = line.match(keyRe);
|
|
73
|
+
if (m) {
|
|
74
|
+
let v = m[1];
|
|
75
|
+
const q = v.match(/^"(.*)"$/);
|
|
76
|
+
return q ? q[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\") : v;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Remove [section] and all its [section.*] subsections, header included.
|
|
83
|
+
function removeTomlSection(text, section) {
|
|
84
|
+
if (!text) return text;
|
|
85
|
+
const lines = text.split(/\r?\n/);
|
|
86
|
+
const out = [];
|
|
87
|
+
let skipping = false;
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const stripped = line.trim();
|
|
90
|
+
if (stripped.startsWith("[")) {
|
|
91
|
+
skipping =
|
|
92
|
+
stripped === "[" + section + "]" || stripped.startsWith("[" + section + ".");
|
|
93
|
+
}
|
|
94
|
+
if (!skipping) out.push(line);
|
|
95
|
+
}
|
|
96
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Remove a top-level `key = ...` line.
|
|
100
|
+
function removeTomlKey(text, key) {
|
|
101
|
+
if (!text) return text;
|
|
102
|
+
const lines = text.split(/\r?\n/);
|
|
103
|
+
const keyRe = new RegExp("^\\s*" + escapeRe(key) + "\\s*=");
|
|
104
|
+
let topLevel = true;
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (line.trim().startsWith("[")) topLevel = false;
|
|
108
|
+
if (topLevel && keyRe.test(line)) continue;
|
|
109
|
+
out.push(line);
|
|
110
|
+
}
|
|
111
|
+
return out.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { tstr, setTomlKey, getTomlKey, removeTomlSection, removeTomlKey };
|