@illuminis/comprism 0.1.4 → 0.1.5

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.
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.session = session;
36
37
  exports.showWelcome = showWelcome;
37
38
  /**
38
39
  * The first thing anybody sees.
@@ -95,33 +96,46 @@ async function session() {
95
96
  return reply.session;
96
97
  }
97
98
  /**
98
- * The mark: a prism splitting light.
99
+ * The mark: the app icon as a terminal can draw it cleanly. A five layer prism,
100
+ * each layer two blocks wider than the one above and a shade deeper, with
101
+ * three beams leaving its right side (up, level and down) and a ring of the
102
+ * icon's cyan to purple around it (owner, 28 September 2026).
99
103
  *
100
- * A four row pyramid, drawn only in the full block glyph. The first attempt
101
- * used quarter blocks and came out looking like a rendering fault, because
102
- * those glyphs sit differently in every terminal font and a mark cannot depend
103
- * on a font. A full block renders identically everywhere, so the only thing
104
- * carrying the design is the color, which is the point of a prism.
104
+ * Solid blocks only, no blended edges and no white, because a terminal draws a
105
+ * solid block sharply in any font and a white one vanishes on a white screen.
106
+ * Drawn two pixels to a character, so it is nine lines tall.
105
107
  *
106
- * Color is taken by COLUMN, not by row. The apex sits on the middle of the
107
- * spectrum and each row down widens to one more color on either side, so the
108
- * base carries all eight. That is a prism doing its job: one input at the top,
109
- * the whole spectrum at the bottom. Banding it by row would have shown four of
110
- * the eight and read as a striped triangle instead.
111
- *
112
- * The eight values come from the same helper as the rule beneath it, so the
113
- * mark and the rule can never drift apart.
108
+ * Without color the picture cannot be drawn, so the four row block pyramid
109
+ * stands in: full blocks only, which render the same in every font.
114
110
  */
115
- const MARK_BASE = 8;
111
+ const ICON = [
112
+ '..........................................53a6ed51a1ed509cee4f98ee4d93ee4c8eee4a89ee..................................................................',
113
+ '..............................54abed53a6ed51a1ed..............................4984ef477fef467bef......................................................',
114
+ '........................54abed53a6ed......................................................4476ef4371ef................................................',
115
+ '..................54abed53a6ed..................................................................426cf04067f0..........................................',
116
+ '..................53a6ed....................................a8d8f7a8d8f7..............................3f62f0..........................................',
117
+ '............53a6ed51a1ed....................................a8d8f7a8d8f7..............................3f5ff0425ef0..................4292f23f8af43b82f6',
118
+ '............51a1ed....................................74aef074aef074aef074aef0..............................455cf0......49a2ee469af0..................',
119
+ '............509cee....................................74aef074aef074aef074aef0........................54bbe851b3ea4daaec..............................',
120
+ '............4f98ee..............................4f86e64f86e64f86e64f86e64f86e64f86e6......5bcbe458c3e6......4b59f1....................................',
121
+ '............4d93ee..............................4f86e64f86e64f86e64f86e64f86e64f86e6f07a36f0e35befe25aeee259ece158eae057e9df56e8de55e6de54e4dd53e3dc52',
122
+ '............4c8eee........................3e60d83e60d83e60d83e60d83e60d83e60d83e60d83e60d8ed7435ea6e34......5056f1....................................',
123
+ '............4a89ee........................3e60d83e60d83e60d83e60d83e60d83e60d83e60d83e60d8............e76932e46331e15d30..............................',
124
+ '............4984ef477fef............2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a......5355f15653f2......de572fdb512e..................',
125
+ '..................467bef............2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a2e3a9a......5653f2........................d84c2cd5462bd2402a',
126
+ '..................4476ef4371ef..................................................................5653f25952f2..........................................',
127
+ '........................426cf04067f0......................................................5653f25952f2................................................',
128
+ '..............................3f62f03f5ff0425ef0..............................5355f15653f25952f2......................................................',
129
+ '..........................................455cf0485bf14b59f14e58f15056f15355f15653f2..................................................................',
130
+ ];
116
131
  function mark() {
132
+ const picture = ui.pixelArt(ICON);
133
+ if (picture.length)
134
+ return picture;
117
135
  const rows = [];
118
136
  for (let r = 1; r <= 4; r++) {
119
- const blocks = r * 2;
120
- const left = (MARK_BASE - blocks) / 2;
121
- let line = ' '.repeat(left);
122
- for (let i = 0; i < blocks; i++)
123
- line += ui.spectrumAt(left + i, '\u2588');
124
- rows.push(line + ' '.repeat(left));
137
+ const left = 4 - r;
138
+ rows.push(' '.repeat(left) + '\u2588'.repeat(r * 2) + ' '.repeat(left));
125
139
  }
126
140
  return rows;
127
141
  }
