@coldtea/abide 0.0.1 → 0.0.2
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/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +29 -49
- package/dist/commands/login.js.map +1 -1
- package/dist/lib/secret.d.ts +23 -0
- package/dist/lib/secret.d.ts.map +1 -0
- package/dist/lib/secret.js +41 -0
- package/dist/lib/secret.js.map +1 -0
- package/dist/ui/components/Picker.d.ts +13 -0
- package/dist/ui/components/Picker.d.ts.map +1 -0
- package/dist/ui/components/Picker.js +48 -0
- package/dist/ui/components/Picker.js.map +1 -0
- package/dist/ui/render.d.ts +3 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +10 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/views/HelpView.js +1 -1
- package/dist/ui/views/HelpView.js.map +1 -1
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/commands/login.ts +32 -50
- package/src/lib/secret.ts +58 -0
- package/src/ui/components/Picker.tsx +85 -0
- package/src/ui/render.tsx +21 -0
- package/src/ui/views/HelpView.tsx +1 -1
package/dist/bin.js
CHANGED
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;IAC1D,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,KAAK,IAAqB,EAAE;IACvC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,QAAQ,
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;IAC1D,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,KAAK,IAAqB,EAAE;IACvC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,QAAQ,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;QACvB,KAAK,SAAS;YACZ,OAAO,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACjC,KAAK,MAAM;YACT,OAAO,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChC,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,WAAW;YACd,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;QAC5B,KAAK,OAAO;YACV,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;QAC/B,KAAK,OAAO;YACV,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;QACxB,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,OAAO;YACV,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;QACxB,KAAK,WAAW;YACd,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;QAC5B,KAAK,SAAS,CAAC;QACf,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI;YACP,MAAM,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC7B,OAAO,CAAC,CAAC;QACX;YACE,MAAM,SAAS,CAAC,IAAI,KAAK,CAAC,oBAAoB,OAAO,oBAAoB,CAAC,CAAC,CAAC;YAC5E,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC,CAAC;AAEF,IAAI,CAAC;IACH,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAC;AAClC,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;IACvB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AA6BA,+DAA+D;AAC/D,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC,MAAM,CAY/C,CAAC"}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,58 +1,38 @@
|
|
|
1
|
-
import { parseArgs } from "node:util";
|
|
2
1
|
import { AbideError } from "@coldtea/abide-schema";
|
|
3
|
-
import {
|
|
2
|
+
import { readSecret } from "../lib/secret.js";
|
|
4
3
|
import { saveUserKey } from "../lib/credentials.js";
|
|
5
4
|
import { Callout } from "../ui/components/Callout.js";
|
|
6
|
-
import { showStatic } from "../ui/render.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (const ch of chunk) {
|
|
27
|
-
if (ch === "\u0003") {
|
|
28
|
-
done();
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
32
|
-
done();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (ch === "\u007f" || ch === "\b") {
|
|
36
|
-
value = value.slice(0, -1);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
value += ch;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
stdin.on("data", onData);
|
|
43
|
-
});
|
|
5
|
+
import { showPicker, showStatic } from "../ui/render.js";
|
|
6
|
+
import { GATEWAY_KEY_ENV, TYPESAFE_KEY_ENV } from "../lib/constants.js";
|
|
7
|
+
const TYPESAFE = {
|
|
8
|
+
value: { name: TYPESAFE_KEY_ENV, prompt: "TypeSafe API key: " },
|
|
9
|
+
label: "TypeSafe API key",
|
|
10
|
+
hint: "from typesafe.ai",
|
|
11
|
+
};
|
|
12
|
+
const GATEWAY = {
|
|
13
|
+
value: { name: GATEWAY_KEY_ENV, prompt: "Vercel AI Gateway key: " },
|
|
14
|
+
label: "Vercel AI Gateway key",
|
|
15
|
+
hint: "a key you already have",
|
|
16
|
+
};
|
|
17
|
+
const chooseProvider = async () => {
|
|
18
|
+
if (!process.stdin.isTTY)
|
|
19
|
+
return TYPESAFE.value;
|
|
20
|
+
const chosen = await showPicker("Which key do you have?", [TYPESAFE, GATEWAY]);
|
|
21
|
+
if (chosen === undefined)
|
|
22
|
+
throw new AbideError("NO_API_KEY", "no key was chosen");
|
|
23
|
+
return chosen.value;
|
|
24
|
+
};
|
|
44
25
|
/** Never a flag: a flag lands in shell history and CI logs. */
|
|
45
|
-
export const runLogin = async (
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
options: { gateway: { type: "boolean", default: false } },
|
|
49
|
-
});
|
|
50
|
-
const name = values.gateway ? GATEWAY_KEY_ENV : TYPESAFE_KEY_ENV;
|
|
51
|
-
const key = await readSecret(values.gateway ? "Vercel AI Gateway key: " : "TypeSafe API key (from typesafe.ai): ");
|
|
26
|
+
export const runLogin = async () => {
|
|
27
|
+
const provider = await chooseProvider();
|
|
28
|
+
const key = await readSecret(provider.prompt);
|
|
52
29
|
if (key === "")
|
|
53
30
|
throw new AbideError("NO_API_KEY", "nothing was entered");
|
|
54
|
-
const file = saveUserKey(name, key);
|
|
55
|
-
await showStatic(Callout({
|
|
31
|
+
const file = saveUserKey(provider.name, key);
|
|
32
|
+
await showStatic(Callout({
|
|
33
|
+
tone: "ok",
|
|
34
|
+
title: `${provider.name} saved to ${file} (owner-only). Run abide init next.`,
|
|
35
|
+
}));
|
|
56
36
|
return 0;
|
|
57
37
|
};
|
|
58
38
|
//# sourceMappingURL=login.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAIxE,MAAM,QAAQ,GAAyB;IACrC,KAAK,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,oBAAoB,EAAE;IAC/D,KAAK,EAAE,kBAAkB;IACzB,IAAI,EAAE,kBAAkB;CACzB,CAAC;AAEF,MAAM,OAAO,GAAyB;IACpC,KAAK,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,yBAAyB,EAAE;IACnE,KAAK,EAAE,uBAAuB;IAC9B,IAAI,EAAE,wBAAwB;CAC/B,CAAC;AAEF,MAAM,cAAc,GAAG,KAAK,IAAuB,EAAE;IACnD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC,KAAK,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,wBAAwB,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/E,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,IAAI,UAAU,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC;IAClF,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC,CAAC;AAEF,+DAA+D;AAC/D,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAqB,EAAE;IAClD,MAAM,QAAQ,GAAG,MAAM,cAAc,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,GAAG,KAAK,EAAE;QAAE,MAAM,IAAI,UAAU,CAAC,YAAY,EAAE,qBAAqB,CAAC,CAAC;IAC1E,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7C,MAAM,UAAU,CACd,OAAO,CAAC;QACN,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,GAAG,QAAQ,CAAC,IAAI,aAAa,IAAI,qCAAqC;KAC9E,CAAC,CACH,CAAC;IACF,OAAO,CAAC,CAAC;AACX,CAAC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The slice of a stdin the reader needs, so a test can hand in a stream of its own. */
|
|
2
|
+
export type SecretInput = {
|
|
3
|
+
isTTY: boolean;
|
|
4
|
+
isRaw: boolean;
|
|
5
|
+
setRawMode(mode: boolean): unknown;
|
|
6
|
+
setEncoding(encoding: BufferEncoding): unknown;
|
|
7
|
+
/** A TTY or pipe has this; a redirected file does not. */
|
|
8
|
+
ref?(): unknown;
|
|
9
|
+
resume(): unknown;
|
|
10
|
+
pause(): unknown;
|
|
11
|
+
on(event: "data", listener: (chunk: string) => void): unknown;
|
|
12
|
+
off(event: "data", listener: (chunk: string) => void): unknown;
|
|
13
|
+
};
|
|
14
|
+
type SecretIo = {
|
|
15
|
+
input: SecretInput;
|
|
16
|
+
output: {
|
|
17
|
+
write(text: string): unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/** Reads one line without echoing it, so the key never lands in a terminal scrollback. */
|
|
21
|
+
export declare const readSecret: (prompt: string, io?: SecretIo) => Promise<string>;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=secret.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../../src/lib/secret.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACnC,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC;IAC/C,0DAA0D;IAC1D,GAAG,CAAC,IAAI,OAAO,CAAC;IAChB,MAAM,IAAI,OAAO,CAAC;IAClB,KAAK,IAAI,OAAO,CAAC;IACjB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;CAChE,CAAC;AAEF,KAAK,QAAQ,GAAG;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAE,CAAC;AAIjF,0FAA0F;AAC1F,eAAO,MAAM,UAAU,WAAY,MAAM,OAAM,QAAQ,KAAiB,OAAO,CAAC,MAAM,CAsClF,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const processIo = () => ({ input: process.stdin, output: process.stderr });
|
|
2
|
+
/** Reads one line without echoing it, so the key never lands in a terminal scrollback. */
|
|
3
|
+
export const readSecret = (prompt, io = processIo()) => new Promise((resolve) => {
|
|
4
|
+
const { input, output } = io;
|
|
5
|
+
output.write(prompt);
|
|
6
|
+
const wasRaw = input.isTTY ? input.isRaw : false;
|
|
7
|
+
if (input.isTTY)
|
|
8
|
+
input.setRawMode(true);
|
|
9
|
+
input.setEncoding("utf8");
|
|
10
|
+
if (input.isTTY)
|
|
11
|
+
input.ref?.();
|
|
12
|
+
input.resume();
|
|
13
|
+
let value = "";
|
|
14
|
+
const done = () => {
|
|
15
|
+
input.off("data", onData);
|
|
16
|
+
if (input.isTTY)
|
|
17
|
+
input.setRawMode(wasRaw);
|
|
18
|
+
input.pause();
|
|
19
|
+
output.write("\n");
|
|
20
|
+
resolve(value.trim());
|
|
21
|
+
};
|
|
22
|
+
const onData = (chunk) => {
|
|
23
|
+
for (const ch of chunk) {
|
|
24
|
+
if (ch === "\u0003") {
|
|
25
|
+
done();
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
29
|
+
done();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
33
|
+
value = value.slice(0, -1);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
value += ch;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
input.on("data", onData);
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=secret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/lib/secret.ts"],"names":[],"mappings":"AAgBA,MAAM,SAAS,GAAG,GAAa,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAErF,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAc,EAAE,EAAE,GAAa,SAAS,EAAE,EAAmB,EAAE,CACxF,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACtB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IACjD,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACxC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1B,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;IAC/B,KAAK,CAAC,MAAM,EAAE,CAAC;IAEf,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC1B,IAAI,KAAK,CAAC,KAAK;YAAE,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1C,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACxB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,CAAC,KAAa,EAAQ,EAAE;QACrC,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;gBACpB,IAAI,EAAE,CAAC;gBACP,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;YACD,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;gBAClD,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YACD,IAAI,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBACnC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC3B,SAAS;YACX,CAAC;YACD,KAAK,IAAI,EAAE,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PickerItem<T> = {
|
|
2
|
+
value: T;
|
|
3
|
+
label: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
};
|
|
6
|
+
type PickerProps<T> = {
|
|
7
|
+
title: string;
|
|
8
|
+
items: readonly PickerItem<T>[];
|
|
9
|
+
onDone: (chosen: PickerItem<T> | undefined) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare function Picker<T>({ title, items, onDone }: PickerProps<T>): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=Picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../../src/ui/components/Picker.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,KAAK,WAAW,CAAC,CAAC,IAAI;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;CACrD,CAAC;AAOF,wBAAgB,MAAM,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,+BAmEjE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import { glyph, palette } from "../theme.js";
|
|
5
|
+
export function Picker({ title, items, onDone }) {
|
|
6
|
+
const { exit } = useApp();
|
|
7
|
+
const [state, setState] = useState({ kind: "choosing", index: 0 });
|
|
8
|
+
// Keys can arrive faster than a re-render, so the handler reads the cursor from here, not from its closure.
|
|
9
|
+
const cursor = useRef(0);
|
|
10
|
+
const move = (index) => {
|
|
11
|
+
cursor.current = index;
|
|
12
|
+
setState({ kind: "choosing", index });
|
|
13
|
+
};
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.upArrow || input === "k") {
|
|
16
|
+
move(Math.max(0, cursor.current - 1));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (key.downArrow || input === "j") {
|
|
20
|
+
move(Math.min(items.length - 1, cursor.current + 1));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (key.return) {
|
|
24
|
+
setState({ kind: "chosen", index: cursor.current });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (key.escape)
|
|
28
|
+
setState({ kind: "left", index: cursor.current });
|
|
29
|
+
}, { isActive: state.kind === "choosing" });
|
|
30
|
+
// Leave only after the answer has been drawn, not in the same tick it was picked.
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (state.kind === "choosing")
|
|
33
|
+
return;
|
|
34
|
+
onDone(state.kind === "chosen" ? items[state.index] : undefined);
|
|
35
|
+
exit();
|
|
36
|
+
// The pick is final; nothing about it changes afterwards.
|
|
37
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
|
+
}, [state.kind]);
|
|
39
|
+
const current = items[state.index];
|
|
40
|
+
if (state.kind === "chosen") {
|
|
41
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: palette.mist, children: [title, " "] }), _jsx(Text, { color: palette.teal, children: current?.label })] }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: palette.cloud, bold: true, children: title }), items.map((item, index) => {
|
|
44
|
+
const active = index === state.index;
|
|
45
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: active ? palette.teal : palette.ash, children: [active ? glyph.arrow : " ", " "] }), _jsx(Text, { color: active ? palette.teal : palette.cloud, bold: active, children: item.label }), item.hint ? _jsxs(Text, { color: palette.mist, children: [" ", item.hint] }) : null] }, item.label));
|
|
46
|
+
}), _jsxs(Text, { color: palette.ash, children: ["\u2191\u2193 move ", glyph.dotSep, " enter pick ", glyph.dotSep, " esc quit"] })] }));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=Picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../../src/ui/components/Picker.tsx"],"names":[],"mappings":"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"}
|
package/dist/ui/render.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
import { type Progress } from "./components/Live.js";
|
|
3
|
+
import { type PickerItem } from "./components/Picker.js";
|
|
3
4
|
/**
|
|
4
5
|
* Draws a finished view once and leaves. Ink prints the final frame on a pipe
|
|
5
6
|
* too, so `abide report | less` still reads well. Nothing in the hooks goes
|
|
@@ -15,6 +16,8 @@ type LiveJob<T> = {
|
|
|
15
16
|
};
|
|
16
17
|
/** Runs a job under a spinner, draws its result, and resolves to the exit code. */
|
|
17
18
|
export declare const showLive: <T>(job: LiveJob<T>) => Promise<number>;
|
|
19
|
+
/** Asks for one choice on a TTY and resolves to it, or to nothing when the person backs out. */
|
|
20
|
+
export declare const showPicker: <T>(title: string, items: readonly PickerItem<T>[]) => Promise<PickerItem<T> | undefined>;
|
|
18
21
|
export declare const showError: (error: unknown) => Promise<void>;
|
|
19
22
|
export {};
|
|
20
23
|
//# sourceMappingURL=render.d.ts.map
|
package/dist/ui/render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ui/render.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ui/render.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGjE;;;;GAIG;AACH,eAAO,MAAM,UAAU,YAAmB,YAAY,KAAG,OAAO,CAAC,IAAI,CAIpE,CAAC;AAEF,KAAK,OAAO,CAAC,CAAC,IAAI;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,SAAS,CAAC;IAC/B,0CAA0C;IAC1C,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;CAC9B,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,QAAQ,GAAU,CAAC,OAAQ,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,MAAM,CAqBlE,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,UAAU,GAAU,CAAC,SACzB,MAAM,SACN,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,KAC9B,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,SAAS,CAcnC,CAAC;AAEF,eAAO,MAAM,SAAS,UAAiB,OAAO,KAAG,OAAO,CAAC,IAAI,CAE5D,CAAC"}
|
package/dist/ui/render.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { Live } from "./components/Live.js";
|
|
4
|
+
import { Picker } from "./components/Picker.js";
|
|
4
5
|
import { ErrorView } from "./views/ErrorView.js";
|
|
5
6
|
/**
|
|
6
7
|
* Draws a finished view once and leaves. Ink prints the final frame on a pipe
|
|
@@ -26,6 +27,15 @@ export const showLive = async (job) => {
|
|
|
26
27
|
return job.code(outcome.result.value);
|
|
27
28
|
return outcome.code;
|
|
28
29
|
};
|
|
30
|
+
/** Asks for one choice on a TTY and resolves to it, or to nothing when the person backs out. */
|
|
31
|
+
export const showPicker = async (title, items) => {
|
|
32
|
+
let chosen;
|
|
33
|
+
const instance = render(_jsx(Picker, { title: title, items: items, onDone: (item) => {
|
|
34
|
+
chosen = item;
|
|
35
|
+
} }), { patchConsole: false, exitOnCtrlC: true });
|
|
36
|
+
await instance.waitUntilExit();
|
|
37
|
+
return chosen;
|
|
38
|
+
};
|
|
29
39
|
export const showError = async (error) => {
|
|
30
40
|
await showStatic(_jsx(ErrorView, { error: error }));
|
|
31
41
|
};
|
package/dist/ui/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/ui/render.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAAE,IAAI,EAAiB,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAqB,EAAiB,EAAE;IACvE,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7E,QAAQ,CAAC,OAAO,EAAE,CAAC;IACnB,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;AACjC,CAAC,CAAC;AAUF,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAM,GAAe,EAAmB,EAAE;IACrE,MAAM,OAAO,GAA4C,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrE,MAAM,QAAQ,GAAG,MAAM,CACrB,KAAC,IAAI,IACH,MAAM,EAAE,GAAG,CAAC,MAAM,EAClB,GAAG,EAAE,GAAG,CAAC,GAAG,EACZ,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE;YACf,OAAO,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YACnC,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1B,CAAC,EACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,GAAI,EAC9C,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE;YACnB,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;QAC1B,CAAC,EACD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GACtC,EACF,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAC3C,CAAC;IACF,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;IAC/B,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpF,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,KAAc,EAAiB,EAAE;IAC/D,MAAM,UAAU,CAAC,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,GAAI,CAAC,CAAC;AAChD,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/ui/render.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAAE,IAAI,EAAiB,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAmB,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAqB,EAAiB,EAAE;IACvE,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7E,QAAQ,CAAC,OAAO,EAAE,CAAC;IACnB,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;AACjC,CAAC,CAAC;AAUF,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAM,GAAe,EAAmB,EAAE;IACrE,MAAM,OAAO,GAA4C,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrE,MAAM,QAAQ,GAAG,MAAM,CACrB,KAAC,IAAI,IACH,MAAM,EAAE,GAAG,CAAC,MAAM,EAClB,GAAG,EAAE,GAAG,CAAC,GAAG,EACZ,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE;YACf,OAAO,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YACnC,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1B,CAAC,EACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,GAAI,EAC9C,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE;YACnB,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;QAC1B,CAAC,EACD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GACtC,EACF,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAC3C,CAAC;IACF,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;IAC/B,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpF,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC,CAAC;AAEF,gGAAgG;AAChG,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,KAAa,EACb,KAA+B,EACK,EAAE;IACtC,IAAI,MAAiC,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,CACrB,KAAC,MAAM,IACL,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACf,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC,GACD,EACF,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAC3C,CAAC;IACF,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;IAC/B,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,KAAc,EAAiB,EAAE;IAC/D,MAAM,UAAU,CAAC,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,GAAI,CAAC,CAAC;AAChD,CAAC,CAAC"}
|
|
@@ -3,7 +3,7 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { Header } from "../components/Header.js";
|
|
4
4
|
import { palette } from "../theme.js";
|
|
5
5
|
const COMMANDS = [
|
|
6
|
-
["login
|
|
6
|
+
["login", "store your TypeSafe or Vercel AI Gateway key in ~/.abide/.env, owner-only"],
|
|
7
7
|
["init [agent] [--project]", "hook into claude, codex, opencode, or every one found here"],
|
|
8
8
|
["compile", "compile the rubric now, in a headless Claude Code turn"],
|
|
9
9
|
["tune [--global]", "rewrite rules that never fire, with their statistics attached"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HelpView.js","sourceRoot":"","sources":["../../../src/ui/views/HelpView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,MAAM,QAAQ,GAAuB;IACnC,CAAC,
|
|
1
|
+
{"version":3,"file":"HelpView.js","sourceRoot":"","sources":["../../../src/ui/views/HelpView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,MAAM,QAAQ,GAAuB;IACnC,CAAC,OAAO,EAAE,2EAA2E,CAAC;IACtF,CAAC,0BAA0B,EAAE,4DAA4D,CAAC;IAC1F,CAAC,SAAS,EAAE,wDAAwD,CAAC;IACrE,CAAC,iBAAiB,EAAE,+DAA+D,CAAC;IACpF,CAAC,4BAA4B,EAAE,oDAAoD,CAAC;IACpF,CAAC,sBAAsB,EAAE,oDAAoD,CAAC;IAC9E,CAAC,uBAAuB,EAAE,mDAAmD,CAAC;IAC9E,CAAC,uBAAuB,EAAE,sEAAsE,CAAC;IACjG,CAAC,QAAQ,EAAE,gDAAgD,CAAC;IAC5D;QACE,gBAAgB;QAChB,2FAA2F;KAC5F;IACD,CAAC,kBAAkB,EAAE,6CAA6C,CAAC;IACnE,CAAC,+BAA+B,EAAE,yCAAyC,CAAC;CAC7E,CAAC;AAEF,MAAM,UAAU,QAAQ;IACtB,OAAO,CACL,MAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,aACzB,KAAC,MAAM,IACL,OAAO,EAAC,MAAM,EACd,IAAI,EAAC,sEAAsE,GAC3E,EACD,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAC9B,MAAC,GAAG,eACF,KAAC,GAAG,IAAC,KAAK,EAAE,EAAE,YACZ,KAAC,IAAI,IAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,kBAC7B,IAAI,GACA,GACH,EACN,KAAC,IAAI,IAAC,KAAK,EAAE,OAAO,CAAC,IAAI,YAAG,IAAI,GAAQ,KANhC,IAAI,CAOR,CACP,CAAC,EACF,MAAC,GAAG,IAAC,SAAS,EAAE,CAAC,EAAE,aAAa,EAAC,QAAQ,aACvC,KAAC,IAAI,IAAC,KAAK,EAAE,OAAO,CAAC,GAAG,oGAEjB,EACP,KAAC,IAAI,IAAC,KAAK,EAAE,OAAO,CAAC,GAAG,gIAGjB,IACH,IACF,CACP,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
package/src/bin.ts
CHANGED
package/src/commands/login.ts
CHANGED
|
@@ -1,61 +1,43 @@
|
|
|
1
|
-
import { parseArgs } from "node:util";
|
|
2
1
|
import { AbideError } from "@coldtea/abide-schema";
|
|
3
|
-
import {
|
|
2
|
+
import { readSecret } from "../lib/secret.js";
|
|
4
3
|
import { saveUserKey } from "../lib/credentials.js";
|
|
5
4
|
import { Callout } from "../ui/components/Callout.js";
|
|
6
|
-
import { showStatic } from "../ui/render.js";
|
|
5
|
+
import { showPicker, showStatic } from "../ui/render.js";
|
|
6
|
+
import type { PickerItem } from "../ui/components/Picker.js";
|
|
7
|
+
import { GATEWAY_KEY_ENV, TYPESAFE_KEY_ENV } from "../lib/constants.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
done();
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
32
|
-
done();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (ch === "\u007f" || ch === "\b") {
|
|
36
|
-
value = value.slice(0, -1);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
value += ch;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
stdin.on("data", onData);
|
|
43
|
-
});
|
|
9
|
+
type Provider = { name: string; prompt: string };
|
|
10
|
+
|
|
11
|
+
const TYPESAFE: PickerItem<Provider> = {
|
|
12
|
+
value: { name: TYPESAFE_KEY_ENV, prompt: "TypeSafe API key: " },
|
|
13
|
+
label: "TypeSafe API key",
|
|
14
|
+
hint: "from typesafe.ai",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const GATEWAY: PickerItem<Provider> = {
|
|
18
|
+
value: { name: GATEWAY_KEY_ENV, prompt: "Vercel AI Gateway key: " },
|
|
19
|
+
label: "Vercel AI Gateway key",
|
|
20
|
+
hint: "a key you already have",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const chooseProvider = async (): Promise<Provider> => {
|
|
24
|
+
if (!process.stdin.isTTY) return TYPESAFE.value;
|
|
25
|
+
const chosen = await showPicker("Which key do you have?", [TYPESAFE, GATEWAY]);
|
|
26
|
+
if (chosen === undefined) throw new AbideError("NO_API_KEY", "no key was chosen");
|
|
27
|
+
return chosen.value;
|
|
28
|
+
};
|
|
44
29
|
|
|
45
30
|
/** Never a flag: a flag lands in shell history and CI logs. */
|
|
46
|
-
export const runLogin = async (
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
options: { gateway: { type: "boolean", default: false } },
|
|
50
|
-
});
|
|
51
|
-
const name = values.gateway ? GATEWAY_KEY_ENV : TYPESAFE_KEY_ENV;
|
|
52
|
-
const key = await readSecret(
|
|
53
|
-
values.gateway ? "Vercel AI Gateway key: " : "TypeSafe API key (from typesafe.ai): ",
|
|
54
|
-
);
|
|
31
|
+
export const runLogin = async (): Promise<number> => {
|
|
32
|
+
const provider = await chooseProvider();
|
|
33
|
+
const key = await readSecret(provider.prompt);
|
|
55
34
|
if (key === "") throw new AbideError("NO_API_KEY", "nothing was entered");
|
|
56
|
-
const file = saveUserKey(name, key);
|
|
35
|
+
const file = saveUserKey(provider.name, key);
|
|
57
36
|
await showStatic(
|
|
58
|
-
Callout({
|
|
37
|
+
Callout({
|
|
38
|
+
tone: "ok",
|
|
39
|
+
title: `${provider.name} saved to ${file} (owner-only). Run abide init next.`,
|
|
40
|
+
}),
|
|
59
41
|
);
|
|
60
42
|
return 0;
|
|
61
43
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The slice of a stdin the reader needs, so a test can hand in a stream of its own. */
|
|
2
|
+
export type SecretInput = {
|
|
3
|
+
isTTY: boolean;
|
|
4
|
+
isRaw: boolean;
|
|
5
|
+
setRawMode(mode: boolean): unknown;
|
|
6
|
+
setEncoding(encoding: BufferEncoding): unknown;
|
|
7
|
+
/** A TTY or pipe has this; a redirected file does not. */
|
|
8
|
+
ref?(): unknown;
|
|
9
|
+
resume(): unknown;
|
|
10
|
+
pause(): unknown;
|
|
11
|
+
on(event: "data", listener: (chunk: string) => void): unknown;
|
|
12
|
+
off(event: "data", listener: (chunk: string) => void): unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SecretIo = { input: SecretInput; output: { write(text: string): unknown } };
|
|
16
|
+
|
|
17
|
+
const processIo = (): SecretIo => ({ input: process.stdin, output: process.stderr });
|
|
18
|
+
|
|
19
|
+
/** Reads one line without echoing it, so the key never lands in a terminal scrollback. */
|
|
20
|
+
export const readSecret = (prompt: string, io: SecretIo = processIo()): Promise<string> =>
|
|
21
|
+
new Promise((resolve) => {
|
|
22
|
+
const { input, output } = io;
|
|
23
|
+
output.write(prompt);
|
|
24
|
+
const wasRaw = input.isTTY ? input.isRaw : false;
|
|
25
|
+
if (input.isTTY) input.setRawMode(true);
|
|
26
|
+
input.setEncoding("utf8");
|
|
27
|
+
|
|
28
|
+
if (input.isTTY) input.ref?.();
|
|
29
|
+
input.resume();
|
|
30
|
+
|
|
31
|
+
let value = "";
|
|
32
|
+
const done = (): void => {
|
|
33
|
+
input.off("data", onData);
|
|
34
|
+
if (input.isTTY) input.setRawMode(wasRaw);
|
|
35
|
+
input.pause();
|
|
36
|
+
output.write("\n");
|
|
37
|
+
resolve(value.trim());
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const onData = (chunk: string): void => {
|
|
41
|
+
for (const ch of chunk) {
|
|
42
|
+
if (ch === "\u0003") {
|
|
43
|
+
done();
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
47
|
+
done();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
51
|
+
value = value.slice(0, -1);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
value += ch;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
input.on("data", onData);
|
|
58
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { glyph, palette } from "../theme.js";
|
|
4
|
+
|
|
5
|
+
export type PickerItem<T> = { value: T; label: string; hint?: string };
|
|
6
|
+
|
|
7
|
+
type PickerProps<T> = {
|
|
8
|
+
title: string;
|
|
9
|
+
items: readonly PickerItem<T>[];
|
|
10
|
+
onDone: (chosen: PickerItem<T> | undefined) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type State =
|
|
14
|
+
| { kind: "choosing"; index: number }
|
|
15
|
+
| { kind: "chosen"; index: number }
|
|
16
|
+
| { kind: "left"; index: number };
|
|
17
|
+
|
|
18
|
+
export function Picker<T>({ title, items, onDone }: PickerProps<T>) {
|
|
19
|
+
const { exit } = useApp();
|
|
20
|
+
const [state, setState] = useState<State>({ kind: "choosing", index: 0 });
|
|
21
|
+
// Keys can arrive faster than a re-render, so the handler reads the cursor from here, not from its closure.
|
|
22
|
+
const cursor = useRef(0);
|
|
23
|
+
const move = (index: number): void => {
|
|
24
|
+
cursor.current = index;
|
|
25
|
+
setState({ kind: "choosing", index });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
useInput(
|
|
29
|
+
(input, key) => {
|
|
30
|
+
if (key.upArrow || input === "k") {
|
|
31
|
+
move(Math.max(0, cursor.current - 1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key.downArrow || input === "j") {
|
|
35
|
+
move(Math.min(items.length - 1, cursor.current + 1));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.return) {
|
|
39
|
+
setState({ kind: "chosen", index: cursor.current });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.escape) setState({ kind: "left", index: cursor.current });
|
|
43
|
+
},
|
|
44
|
+
{ isActive: state.kind === "choosing" },
|
|
45
|
+
);
|
|
46
|
+
// Leave only after the answer has been drawn, not in the same tick it was picked.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (state.kind === "choosing") return;
|
|
49
|
+
onDone(state.kind === "chosen" ? items[state.index] : undefined);
|
|
50
|
+
exit();
|
|
51
|
+
// The pick is final; nothing about it changes afterwards.
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [state.kind]);
|
|
54
|
+
const current = items[state.index];
|
|
55
|
+
if (state.kind === "chosen") {
|
|
56
|
+
return (
|
|
57
|
+
<Text>
|
|
58
|
+
<Text color={palette.mist}>{title} </Text>
|
|
59
|
+
<Text color={palette.teal}>{current?.label}</Text>
|
|
60
|
+
</Text>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
65
|
+
<Text color={palette.cloud} bold>
|
|
66
|
+
{title}
|
|
67
|
+
</Text>
|
|
68
|
+
{items.map((item, index) => {
|
|
69
|
+
const active = index === state.index;
|
|
70
|
+
return (
|
|
71
|
+
<Box key={item.label}>
|
|
72
|
+
<Text color={active ? palette.teal : palette.ash}>{active ? glyph.arrow : " "} </Text>
|
|
73
|
+
<Text color={active ? palette.teal : palette.cloud} bold={active}>
|
|
74
|
+
{item.label}
|
|
75
|
+
</Text>
|
|
76
|
+
{item.hint ? <Text color={palette.mist}> {item.hint}</Text> : null}
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
<Text color={palette.ash}>
|
|
81
|
+
↑↓ move {glyph.dotSep} enter pick {glyph.dotSep} esc quit
|
|
82
|
+
</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/ui/render.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render } from "ink";
|
|
2
2
|
import type { ReactElement, ReactNode } from "react";
|
|
3
3
|
import { Live, type Progress } from "./components/Live.js";
|
|
4
|
+
import { Picker, type PickerItem } from "./components/Picker.js";
|
|
4
5
|
import { ErrorView } from "./views/ErrorView.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -46,6 +47,26 @@ export const showLive = async <T,>(job: LiveJob<T>): Promise<number> => {
|
|
|
46
47
|
return outcome.code;
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
/** Asks for one choice on a TTY and resolves to it, or to nothing when the person backs out. */
|
|
51
|
+
export const showPicker = async <T,>(
|
|
52
|
+
title: string,
|
|
53
|
+
items: readonly PickerItem<T>[],
|
|
54
|
+
): Promise<PickerItem<T> | undefined> => {
|
|
55
|
+
let chosen: PickerItem<T> | undefined;
|
|
56
|
+
const instance = render(
|
|
57
|
+
<Picker<T>
|
|
58
|
+
title={title}
|
|
59
|
+
items={items}
|
|
60
|
+
onDone={(item) => {
|
|
61
|
+
chosen = item;
|
|
62
|
+
}}
|
|
63
|
+
/>,
|
|
64
|
+
{ patchConsole: false, exitOnCtrlC: true },
|
|
65
|
+
);
|
|
66
|
+
await instance.waitUntilExit();
|
|
67
|
+
return chosen;
|
|
68
|
+
};
|
|
69
|
+
|
|
49
70
|
export const showError = async (error: unknown): Promise<void> => {
|
|
50
71
|
await showStatic(<ErrorView error={error} />);
|
|
51
72
|
};
|
|
@@ -3,7 +3,7 @@ import { Header } from "../components/Header.js";
|
|
|
3
3
|
import { palette } from "../theme.js";
|
|
4
4
|
|
|
5
5
|
const COMMANDS: [string, string][] = [
|
|
6
|
-
["login
|
|
6
|
+
["login", "store your TypeSafe or Vercel AI Gateway key in ~/.abide/.env, owner-only"],
|
|
7
7
|
["init [agent] [--project]", "hook into claude, codex, opencode, or every one found here"],
|
|
8
8
|
["compile", "compile the rubric now, in a headless Claude Code turn"],
|
|
9
9
|
["tune [--global]", "rewrite rules that never fire, with their statistics attached"],
|