oh-aicoding-tool 0.1.7 → 0.1.9

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.
Files changed (2) hide show
  1. package/bin/cli.js +157 -53
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -4,9 +4,11 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { createRequire } from "node:module";
6
6
  import { createInterface } from "node:readline/promises";
7
+ import { fileURLToPath } from "node:url";
7
8
 
8
- const require = createRequire(import.meta.url);
9
-
9
+ const require = createRequire(import.meta.url);
10
+ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11
+
10
12
  const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
11
13
  const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
12
14
  const rgb = (r, g, b) => (colorEnabled ? `\x1b[38;2;${r};${g};${b}m` : "");
@@ -46,16 +48,22 @@ function renderHeader(subtitle = "Interactive setup") {
46
48
  console.log(`${paint("Space", t.gold)} toggle ${paint("Enter", t.gold)} continue ${paint("Up/Down", t.gold)} move ${paint("q", t.gold)} exit`);
47
49
  }
48
50
 
49
- function printHelp() {
50
- console.log("oh-aicoding-tool");
51
- console.log("");
52
- console.log("Usage:");
53
- console.log(" oh-aicoding-tool");
54
- console.log(" oh-aicoding-tool langfuse [setup|check] [target]");
55
- console.log(" oh-aicoding-tool report install opencode [--email user@company.com]");
56
- console.log(" oh-aicoding-tool report install claude");
57
- console.log(" oh-aicoding-tool report install both");
58
- }
51
+ function printHelp() {
52
+ console.log("oh-aicoding-tool");
53
+ console.log("");
54
+ console.log("Usage:");
55
+ console.log(" oh-aicoding-tool");
56
+ console.log(" oh-aicoding-tool --version");
57
+ console.log(" oh-aicoding-tool debug-keys");
58
+ console.log(" oh-aicoding-tool langfuse [setup|check] [target]");
59
+ console.log(" oh-aicoding-tool report install opencode [--email user@company.com]");
60
+ console.log(" oh-aicoding-tool report install claude");
61
+ console.log(" oh-aicoding-tool report install both");
62
+ }
63
+
64
+ function readOwnPackage() {
65
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
66
+ }
59
67
 