@@ -133,19 +147,31 @@ function row(label, value) {
133
147
  function readable(name) {
134
148
  return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase());
135
149
  }
136
- async function showWelcome() {
150
+ /** `known` is who this machine is, when the caller already has it (the last
151
+ * answer, drawn at once while a fresh one is asked for). */
152
+ async function showWelcome(known, signingIn = false, withMark = true) {
137
153
  const conn = (0, connection_1.readConnection)();
138
154
  const cfg = (0, config_1.loadConfig)();
139
155
  const folder = (0, settings_1.currentWorkspace)();
140
- const me = await session();
141
- const [m1, m2, m3, m4] = mark();
142
- out();
143
- out(` ${m1} ${ui.c.bold('CompletionPrism')} ${ui.c.dim(`v${version_1.COMPRISM_VERSION}`)}`);
144
- out(` ${m2} ${ui.c.muted('Optimize AI completion economics by optimizing work.')}`);
145
- out(` ${m3}`);
146
- out(` ${m4}`);
147
- out(ui.prismRule());
148
- out();
156
+ const me = known !== undefined ? known : await session();
157
+ // The mark once per start. Signing in first draws it; the session that
158
+ // follows draws only the account, so nobody sees the same screen twice.
159
+ if (withMark) {
160
+ const art = mark();
161
+ // The name and the line under it sit beside the middle of the mark.
162
+ const at = Math.max(0, Math.floor(art.length / 2) - 1);
163
+ const beside = {
164
+ [at]: `${ui.c.bold('CompletionPrism')} ${ui.c.dim(`v${version_1.COMPRISM_VERSION}`)}`,
165
+ [at + 1]: ui.c.muted('Optimize AI completion economics by optimizing work.'),
166
+ };
167
+ out();
168
+ art.forEach((line, i) => out(beside[i] ? ` ${line} ${beside[i]}` : ` ${line}`));
169
+ out(ui.prismRule());
170
+ out();
171
+ }
172
+ // Signing in comes next and says everything else itself.
173
+ if (signingIn && !conn?.url)
174
+ return;
149
175
  if (!conn?.url) {
150
176
  // Nothing else here is knowable yet, and the one useful thing to say is the
151
177
  // single command that changes that.
@@ -153,8 +179,12 @@ async function showWelcome() {
153
179
  out(row('Comparing', cfg.defaultModel || 'not set'));
154
180
  out(row('Workspace', `${folder.path} ${ui.c.dim('(where you are)')}`));
155
181
  out();
156
- out(` ${ui.c.dim('start with')} ${ui.c.bold('comprism login')}`);
157
- out();
182
+ // Not said when signing in follows at once: naming a command to type, just
183
+ // above the sign in it would start, reads as a second step.
184
+ if (!signingIn) {
185
+ out(` ${ui.c.dim('start with')} ${ui.c.bold('comprism login')}`);
186
+ out();
187
+ }
158
188
  return;
159
189
  }
160
190
  const company = me?.company || readable(conn.tenant || 'unknown');
@@ -165,10 +195,16 @@ async function showWelcome() {
165
195
  // machine happens to hold. A client that can only echo its own stored address
166
196
  // can never tell a person that the address is the thing that is wrong, which
167
197
  // is exactly what happened when a machine sat pointed at a dead laptop port.
168
- const address = (me?.workspace_url || conn.url).replace(/^https?:\/\//, '');
198
+ // Except on this laptop: there the server names its public address, which is
199
+ // not where this machine is talking, and "you are on production" is the
200
+ // single most confusing thing a developer testing locally can be told.
201
+ const local = (0, connection_1.isLocalAddress)(conn.url);
202
+ const address = local
203
+ ? `${conn.url.replace(/^https?:\/\//, '')} ${ui.c.dim('(this laptop)')}`
204
+ : (me?.workspace_url || conn.url).replace(/^https?:\/\//, '');
169
205
  out(row('User', who));
170
206
  out(row('Company', company));
171
- out(row('Server', ui.c.dim(address)));
207
+ out(row('Server', local ? address : ui.c.dim(address)));
172
208
  out(row('Comparing', `${cfg.defaultModel || ui.c.amber('not set')} ${ui.c.dim('change with `comprism model`')}`));
173
209
  out(row('Workspace', `${folder.path} ${ui.c.dim(folder.source === 'cwd' ? '(where you are)' : '(saved)')}`));
174
210
  if (plan) {
@@ -68,6 +68,8 @@ export interface GatewayReply {
68
68
  answer: string | null;
69
69
  /** The receipt, already worded by the server. Clients never compose one. */
70
70
  receipt: string | null;
71
+ /** The receipt card, worded by the server for every client. */
72
+ card?: import('../agent/render').ReceiptCard | null;
71
73
  /** The model that actually answered. */
72
74
  ran_on: string | null;
73
75
  /** What this job cost to finish, on the model we ran. */
@@ -262,6 +264,8 @@ export interface SessionRecord {
262
264
  conversation_kept?: boolean;
263
265
  cost_usd: number;
264
266
  jobs: number;
267
+ /** The session as a list entry, in the service's words: name, then where and when. */
268
+ card?: string[];
265
269
  }
266
270
  export interface SessionsReply extends Partial<SessionRecord> {
267
271
  lines: string[];
@@ -47,6 +47,9 @@ export declare class Keyboard {
47
47
  /** Esc on its own (manual 6.7). Returns true when it was used, so the key
48
48
  * goes no further; false passes it to the reader as before. */
49
49
  onEscape: (() => boolean) | null;
50
+ /** The command list under the prompt (manual 6.4): Up, Down, Enter and Tab
51
+ * go to it while it is showing. Returns true when it used the key. */
52
+ onMenuKey: ((key: 'up' | 'down' | 'enter' | 'tab') => boolean) | null;
50
53
  /** Ctrl+O, the short or full view (manual 6.9). */
51
54
  onFullView: (() => void) | null;
52
55
  /** Ctrl+G, the request in your own editor (manual 6.6). */
@@ -57,6 +57,9 @@ class Keyboard {
57
57
  /** Esc on its own (manual 6.7). Returns true when it was used, so the key
58
58
  * goes no further; false passes it to the reader as before. */
59
59
  onEscape = null;
60
+ /** The command list under the prompt (manual 6.4): Up, Down, Enter and Tab
61
+ * go to it while it is showing. Returns true when it used the key. */
62
+ onMenuKey = null;
60
63
  /** Ctrl+O, the short or full view (manual 6.9). */
61
64
  onFullView = null;
62
65
  /** Ctrl+G, the request in your own editor (manual 6.6). */
@@ -167,6 +170,10 @@ class Keyboard {
167
170
  // arrow or another sequence and goes to the reader.
168
171
  if (s === '\x1b' && this.onEscape?.())
169
172
  return;
173
+ const menu = { '\x1b[A': 'up', '\x1bOA': 'up', '\x1b[B': 'down', '\x1bOB': 'down',
174
+ '\r': 'enter', '\t': 'tab' }[s];
175
+ if (menu && this.onMenuKey?.(menu))
176
+ return;
170
177
  if (this.vi) {
171
178
  if (s === '\x1b') {
172
179
  this.normal = true;
@@ -0,0 +1,14 @@
1
+ export interface LastLogin {
2
+ email: string;
3
+ /** Exactly what was typed or found as the workspace, so a local one stays local. */
4
+ workspace: string;
5
+ /** Whether that workspace was typed by the person (only then may it be local). */
6
+ typed: boolean;
7
+ }
8
+ export declare function readLast(): LastLogin | null;
9
+ export declare function saveLast(last: LastLogin): void;
10
+ export declare function forgetLast(): void;
11
+ export declare const keychainAvailable: () => boolean;
12
+ export declare function savedPassword(email: string, workspace: string): string | null;
13
+ export declare function savePassword(email: string, workspace: string, password: string): boolean;
14
+ export declare function forgetPassword(email: string, workspace: string): void;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.keychainAvailable = void 0;
37
+ exports.readLast = readLast;
38
+ exports.saveLast = saveLast;
39
+ exports.forgetLast = forgetLast;
40
+ exports.savedPassword = savedPassword;
41
+ exports.savePassword = savePassword;
42
+ exports.forgetPassword = forgetPassword;
43
+ /**
44
+ * Who signed in last on this machine, so signing in again is one key.
45
+ *
46
+ * The email and the workspace address are kept in a small file. The password
47
+ * is never written to that file or anywhere else this tool controls: on a Mac,
48
+ * and only when the person says yes, it goes into their own login keychain,
49
+ * the same place Safari and git keep theirs, and it is read back from there.
50
+ * Signing out leaves both, so the next `login` is Enter; `login --forget`
51
+ * removes both.
52
+ */
53
+ const node_child_process_1 = require("node:child_process");
54
+ const fs = __importStar(require("node:fs"));
55
+ const path = __importStar(require("node:path"));
56
+ const config_1 = require("./config");
57
+ function file() {
58
+ return path.join((0, config_1.ensureHome)(), 'last-login.json');
59
+ }
60
+ function readLast() {
61
+ try {
62
+ const got = JSON.parse(fs.readFileSync(file(), 'utf8'));
63
+ return got.email && got.workspace ? got : null;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ function saveLast(last) {
70
+ try {
71
+ fs.writeFileSync(file(), JSON.stringify(last), { mode: 0o600 });
72
+ }
73
+ catch { /* next login asks */ }
74
+ }
75
+ function forgetLast() {
76
+ const last = readLast();
77
+ if (last)
78
+ forgetPassword(last.email, last.workspace);
79
+ try {
80
+ fs.rmSync(file(), { force: true });
81
+ }
82
+ catch { /* nothing to remove */ }
83
+ }
84
+ /** The keychain entry's name: one per workspace, so two workspaces never mix. */
85
+ const service = (workspace) => `CompletionPrism ${workspace}`;
86
+ const keychainAvailable = () => process.platform === 'darwin';
87
+ exports.keychainAvailable = keychainAvailable;
88
+ function savedPassword(email, workspace) {
89
+ if (!(0, exports.keychainAvailable)() || !email || !workspace)
90
+ return null;
91
+ const r = (0, node_child_process_1.spawnSync)('security', ['find-generic-password', '-a', email, '-s', service(workspace), '-w'], { encoding: 'utf8' });
92
+ return r.status === 0 ? r.stdout.replace(/\n$/, '') : null;
93
+ }
94
+ function savePassword(email, workspace, password) {
95
+ if (!(0, exports.keychainAvailable)())
96
+ return false;
97
+ const r = (0, node_child_process_1.spawnSync)('security', ['add-generic-password', '-U', '-a', email, '-s', service(workspace),
98
+ '-l', service(workspace), '-w', password], { stdio: 'ignore' });
99
+ return r.status === 0;
100
+ }
101
+ function forgetPassword(email, workspace) {
102
+ if (!(0, exports.keychainAvailable)())
103
+ return;
104
+ (0, node_child_process_1.spawnSync)('security', ['delete-generic-password', '-a', email, '-s', service(workspace)], { stdio: 'ignore' });
105
+ }
@@ -0,0 +1 @@
1
+ export declare function pick(entries: Array<string | string[]>, hint: string, start?: number, numbered?: boolean): Promise<number | null>;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.pick = pick;
40
+ /**
41
+ * Choosing one thing from a list with the arrow keys.
42
+ *
43
+ * Each entry is one or more lines: the first is its name, the rest are quieter
44
+ * detail. Entries are separated by a blank line so each reads as its own item.
45
+ * Up and Down move, Enter picks, Esc (or Ctrl+C) leaves without picking.
46
+ *
47
+ * Only as many entries as fit the window are drawn, and the view follows the
48
+ * marker, so a long list scrolls instead of running off the screen (which would
49
+ * leave the redraw writing over lines it can no longer reach). Every line is
50
+ * cut to the window's width for the same reason: a wrapped line throws the
51
+ * redraw off by one. Off a terminal there is nothing to move through, so it
52
+ * returns null and the caller falls back to its typed form.
53
+ *
54
+ * With `numbered`, every entry carries its number, and typing a number moves
55
+ * the marker straight to it. A list of two hundred sessions is not something
56
+ * anybody should have to arrow through to reach the one they can already see.
57
+ */
58
+ const node_readline_1 = __importDefault(require("node:readline"));
59
+ const ui = __importStar(require("./ui"));
60
+ const CSI = `${String.fromCharCode(27)}[`;
61
+ function pick(entries, hint, start = 0, numbered = false) {
62
+ if (!process.stdin.isTTY || !process.stdout.isTTY || entries.length === 0)
63
+ return Promise.resolve(null);
64
+ const items = entries.map((e) => (Array.isArray(e) ? e : [e]));
65
+ const width = Math.max(20, (process.stdout.columns || 100) - 6);
66
+ const cut = (s) => (s.length > width ? `${s.slice(0, width - 3)}...` : s);
67
+ const tall = Math.max(...items.map((i) => i.length)) + 1;
68
+ // Room for the entries, the hint and a line either side.
69
+ const fits = Math.max(1, Math.floor(((process.stdout.rows || 24) - 4) / tall));
70
+ let at = Math.min(Math.max(0, start), items.length - 1);
71
+ let top = 0;
72
+ let drawn = 0;
73
+ // The number typed so far, when the entries are numbered.
74
+ let typed = '';
75
+ const digits = String(items.length).length;
76
+ const label = (i, line) => (numbered ? `${String(i + 1).padStart(digits)}. ${line}` : line);
77
+ const draw = () => {
78
+ if (at < top)
79
+ top = at;
80
+ if (at >= top + fits)
81
+ top = at - fits + 1;
82
+ if (drawn)
83
+ process.stdout.write(`${CSI}${drawn}A\r`);
84
+ const lines = [];
85
+ if (top > 0)
86
+ lines.push(ui.c.dim(` ${top} more above`));
87
+ items.slice(top, top + fits).forEach((item, k) => {
88
+ const i = top + k;
89
+ const chosen = i === at;
90
+ item.forEach((line, n) => {
91
+ if (n === 0) {
92
+ const name = cut(label(i, line));
93
+ lines.push(chosen ? ` ${ui.c.bold(ui.c.blue(`▸ ${name}`))}` : ` ${ui.c.bold(name)}`);
94
+ }
95
+ else
96
+ lines.push(` ${ui.c.dim(cut(numbered ? `${' '.repeat(digits + 2)}${line}` : line))}`);
97
+ });
98
+ lines.push('');
99
+ });
100
+ const below = items.length - (top + fits);
101
+ if (below > 0)
102
+ lines.push(ui.c.dim(` ${below} more below`));
103
+ lines.push(` ${ui.c.dim(hint)}${typed ? ` ${ui.c.bold(`number ${typed}`)}` : ''}`);
104
+ // The same height every time, so the next redraw lands exactly on this one.
105
+ while (lines.length < drawn)
106
+ lines.push('');
107
+ process.stdout.write(`${lines.map((l) => `${CSI}2K${l}`).join('\n')}\n`);
108
+ drawn = lines.length;
109
+ };
110
+ return new Promise((resolve) => {
111
+ node_readline_1.default.emitKeypressEvents(process.stdin);
112
+ const wasRaw = process.stdin.isRaw;
113
+ process.stdin.setRawMode(true);
114
+ process.stdin.resume();
115
+ const done = (choice) => {
116
+ process.stdin.off('keypress', onKey);
117
+ process.stdin.setRawMode(wasRaw);
118
+ process.stdin.pause();
119
+ resolve(choice);
120
+ };
121
+ const onKey = (s, key) => {
122
+ if (!key && !(numbered && /^[0-9]$/.test(s ?? '')))
123
+ return;
124
+ if (numbered && /^[0-9]$/.test(s ?? '')) {
125
+ // A number past the end starts again from this digit, so a mistyped
126
+ // number is corrected by typing the right one, not by clearing first.
127
+ const next = Number(typed + s) <= items.length ? typed + s : s;
128
+ const n = Number(next);
129
+ typed = n >= 1 && n <= items.length ? next : '';
130
+ if (typed)
131
+ at = n - 1;
132
+ draw();
133
+ }
134
+ else if (numbered && key.name === 'backspace' && typed) {
135
+ typed = typed.slice(0, -1);
136
+ if (typed)
137
+ at = Number(typed) - 1;
138
+ draw();
139
+ }
140
+ else if (key.name === 'up' || key.name === 'k') {
141
+ typed = '';
142
+ at = Math.max(0, at - 1);
143
+ draw();
144
+ }
145
+ else if (key.name === 'down' || key.name === 'j') {
146
+ typed = '';
147
+ at = Math.min(items.length - 1, at + 1);
148
+ draw();
149
+ }
150
+ else if (key.name === 'return')
151
+ done(at);
152
+ else if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c'))
153
+ done(null);
154
+ };
155
+ process.stdin.on('keypress', onKey);
156
+ draw();
157
+ });
158
+ }
@@ -2,6 +2,9 @@
2
2
  export declare function closePrompt(): void;
3
3
  /** Ask a question and wait for the answer. */
4
4
  export declare function ask(prompt: string): Promise<string>;
5
+ /** Ask, telling a cancel (Ctrl+C, input closed) apart from an empty answer:
6
+ * null is "never mind", "" is Enter. */
7
+ export declare function askOrCancel(prompt: string): Promise<string | null>;
5
8
  /**
6
9
  * Ask for a secret, and do not put it on the screen.
7
10
  *
package/out/lib/prompt.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.closePrompt = closePrompt;
7
7
  exports.ask = ask;
8
+ exports.askOrCancel = askOrCancel;
8
9
  exports.askHidden = askHidden;
9
10
  exports.freeText = freeText;
10
11
  exports.subject = subject;
@@ -24,6 +25,11 @@ function open() {
24
25
  output: process.stdout,
25
26
  terminal: process.stdin.isTTY === true,
26
27
  });
28
+ // Ctrl+C closes it. The next question then opens a new one instead of
29
+ // asking a closed reader, which threw and ended the whole program.
30
+ const mine = reader;
31
+ mine.once('close', () => { if (reader === mine)
32
+ reader = null; });
27
33
  }
28
34
  return reader;
29
35
  }
@@ -51,6 +57,19 @@ function ask(prompt) {
51
57
  });
52
58
  });
53
59
  }
60
+ /** Ask, telling a cancel (Ctrl+C, input closed) apart from an empty answer:
61
+ * null is "never mind", "" is Enter. */
62
+ function askOrCancel(prompt) {
63
+ return new Promise((resolve) => {
64
+ const r = open();
65
+ const closed = () => resolve(null);
66
+ r.once('close', closed);
67
+ r.question(prompt, (answer) => {
68
+ r.off('close', closed);
69
+ resolve(answer.trim());
70
+ });
71
+ });
72
+ }
54
73
  /**
55
74
  * Ask for a secret, and do not put it on the screen.
56
75
  *
@@ -86,6 +105,8 @@ function askHidden(prompt) {
86
105
  process.stdout.write(`\r${prompt}${"\u2022".repeat(inner.line.length)}`);
87
106
  };
88
107
  return new Promise((resolve) => {
108
+ // Ctrl+C at the password is "never mind", answered as nothing, never a crash.
109
+ reader.once('close', () => { inner._writeToOutput = asItWas; resolve(''); });
89
110
  reader.question(prompt, (answer) => {
90
111
  // Put readline back the way it was, whatever happened, or every later
91
112
  // question on this reader would come out as dots.
@@ -0,0 +1,13 @@
1
+ import type { SessionOut } from './gateway';
2
+ import type { CommandList } from './commandlist';
3
+ export interface StartCache {
4
+ /** Which workspace these answers came from; another workspace's are never used. */
5
+ url?: string;
6
+ me?: SessionOut | null;
7
+ hasKey?: boolean;
8
+ /** The project line, by project folder. */
9
+ lines?: Record<string, string>;
10
+ list?: CommandList | null;
11
+ }
12
+ export declare function readStart(url: string): StartCache;
13
+ export declare function saveStart(url: string, change: Partial<StartCache>): void;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readStart = readStart;
37
+ exports.saveStart = saveStart;
38
+ /**
39
+ * What the service said the last time a session opened, kept so the next one
40
+ * can open at once.
41
+ *
42
+ * Opening a session asks the service four things (who you are, whether a key
43
+ * is stored, the project line, the command list). Asked from a laptop whose
44
+ * database is a region away, each takes seconds, and the prompt waited for all
45
+ * of them. Now the last answers are drawn immediately and the same questions
46
+ * are asked again in the background; the fresh answers are kept for next time,
47
+ * and a change that matters (no key any more) is said as soon as it is known.
48
+ *
49
+ * Only the service's own answers are kept, exactly as sent. Nothing here is
50
+ * decided on this machine, and a missing or unreadable file simply means the
51
+ * session waits for the service, as it always did.
52
+ */
53
+ const fs = __importStar(require("node:fs"));
54
+ const path = __importStar(require("node:path"));
55
+ const config_1 = require("./config");
56
+ function file() {
57
+ return path.join((0, config_1.ensureHome)(), 'start-cache.json');
58
+ }
59
+ function readStart(url) {
60
+ try {
61
+ const got = JSON.parse(fs.readFileSync(file(), 'utf8'));
62
+ return got.url === url ? got : { url };
63
+ }
64
+ catch {
65
+ return { url };
66
+ }
67
+ }
68
+ function saveStart(url, change) {
69
+ try {
70
+ const now = readStart(url);
71
+ const next = {
72
+ ...now, ...change, url,
73
+ lines: { ...(now.lines ?? {}), ...(change.lines ?? {}) },
74
+ };
75
+ fs.writeFileSync(file(), JSON.stringify(next), { mode: 0o600 });
76
+ }
77
+ catch { /* a cache that cannot be written only means the next start waits */ }
78
+ }
package/out/lib/ui.d.ts CHANGED
@@ -35,6 +35,14 @@ export declare const c: {
35
35
  * and the brand is the one thing a customer notices before the engine.
36
36
  */
37
37
  export declare function spectrumAt(index: number, glyph: string): string;
38
+ /**
39
+ * A small picture, drawn two pixels to a character with the half block glyph:
40
+ * the top pixel is the glyph's color and the bottom one its background, which
41
+ * makes each pixel square in an ordinary terminal font. Each row of `pixels`
42
+ * is six hex digits per pixel, `......` for a clear one, and two rows make one
43
+ * line. Returns nothing without color, because the picture is only color.
44
+ */
45
+ export declare function pixelArt(pixels: string[]): string[];
38
46
  export declare function prismRule(w?: number): string;
39
47
  export declare function banner(subtitle?: string): string;
40
48
  export declare function money(v: number | null): string;