@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/lib/ui.js ADDED
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ // Minimal zero-dependency terminal UI: colors, prompts, interactive pickers.
3
+ // Falls back to plain defaults when stdin/stdout is not a TTY (CI / piped).
4
+
5
+ const readline = require("readline");
6
+
7
+ const CTRL_C = "";
8
+ const DEL = "";
9
+ const ESC = "";
10
+ const UP = ESC + "[A";
11
+ const DOWN = ESC + "[B";
12
+
13
+ const useColor = !!process.stdout.isTTY && !process.env.NO_COLOR;
14
+ const wrap = (open, close) => (s) =>
15
+ useColor ? "\x1b[" + open + "m" + s + "\x1b[" + close + "m" : String(s);
16
+
17
+ const fmt = {
18
+ bold: wrap(1, 22),
19
+ dim: wrap(2, 22),
20
+ red: wrap(31, 39),
21
+ green: wrap(32, 39),
22
+ yellow: wrap(33, 39),
23
+ cyan: wrap(36, 39),
24
+ gray: wrap(90, 39),
25
+ };
26
+
27
+ const log = (s = "") => process.stdout.write(s + "\n");
28
+ const ok = (s) => log(fmt.green(" + ") + s);
29
+ const warn = (s) => log(fmt.yellow(" ! ") + s);
30
+ const err = (s) => log(fmt.red(" x ") + s);
31
+ const info = (s) => log(fmt.gray(" - ") + s);
32
+
33
+ const isInteractive = () => !!(process.stdin.isTTY && process.stdout.isTTY);
34
+
35
+ function ask(question, def) {
36
+ if (!isInteractive()) return Promise.resolve(def || "");
37
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
+ const suffix = def ? fmt.gray(" (" + def + ")") : "";
39
+ return new Promise((resolve) => {
40
+ rl.question(" " + question + suffix + " ", (answer) => {
41
+ rl.close();
42
+ resolve(answer.trim() || def || "");
43
+ });
44
+ });
45
+ }
46
+
47
+ // Masked input for API keys: raw mode so the key never echoes to the terminal.
48
+ function askHidden(question) {
49
+ if (!isInteractive()) return Promise.resolve("");
50
+ return new Promise((resolve) => {
51
+ process.stdout.write(" " + question + " " + fmt.gray("(input hidden) "));
52
+ const stdin = process.stdin;
53
+ stdin.setRawMode(true);
54
+ stdin.resume();
55
+ stdin.setEncoding("utf8");
56
+ let buf = "";
57
+ const onData = (chunk) => {
58
+ for (const ch of chunk) {
59
+ if (ch === "\r" || ch === "\n") {
60
+ stdin.setRawMode(false);
61
+ stdin.pause();
62
+ stdin.removeListener("data", onData);
63
+ process.stdout.write("\n");
64
+ resolve(buf.trim());
65
+ return;
66
+ }
67
+ if (ch === CTRL_C) {
68
+ stdin.setRawMode(false);
69
+ process.stdout.write("\n");
70
+ process.exit(130);
71
+ }
72
+ if (ch === DEL || ch === "\b") buf = buf.slice(0, -1);
73
+ else if (ch >= " ") buf += ch;
74
+ }
75
+ };
76
+ stdin.on("data", onData);
77
+ });
78
+ }
79
+
80
+ async function confirm(question, def = true) {
81
+ if (!isInteractive()) return def;
82
+ const a = await ask(question + (def ? " [Y/n]" : " [y/N]"));
83
+ if (!a) return def;
84
+ return /^y(es)?$/i.test(a);
85
+ }
86
+
87
+ // Interactive list picker. items: [{value, label, hint}]
88
+ // - single: arrows + enter, type to filter
89
+ // - multi: space toggles, enter confirms, type to filter
90
+ function listPicker({ title, items, multi = false, preselected = [], defValue = null }) {
91
+ if (!isInteractive()) {
92
+ if (multi) {
93
+ const pre = new Set(preselected);
94
+ return Promise.resolve(items.filter((it) => pre.has(it.value)).map((it) => it.value));
95
+ }
96
+ const def = items.find((it) => it.value === defValue) || items[0];
97
+ return Promise.resolve(def ? def.value : null);
98
+ }
99
+
100
+ return new Promise((resolve) => {
101
+ const stdin = process.stdin;
102
+ const out = process.stdout;
103
+ const chosen = new Set(preselected);
104
+ const MAXROWS = 12;
105
+ let filter = "";
106
+ let cursor = Math.max(0, items.findIndex((it) => it.value === defValue));
107
+ let lastRows = 0;
108
+
109
+ const visible = () => {
110
+ const f = filter.toLowerCase();
111
+ return f
112
+ ? items.filter((it) => (it.label + " " + (it.hint || "")).toLowerCase().includes(f))
113
+ : items;
114
+ };
115
+
116
+ const render = (final = false) => {
117
+ const list = visible();
118
+ if (cursor >= list.length) cursor = Math.max(0, list.length - 1);
119
+ let start = Math.max(0, Math.min(cursor - Math.floor(MAXROWS / 2), list.length - MAXROWS));
120
+ const end = Math.min(list.length, start + MAXROWS);
121
+ const rows = [];
122
+ const keys = multi
123
+ ? "space toggle - enter confirm - type to filter"
124
+ : "arrows move - enter select - type to filter";
125
+ rows.push(" " + fmt.bold(title) + " " + fmt.gray(keys));
126
+ rows.push(
127
+ " " +
128
+ (filter
129
+ ? fmt.cyan("filter: " + filter)
130
+ : fmt.gray(list.length + " option" + (list.length === 1 ? "" : "s")))
131
+ );
132
+ if (!list.length) rows.push(fmt.gray(" (no match)"));
133
+ for (let i = start; i < end; i++) {
134
+ const it = list[i];
135
+ const mark = multi ? (chosen.has(it.value) ? fmt.green("[x] ") : fmt.gray("[ ] ")) : "";
136
+ const line = mark + it.label + (it.hint ? " " + fmt.gray(it.hint) : "");
137
+ rows.push(i === cursor && !final ? fmt.cyan(" > ") + line : " " + line);
138
+ }
139
+ if (end < list.length) rows.push(fmt.gray(" ... " + (list.length - end) + " more"));
140
+ if (lastRows) {
141
+ readline.moveCursor(out, 0, -lastRows);
142
+ readline.cursorTo(out, 0);
143
+ readline.clearScreenDown(out);
144
+ }
145
+ out.write(rows.join("\n") + "\n");
146
+ lastRows = rows.length;
147
+ };
148
+
149
+ const done = (result) => {
150
+ stdin.setRawMode(false);
151
+ stdin.pause();
152
+ stdin.removeListener("data", onData);
153
+ resolve(result);
154
+ };
155
+
156
+ const onData = (chunk) => {
157
+ const list = visible();
158
+ if (chunk === CTRL_C) {
159
+ stdin.setRawMode(false);
160
+ out.write("\n");
161
+ process.exit(130);
162
+ } else if (chunk === "\r" || chunk === "\n") {
163
+ if (multi) return done([...chosen]);
164
+ if (list[cursor]) return done(list[cursor].value);
165
+ return;
166
+ } else if (chunk === UP) {
167
+ cursor = Math.max(0, cursor - 1);
168
+ } else if (chunk === DOWN) {
169
+ cursor = Math.min(list.length - 1, cursor + 1);
170
+ } else if (chunk === " " && multi) {
171
+ const it = list[cursor];
172
+ if (it) chosen.has(it.value) ? chosen.delete(it.value) : chosen.add(it.value);
173
+ } else if (chunk === DEL || chunk === "\b") {
174
+ filter = filter.slice(0, -1);
175
+ cursor = 0;
176
+ } else if (!chunk.startsWith(ESC)) {
177
+ for (const ch of chunk) if (ch >= " ") filter += ch;
178
+ cursor = 0;
179
+ }
180
+ render();
181
+ };
182
+
183
+ stdin.setRawMode(true);
184
+ stdin.resume();
185
+ stdin.setEncoding("utf8");
186
+ stdin.on("data", onData);
187
+ render();
188
+ });
189
+ }
190
+
191
+ const selectOne = (title, items, defValue) => listPicker({ title, items, defValue });
192
+ const selectMany = (title, items, preselected) =>
193
+ listPicker({ title, items, multi: true, preselected });
194
+
195
+ module.exports = {
196
+ fmt,
197
+ log,
198
+ ok,
199
+ warn,
200
+ err,
201
+ info,
202
+ ask,
203
+ askHidden,
204
+ confirm,
205
+ selectOne,
206
+ selectMany,
207
+ isInteractive,
208
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@z0ne/installer",
3
+ "version": "0.1.0",
4
+ "description": "Point coding agents at z0ne.ai — Claude Code, Codex, OpenCode, Cline, Hermes, OpenClaw. One key, one command. Optionally installs a Claude Code statusline.",
5
+ "license": "MIT",
6
+ "homepage": "https://z0ne.ai",
7
+ "bin": {
8
+ "z0ne-installer": "bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "lib",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "keywords": [
23
+ "z0ne",
24
+ "ai",
25
+ "gateway",
26
+ "claude-code",
27
+ "codex",
28
+ "opencode",
29
+ "cline",
30
+ "hermes",
31
+ "openclaw",
32
+ "installer",
33
+ "cli",
34
+ "statusline"
35
+ ]
36
+ }