60
68
  function packageBinScript(packageName, preferredBin) {
61
69
  const pkgPath = require.resolve(`${packageName}/package.json`);
@@ -108,26 +116,107 @@ function rawKeySeq(raw) {
108
116
  return String(raw ?? "");
109
117
  }
110
118
 
111
- function parseRawKey(raw) {
112
- const seq = rawKeySeq(raw);
113
- if (seq === "\x03") return { name: "ctrl-c", sequence: seq };
114
- if (seq === "\x1b[A" || seq === "\x1bOA" || seq === "\x00H" || seq === "\xe0H") return { name: "up", sequence: seq };
115
- if (seq === "\x1b[B" || seq === "\x1bOB" || seq === "\x00P" || seq === "\xe0P") return { name: "down", sequence: seq };
116
- if (seq === "\r" || seq === "\n" || seq === "\r\n") return { name: "enter", sequence: seq };
117
- if (seq === " ") return { name: "space", sequence: seq };
118
- if (seq === "\x1b") return { name: "escape", sequence: seq };
119
- if (/^[1-9]$/.test(seq)) return { name: "number", number: Number.parseInt(seq, 10), sequence: seq };
120
- if (seq.length === 1) return { name: seq.toLowerCase(), sequence: seq };
121
- return { name: "", sequence: seq };
119
+ function readKeyFromBuffer(buffer) {
120
+ if (!buffer) return { pending: true, rest: "" };
121
+ if (buffer === "\x1b" || buffer === "\x00" || buffer === "\xe0" || /^\x1b\[[0-9;?]*$/.test(buffer) || buffer === "\x1bO") {
122
+ return { pending: true, rest: buffer };
123
+ }
124
+
125
+ const csi = buffer.match(/^\x1b\[[0-9;?]*([A-Za-z~])/);
126
+ if (csi?.[1] === "A") return { key: { name: "up", sequence: csi[0] }, rest: buffer.slice(csi[0].length) };
127
+ if (csi?.[1] === "B") return { key: { name: "down", sequence: csi[0] }, rest: buffer.slice(csi[0].length) };
128
+ if (csi) return { key: { name: "", sequence: csi[0] }, rest: buffer.slice(csi[0].length) };
129
+
130
+ const ss3 = buffer.match(/^\x1bO([A-Za-z])/);
131
+ if (ss3?.[1] === "A") return { key: { name: "up", sequence: ss3[0] }, rest: buffer.slice(ss3[0].length) };
132
+ if (ss3?.[1] === "B") return { key: { name: "down", sequence: ss3[0] }, rest: buffer.slice(ss3[0].length) };
133
+ if (ss3) return { key: { name: "", sequence: ss3[0] }, rest: buffer.slice(ss3[0].length) };
134
+
135
+ const three = buffer.slice(0, 3);
136
+ if (three === "\x1b[A" || three === "\x1bOA") return { key: { name: "up", sequence: three }, rest: buffer.slice(3) };
137
+ if (three === "\x1b[B" || three === "\x1bOB") return { key: { name: "down", sequence: three }, rest: buffer.slice(3) };
138
+
139
+ const two = buffer.slice(0, 2);
140
+ if (two === "\x00H" || two === "\xe0H") return { key: { name: "up", sequence: two }, rest: buffer.slice(2) };
141
+ if (two === "\x00P" || two === "\xe0P") return { key: { name: "down", sequence: two }, rest: buffer.slice(2) };
142
+ if (two === "\r\n") return { key: { name: "enter", sequence: two }, rest: buffer.slice(2) };
143
+
144
+ const seq = buffer[0];
145
+ if (seq === "\x03") return { key: { name: "ctrl-c", sequence: seq }, rest: buffer.slice(1) };
146
+ if (seq === "\r" || seq === "\n") return { key: { name: "enter", sequence: seq }, rest: buffer.slice(1) };
147
+ if (seq === " ") return { key: { name: "space", sequence: seq }, rest: buffer.slice(1) };
148
+ if (seq === "\x1b") return { key: { name: "escape", sequence: seq }, rest: buffer.slice(1) };
149
+ if (/^[1-9]$/.test(seq)) return { key: { name: "number", number: Number.parseInt(seq, 10), sequence: seq }, rest: buffer.slice(1) };
150
+ return { key: { name: seq.toLowerCase(), sequence: seq }, rest: buffer.slice(1) };
151
+ }
152
+
153
+ function showControlChars(value) {
154
+ return value
155
+ .replaceAll("\x1b", "\\x1b")
156
+ .replaceAll("\x00", "\\x00")
157
+ .replaceAll("\xe0", "\\xe0")
158
+ .replaceAll("\r", "\\r")
159
+ .replaceAll("\n", "\\n")
160
+ .replaceAll("\x03", "\\x03");
161
+ }
162
+
163
+ function keyHex(value) {
164
+ return [...Buffer.from(value, "latin1")].map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
165
+ }
166
+
167
+ async function debugKeys() {
168
+ const pkg = readOwnPackage();
169
+ console.log(`oh-aicoding-tool ${pkg.version}`);
170
+ console.log(`node ${process.version} ${process.platform} ${process.arch}`);
171
+ console.log(`stdin.isTTY=${process.stdin.isTTY} stdout.isTTY=${process.stdout.isTTY} rawMode=${process.stdin.isRaw}`);
172
+ console.log(`TERM=${process.env.TERM || ""} WT_SESSION=${process.env.WT_SESSION || ""} TERM_PROGRAM=${process.env.TERM_PROGRAM || ""}`);
173
+ console.log("Press Up, Down, Space, Enter, then q to exit this debug view.");
174
+
175
+ if (!process.stdin.isTTY) return 1;
176
+
177
+ return await new Promise((resolve) => {
178
+ let keyBuffer = "";
179
+ const stdin = process.stdin;
180
+
181
+ function cleanup(code) {
182
+ stdin.off("data", onData);
183
+ if (stdin.isTTY) stdin.setRawMode(false);
184
+ stdin.pause();
185
+ resolve(code);
186
+ }
187
+
188
+ function onData(raw) {
189
+ const seq = rawKeySeq(raw);
190
+ console.log(`raw hex=[${keyHex(seq)}] text=${showControlChars(seq)}`);
191
+ keyBuffer += seq;
192
+ while (keyBuffer) {
193
+ const result = readKeyFromBuffer(keyBuffer);
194
+ if (result.pending) {
195
+ console.log(`pending buffer hex=[${keyHex(result.rest)}] text=${showControlChars(result.rest)}`);
196
+ keyBuffer = result.rest;
197
+ return;
198
+ }
199
+ keyBuffer = result.rest;
200
+ const key = result.key;
201
+ console.log(`parsed name=${key.name || "<unknown>"} sequence=${showControlChars(key.sequence || "")}`);
202
+ if (key.name === "q" || key.name === "ctrl-c") return cleanup(0);
203
+ }
204
+ }
205
+
206
+ stdin.setRawMode(true);
207
+ stdin.on("data", onData);
208
+ stdin.resume();
209
+ });
122
210
  }
123
211
 
124
212
  async function askMultiChoice(rl, title, choices, subtitle) {
125
213
  if (process.stdin.isTTY && process.stdout.isTTY) {
126
214
  rl.pause();
127
215
  return await new Promise((resolve) => {
128
- let index = 0;
129
- const selected = new Set(choices.filter((choice) => choice.selected).map((choice) => choice.value));
130
- const stdin = process.stdin;
216
+ let index = 0;
217
+ let keyBuffer = "";
218
+ const selected = new Set(choices.filter((choice) => choice.selected).map((choice) => choice.value));
219
+ const stdin = process.stdin;
131
220
 
132
221
  function cleanup(value) {
133
222
  stdin.off("data", onData);
@@ -146,25 +235,35 @@ async function askMultiChoice(rl, title, choices, subtitle) {
146
235
  }
147
236
 
148
237
  function onData(raw) {
149
- const key = parseRawKey(raw);
150
- if (key.name === "ctrl-c") return cleanup([]);
151
- if (key.name === "up") {
152
- index = (index - 1 + choices.length) % choices.length;
153
- renderMultiChoice(title, choices, index, selected, subtitle);
154
- return;
155
- }
156
- if (key.name === "down") {
157
- index = (index + 1) % choices.length;
158
- renderMultiChoice(title, choices, index, selected, subtitle);
159
- return;
160
- }
161
- if (key.name === "q" || key.name === "escape") return cleanup([]);
162
- if (key.name === "space") return toggle();
163
- if (key.name === "enter") return cleanup([...selected]);
164
- const number = key.name === "number" ? key.number : Number.NaN;
165
- if (Number.isInteger(number) && choices[number - 1]) {
166
- index = number - 1;
167
- toggle();
238
+ keyBuffer += rawKeySeq(raw);
239
+ while (keyBuffer) {
240
+ const result = readKeyFromBuffer(keyBuffer);
241
+ if (result.pending) {
242
+ keyBuffer = result.rest;
243
+ return;
244
+ }
245
+ keyBuffer = result.rest;
246
+ const key = result.key;
247
+
248
+ if (key.name === "ctrl-c") return cleanup([]);
249
+ if (key.name === "up") {
250
+ index = (index - 1 + choices.length) % choices.length;
251
+ renderMultiChoice(title, choices, index, selected, subtitle);
252
+ return;
253
+ }
254
+ if (key.name === "down") {
255
+ index = (index + 1) % choices.length;
256
+ renderMultiChoice(title, choices, index, selected, subtitle);
257
+ return;
258
+ }
259
+ if (key.name === "q" || key.name === "escape") return cleanup([]);
260
+ if (key.name === "space") return toggle();
261
+ if (key.name === "enter") return cleanup([...selected]);
262
+ const number = key.name === "number" ? key.number : Number.NaN;
263
+ if (Number.isInteger(number) && choices[number - 1]) {
264
+ index = number - 1;
265
+ toggle();
266
+ }
168
267
  }
169
268
  }
170
269
 
@@ -251,14 +350,19 @@ async function interactive() {
251
350
  }
252
351
  }
253
352
 
254
- async function main() {
255
- const argv = process.argv.slice(2);
256
- const [cmd, ...rest] = argv;
257
- if (cmd === "--help" || cmd === "-h" || cmd === "help") {
258
- printHelp();
259
- return 0;
260
- }
261
- if (!cmd) return await interactive();
353
+ async function main() {
354
+ const argv = process.argv.slice(2);
355
+ const [cmd, ...rest] = argv;
356
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
357
+ console.log(readOwnPackage().version);
358
+ return 0;
359
+ }
360
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") {
361
+ printHelp();
362
+ return 0;
363
+ }
364
+ if (cmd === "debug-keys") return await debugKeys();
365
+ if (!cmd) return await interactive();
262
366
  if (cmd === "langfuse") return runPackageBin("oh-langfuse", "oh-langfuse", rest);
263
367
  if (cmd === "report") return runPackageBin("oh-aireport", "oh-aireport", rest);
264
368
  if (cmd === "setup" || cmd === "check") return runPackageBin("oh-langfuse", "oh-langfuse", argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-aicoding-tool",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Interactive installer for AI coding tools: Langfuse tracing and oh-ai-report.",