@illuminis/comprism 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.
Files changed (80) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +281 -0
  3. package/out/agent/command.d.ts +86 -0
  4. package/out/agent/command.js +259 -0
  5. package/out/agent/render.d.ts +97 -0
  6. package/out/agent/render.js +255 -0
  7. package/out/agent/session.d.ts +175 -0
  8. package/out/agent/session.js +573 -0
  9. package/out/commands/ask.d.ts +1 -0
  10. package/out/commands/ask.js +146 -0
  11. package/out/commands/codemap.d.ts +2 -0
  12. package/out/commands/codemap.js +151 -0
  13. package/out/commands/commands-thin.d.ts +39 -0
  14. package/out/commands/commands-thin.js +182 -0
  15. package/out/commands/install.d.ts +163 -0
  16. package/out/commands/install.js +543 -0
  17. package/out/commands/keys.d.ts +55 -0
  18. package/out/commands/keys.js +344 -0
  19. package/out/commands/login.d.ts +9 -0
  20. package/out/commands/login.js +384 -0
  21. package/out/commands/repl.d.ts +1 -0
  22. package/out/commands/repl.js +752 -0
  23. package/out/commands/settings.d.ts +21 -0
  24. package/out/commands/settings.js +244 -0
  25. package/out/commands/welcome.d.ts +1 -0
  26. package/out/commands/welcome.js +196 -0
  27. package/out/executor/documents.d.ts +40 -0
  28. package/out/executor/documents.js +170 -0
  29. package/out/executor/files.d.ts +2 -0
  30. package/out/executor/files.js +360 -0
  31. package/out/executor/git.d.ts +48 -0
  32. package/out/executor/git.js +132 -0
  33. package/out/executor/hooks.d.ts +67 -0
  34. package/out/executor/hooks.js +247 -0
  35. package/out/executor/index.d.ts +29 -0
  36. package/out/executor/index.js +221 -0
  37. package/out/executor/notebook.d.ts +2 -0
  38. package/out/executor/notebook.js +147 -0
  39. package/out/executor/paths.d.ts +15 -0
  40. package/out/executor/paths.js +126 -0
  41. package/out/executor/shell.d.ts +41 -0
  42. package/out/executor/shell.js +336 -0
  43. package/out/graph/build.d.ts +45 -0
  44. package/out/graph/build.js +91 -0
  45. package/out/graph/facts.d.ts +47 -0
  46. package/out/graph/facts.js +12 -0
  47. package/out/graph/files.d.ts +45 -0
  48. package/out/graph/files.js +207 -0
  49. package/out/graph/read-locales.d.ts +29 -0
  50. package/out/graph/read-locales.js +246 -0
  51. package/out/graph/read-python.d.ts +11 -0
  52. package/out/graph/read-python.js +115 -0
  53. package/out/graph/read-typescript.d.ts +16 -0
  54. package/out/graph/read-typescript.js +292 -0
  55. package/out/graph/sync.d.ts +66 -0
  56. package/out/graph/sync.js +242 -0
  57. package/out/lib/attach.d.ts +62 -0
  58. package/out/lib/attach.js +228 -0
  59. package/out/lib/config.d.ts +93 -0
  60. package/out/lib/config.js +198 -0
  61. package/out/lib/connection.d.ts +73 -0
  62. package/out/lib/connection.js +188 -0
  63. package/out/lib/gateway.d.ts +239 -0
  64. package/out/lib/gateway.js +171 -0
  65. package/out/lib/prompt.d.ts +34 -0
  66. package/out/lib/prompt.js +108 -0
  67. package/out/lib/types.d.ts +417 -0
  68. package/out/lib/types.js +21 -0
  69. package/out/lib/ui.d.ts +114 -0
  70. package/out/lib/ui.js +265 -0
  71. package/out/lib/version.d.ts +24 -0
  72. package/out/lib/version.js +27 -0
  73. package/out/lib/voice.d.ts +50 -0
  74. package/out/lib/voice.js +218 -0
  75. package/out/postinstall.d.ts +2 -0
  76. package/out/postinstall.js +92 -0
  77. package/out/thin.d.ts +2 -0
  78. package/out/thin.js +259 -0
  79. package/package.json +101 -0
  80. package/scripts/read_python.py +270 -0
@@ -0,0 +1,239 @@
1
+ export interface Decision {
2
+ /** The model that finishes the work for least. Null when we could not decide. */
3
+ model: string | null;
4
+ /** What the caller would have run without us. */
5
+ would_have_used: string;
6
+ tier: string;
7
+ signals: string[];
8
+ /**
9
+ * True only when the figure rests on a close match in the corpus. False means
10
+ * a coarser lookup answered, and every surface must show it as an estimate.
11
+ */
12
+ confident: boolean;
13
+ rung: string | null;
14
+ rationale: string | null;
15
+ }
16
+ /** One model, as a person choosing between them needs to see it. */
17
+ export interface CatalogModel {
18
+ key: string;
19
+ id: string;
20
+ label: string;
21
+ provider: string;
22
+ tier: string;
23
+ /** True when this workspace can call it with no further setup. */
24
+ reachable: boolean;
25
+ /** Why it cannot be reached, in words a person can act on. */
26
+ why_not: string | null;
27
+ }
28
+ /** One vendor and its models. */
29
+ export interface CatalogProvider {
30
+ provider: string;
31
+ label: string;
32
+ has_key: boolean;
33
+ permitted: boolean;
34
+ models: CatalogModel[];
35
+ }
36
+ export interface GatewayReply {
37
+ intent: 'decide' | 'answer' | 'catalog' | 'store_key' | 'session' | 'job_start' | 'resolve_workspace' | 'sign_in' | 'sign_out' | 'attach' | 'transcribe' | 'graph';
38
+ client: string;
39
+ decision: Decision;
40
+ answer: string | null;
41
+ /** The receipt, already worded by the server. Clients never compose one. */
42
+ receipt: string | null;
43
+ /** The model that actually answered. */
44
+ ran_on: string | null;
45
+ /** What this job cost to finish, on the model we ran. */
46
+ cost_usd: number | null;
47
+ /** What it would have cost on the model the caller named. */
48
+ would_have_cost_usd: number | null;
49
+ /** The difference. Negative means we spent more, and it is shown either way. */
50
+ saved_usd: number | null;
51
+ /** True when any figure rests on a coarse lookup rather than a close match. */
52
+ estimated: boolean;
53
+ /** Where the job opens in full, every figure traceable to its records. */
54
+ details_url: string | null;
55
+ /** Every vendor and model. Present only for a catalog request. */
56
+ providers: CatalogProvider[];
57
+ /** Whether this machine's credential was actually revoked. Present only
58
+ * when signing out. */
59
+ signed_out: boolean;
60
+ /** Whether a key was kept. Present only when storing one. */
61
+ stored: boolean;
62
+ /** What happened, in words a person can act on. */
63
+ message: string | null;
64
+ /**
65
+ * Did this workspace's build serve the intent that was asked for.
66
+ *
67
+ * False is a COMPLETE answer meaning "your workspace is behind", never an
68
+ * error and never an absence. A caller that reads it as an absence repeats
69
+ * the 17 September fault: a tool asked a workspace for its vendor list, the
70
+ * build did not carry that question, and the refusal was reported to the
71
+ * person as "you have no keys".
72
+ *
73
+ * Optional, because a workspace older than this field does not send it, and
74
+ * absent must read as "it answered", not as "it refused".
75
+ */
76
+ served?: boolean;
77
+ /**
78
+ * False when some part of this reply could not be established.
79
+ *
80
+ * An empty list in a reply with `complete: false` is NOT a list of nothing,
81
+ * it is a list we failed to obtain. Reading the two the same way is the whole
82
+ * of the 17 September fault, and it was present on both sides of the wire.
83
+ * Optional, because a workspace older than this field does not send it, and
84
+ * absent must read as "established".
85
+ */
86
+ complete?: boolean;
87
+ /** Every intent this workspace's build does serve. Empty on an older build. */
88
+ intents?: string[];
89
+ /** Present only for a graph request: what the map holds, what it still needs,
90
+ * or the answer to one of the five questions. */
91
+ graph?: Record<string, unknown>;
92
+ /** Who this machine is signed in as. Present only for a session request. */
93
+ session?: SessionOut | null;
94
+ /** Permission for one coding job. Present only for a job_start request. */
95
+ job?: JobOut | null;
96
+ /** Present only for a sign_in request. */
97
+ sign_in?: SignInOut | null;
98
+ /** Present only for an attach or transcribe request. */
99
+ upload?: UploadOut | null;
100
+ /**
101
+ * True when the request was understood, allowed and acted on.
102
+ *
103
+ * False with `served: true` is a REFUSAL carrying its reason in `message`,
104
+ * and it is a complete answer. Without this a caller cannot tell "your plan
105
+ * does not include this" from "this workspace has never heard of it", and
106
+ * those have opposite fixes: one is a purchase, the other is a deploy.
107
+ */
108
+ granted?: boolean;
109
+ /**
110
+ * Did the workspace actually answer.
111
+ *
112
+ * False means we never heard from it, so EVERY other field here is this
113
+ * client's own placeholder rather than anything the workspace said. Without
114
+ * this a caller could not tell "the workspace says you have no provider key"
115
+ * from "the workspace did not answer", and it read the second as the first:
116
+ * somebody whose keys were all present and correct was told none was stored
117
+ * and walked into a form asking them to paste one.
118
+ */
119
+ ok: boolean;
120
+ }
121
+ /** Permission to send one file, and the address to send it to. */
122
+ export interface UploadOut {
123
+ /** Presented at the upload address in place of the workspace credential. */
124
+ ticket: string;
125
+ expires_in: number;
126
+ /** Named by the SERVER. A client composes no addresses of its own. */
127
+ upload_path: string;
128
+ }
129
+ /** Permission to run one coding job, and the pass that opens its stream. */
130
+ export interface JobOut {
131
+ job_id: string;
132
+ /**
133
+ * Presented by the stream in place of the workspace credential.
134
+ *
135
+ * One job, one use, two minutes. The job was already authorized by the one
136
+ * service before this was issued, so the stream decides nothing.
137
+ */
138
+ ticket: string;
139
+ expires_in: number;
140
+ /**
141
+ * What is left of this month's included coding spend, read ONCE by the
142
+ * service. The stream announces this same figure rather than reading it
143
+ * again, because two readings are how a job is authorized against one number
144
+ * and then tells the person watching it another.
145
+ */
146
+ allowance_remaining_usd: number | null;
147
+ stream_path: string;
148
+ }
149
+ /** Everything a client's header prints, answered by the workspace in one call. */
150
+ /** What signing in returns. The credential appears here once and never again. */
151
+ export interface SignInOut {
152
+ token: string;
153
+ /**
154
+ * This machine's own credential, returned ONCE.
155
+ *
156
+ * Nothing in the product ever returns it again, which is what makes revoking
157
+ * it in the portal a meaningful act rather than a gesture.
158
+ */
159
+ machine_credential: string;
160
+ tenant: string;
161
+ /** Computed by the SERVER, never guessed by this tool. */
162
+ workspace_url: string;
163
+ /** True when a second factor is needed. An answer, not a failure. */
164
+ needs_second_factor: boolean;
165
+ challenge: string;
166
+ }
167
+ export interface SessionOut {
168
+ signed_in: boolean;
169
+ user_email: string;
170
+ /** The company as a person says it, not the schema name. */
171
+ company: string;
172
+ tenant: string;
173
+ /**
174
+ * The address the workspace says it answers on, computed by the SERVER.
175
+ *
176
+ * Printed in preference to the address this machine happens to hold, because
177
+ * a client echoing its own stored address can never tell a person that the
178
+ * address is the thing that is wrong.
179
+ */
180
+ workspace_url: string;
181
+ comparing: string;
182
+ plan: string;
183
+ requests_per_minute: number;
184
+ requests_per_month: number;
185
+ vendors_with_key: number;
186
+ vendor_names: string[];
187
+ /** False when the vendor lookup could not answer. The count is then meaningless. */
188
+ vendors_known?: boolean;
189
+ machine: string;
190
+ can_write: boolean;
191
+ }
192
+ export interface Ask {
193
+ intent: 'decide' | 'answer' | 'catalog' | 'store_key' | 'session' | 'job_start' | 'resolve_workspace' | 'sign_in' | 'sign_out' | 'attach' | 'transcribe' | 'graph';
194
+ prompt: string;
195
+ callingModel?: string;
196
+ contextTokens?: number;
197
+ freshContext?: boolean;
198
+ tokensIn?: number;
199
+ tokensOut?: number;
200
+ /** The customer's own provider key, forwarded and never stored by us. */
201
+ providerKey?: string;
202
+ /** For store_key: which vendor the key belongs to. */
203
+ provider?: string;
204
+ /** For store_key: the key. Sent once, over TLS, and never written to disk. */
205
+ key?: string;
206
+ /** For job_start: rejoin this job rather than creating one. */
207
+ resumeJobId?: string;
208
+ /** For sign_in only. Sent once, over TLS, and written nowhere. */
209
+ password?: string;
210
+ /** For sign_in, when a second factor is required. */
211
+ challenge?: string;
212
+ code?: string;
213
+ /** For sign_in: how this machine appears where a person goes to revoke it. */
214
+ machineLabel?: string;
215
+ /** For graph: which of the map's operations this is. */
216
+ graphOp?: 'status' | 'stamps' | 'update' | 'finish' | 'ask' | 'forget' | 'policy';
217
+ /** For graph: the project on this machine the map belongs to. */
218
+ workspace?: string;
219
+ /**
220
+ * For graph: the facts read from this machine's own files, or the question
221
+ * being asked of the map.
222
+ *
223
+ * **Structure only, never source.** What travels is "a function called
224
+ * price_of exists at line 40 of this file, and these things call it". What
225
+ * that function SAYS never leaves the machine, and there is no field here
226
+ * that could carry it.
227
+ */
228
+ graphPayload?: Record<string, unknown>;
229
+ }
230
+ /**
231
+ * The same door, at an address this machine is not signed in to yet.
232
+ *
233
+ * Signing in is the one thing that cannot present a credential, because it is
234
+ * what mints one. So it needs a way to reach the door before a connection file
235
+ * exists. Everything else about the call is identical, which is the point: one
236
+ * door, and the credential is a header on it rather than a different address.
237
+ */
238
+ export declare function askAt(url: string, input: Ask, tenant?: string): Promise<GatewayReply>;
239
+ export declare function ask(input: Ask): Promise<GatewayReply>;
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.askAt = askAt;
4
+ exports.ask = ask;
5
+ /**
6
+ * The one door, from this side of it.
7
+ *
8
+ * ── Why this file replaced four others ────────────────────────────────────
9
+ *
10
+ * This tool used to decide for itself. It carried the whole decision, the
11
+ * retry behavior of every model, the cost of a wrong answer and the price
12
+ * list, and it worked out the winner on the customer's own laptop.
13
+ *
14
+ * That is our method, and a package on a public registry is readable by anyone
15
+ * who installs it, forever. So none of it lives here any more. The tool sends
16
+ * what it is about to do and receives a decision. Everything that makes the
17
+ * decision stays on our servers.
18
+ *
19
+ * The rule, from the owner: anything a client installs is thin; anything
20
+ * model-related runs on our server. What is left here is the network call and
21
+ * the shape of the reply, which is not a secret and cannot be one.
22
+ */
23
+ const connection_1 = require("./connection");
24
+ /**
25
+ * A decision that decides nothing, for when the door cannot be reached.
26
+ *
27
+ * Fail open, rule 10. The tool must never cost somebody their work because our
28
+ * server is slow or down: no model means the caller runs what it was going to
29
+ * run, and the reason is recorded rather than hidden.
30
+ */
31
+ function openFailure(intent, callingModel, why) {
32
+ return {
33
+ // The intent the caller ASKED for, echoed back. Returning 'decide' for a
34
+ // failed catalog call made a caller switching on the intent read the
35
+ // placeholder as a real answer to a different question.
36
+ intent,
37
+ client: 'cli',
38
+ decision: {
39
+ model: null,
40
+ would_have_used: callingModel || '',
41
+ tier: 'standard',
42
+ signals: [],
43
+ confident: false,
44
+ rung: null,
45
+ rationale: why,
46
+ },
47
+ answer: null,
48
+ receipt: null,
49
+ ran_on: null,
50
+ cost_usd: null,
51
+ would_have_cost_usd: null,
52
+ saved_usd: null,
53
+ estimated: false,
54
+ details_url: null,
55
+ providers: [],
56
+ stored: false,
57
+ signed_out: false,
58
+ message: why,
59
+ // We never heard back, so we know nothing about what this workspace serves.
60
+ // `served` stays true: the failure is `ok: false`, and conflating the two
61
+ // would tell a person their workspace is out of date when it is simply
62
+ // unreachable.
63
+ served: true,
64
+ // Nothing in this placeholder was established, because nobody answered.
65
+ complete: false,
66
+ // Nothing was granted either, and a caller that reads `granted` must not
67
+ // see the default of true on a reply that never came from anywhere.
68
+ granted: false,
69
+ intents: [],
70
+ session: null,
71
+ job: null,
72
+ // The whole reason this field exists. We never heard from the workspace,
73
+ // so every field above is this client's placeholder and not something the
74
+ // workspace said. A caller that reads an empty `providers` as "you have no
75
+ // keys" is the fault that sent somebody with four working keys into a form
76
+ // asking them to paste one.
77
+ ok: false,
78
+ };
79
+ }
80
+ /**
81
+ * The same door, at an address this machine is not signed in to yet.
82
+ *
83
+ * Signing in is the one thing that cannot present a credential, because it is
84
+ * what mints one. So it needs a way to reach the door before a connection file
85
+ * exists. Everything else about the call is identical, which is the point: one
86
+ * door, and the credential is a header on it rather than a different address.
87
+ */
88
+ async function askAt(url, input, tenant) {
89
+ return post(url, input, undefined, tenant);
90
+ }
91
+ async function ask(input) {
92
+ const conn = (0, connection_1.readConnection)();
93
+ if (!conn?.url || !conn.workspaceKey) {
94
+ return openFailure(input.intent, input.callingModel, 'this machine is not signed in');
95
+ }
96
+ return post(conn.url, input, conn.workspaceKey, conn.tenant);
97
+ }
98
+ async function post(url, input, credential, tenant) {
99
+ const conn = { url, workspaceKey: credential, tenant };
100
+ const headers = { 'content-type': 'application/json' };
101
+ // No credential on a sign in, because signing in is what mints one. Sending
102
+ // `Bearer undefined` made the door reject the request before it could reach
103
+ // the handler that does not need one.
104
+ if (conn.workspaceKey)
105
+ headers.authorization = `Bearer ${conn.workspaceKey}`;
106
+ if (conn.tenant)
107
+ headers['X-Tenant-ID'] = conn.tenant;
108
+ // Forwarded, never written to disk: the customer's money stays the
109
+ // customer's, and we hold no secret of theirs on their behalf.
110
+ if (input.providerKey)
111
+ headers['X-Anthropic-Key'] = input.providerKey;
112
+ try {
113
+ const res = await fetch(`${conn.url.replace(/\/+$/, '')}/api/v1/dev/gateway`, {
114
+ method: 'POST',
115
+ headers,
116
+ body: JSON.stringify({
117
+ intent: input.intent,
118
+ client: 'cli',
119
+ prompt: input.prompt,
120
+ calling_model: input.callingModel ?? null,
121
+ context_tokens: input.contextTokens ?? 0,
122
+ fresh_context: input.freshContext ?? true,
123
+ tokens_in: input.tokensIn ?? null,
124
+ tokens_out: input.tokensOut ?? null,
125
+ ...(input.provider ? { provider: input.provider } : {}),
126
+ ...(input.key ? { key: input.key } : {}),
127
+ ...(input.resumeJobId ? { resume_job_id: input.resumeJobId } : {}),
128
+ ...(input.password ? { password: input.password } : {}),
129
+ ...(input.challenge ? { challenge: input.challenge } : {}),
130
+ ...(input.code ? { code: input.code } : {}),
131
+ ...(input.machineLabel ? { machine_label: input.machineLabel } : {}),
132
+ ...(input.graphOp ? { graph_op: input.graphOp } : {}),
133
+ ...(input.workspace ? { workspace: input.workspace } : {}),
134
+ ...(input.graphPayload ? { graph_payload: input.graphPayload } : {}),
135
+ }),
136
+ });
137
+ if (!res.ok) {
138
+ // The workspace's OWN words, when it gave any. A bare status number told
139
+ // a person nothing: "403" was really "this product is for organization
140
+ // workspaces and yours is an individual account", which is actionable,
141
+ // and the number was not. An auth failure carries no body here, so
142
+ // nothing echoes a credential back.
143
+ let why = `the service answered ${res.status}`;
144
+ try {
145
+ const body = (await res.json());
146
+ if (body.detail?.message)
147
+ why = body.detail.message;
148
+ }
149
+ catch { /* no body, so the status stays the reason */ }
150
+ const failed = openFailure(input.intent, input.callingModel, why);
151
+ // 422 IS AN OLD WORKSPACE, NOT A BROKEN ONE.
152
+ //
153
+ // A build that predates the intent list rejects an unknown intent with a
154
+ // validation error before its handler runs. Reported as an ordinary
155
+ // failure, that stopped a coding job against a workspace whose own
156
+ // credential path was alive and would have worked. Newer builds answer
157
+ // 200 with `served: false` and never reach this line.
158
+ if (res.status === 422)
159
+ return { ...failed, served: false };
160
+ return failed;
161
+ }
162
+ // `ok` is this client's own marker and the server never sends it: it
163
+ // records that the workspace answered at all. Set here, on the one path
164
+ // where that is true, so no caller has to infer it from an empty field.
165
+ const reply = (await res.json());
166
+ return { ...reply, ok: true };
167
+ }
168
+ catch {
169
+ return openFailure(input.intent, input.callingModel, 'the service could not be reached');
170
+ }
171
+ }
@@ -0,0 +1,34 @@
1
+ /** Close it on every path, or the command holds the terminal and looks hung. */
2
+ export declare function closePrompt(): void;
3
+ /** Ask a question and wait for the answer. */
4
+ export declare function ask(prompt: string): Promise<string>;
5
+ /**
6
+ * Ask for a secret, and do not put it on the screen.
7
+ *
8
+ * ── Why the first two attempts failed, and what is different ──────────────
9
+ *
10
+ * Both earlier tries SUPPRESSED what readline wanted to print. Readline does
11
+ * not print characters one at a time; on every keystroke it redraws the whole
12
+ * line, prompt included. Swallowing that output therefore swallowed the
13
+ * question and the cursor with it, and left a person on a blank line unable to
14
+ * tell whether the program was even listening.
15
+ *
16
+ * So this does not suppress the redraw, it PERFORMS it: the prompt is written
17
+ * back out followed by one dot per character actually held in the line. Paste
18
+ * and backspace come out right for free, because the dots are counted from the
19
+ * line itself rather than tallied from keystrokes.
20
+ *
21
+ * With no terminal attached there is nothing to redraw and nothing to hide
22
+ * from, so the plain question is used. That is the piped case, where the only
23
+ * reader is a script.
24
+ */
25
+ export declare function askHidden(prompt: string): Promise<string>;
26
+ /**
27
+ * Everything on the line that is not a flag or a flag's value.
28
+ *
29
+ * A person types `comprism ask fix the failing test`, not
30
+ * `comprism ask --request "fix the failing test"`.
31
+ */
32
+ export declare function freeText(args: string[], flagsTakingValue: string[]): string;
33
+ /** The subject of a command: taken from the line, or asked for. */
34
+ export declare function subject(args: string[], prompt: string, flagsTakingValue?: string[]): Promise<string>;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.closePrompt = closePrompt;
7
+ exports.ask = ask;
8
+ exports.askHidden = askHidden;
9
+ exports.freeText = freeText;
10
+ exports.subject = subject;
11
+ /**
12
+ * Asking a person for something, in a terminal.
13
+ *
14
+ * One reader, opened on demand and closed at the end of the command. Readline
15
+ * reads ahead, so a fresh reader per question throws away whatever was already
16
+ * typed, which silently lost the second answer of two.
17
+ */
18
+ const node_readline_1 = __importDefault(require("node:readline"));
19
+ let reader = null;
20
+ function open() {
21
+ if (!reader) {
22
+ reader = node_readline_1.default.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout,
25
+ terminal: process.stdin.isTTY === true,
26
+ });
27
+ }
28
+ return reader;
29
+ }
30
+ /** Close it on every path, or the command holds the terminal and looks hung. */
31
+ function closePrompt() {
32
+ reader?.close();
33
+ reader = null;
34
+ }
35
+ /** Ask a question and wait for the answer. */
36
+ function ask(prompt) {
37
+ return new Promise((resolve) => {
38
+ open().question(prompt, (answer) => resolve(answer.trim()));
39
+ });
40
+ }
41
+ /**
42
+ * Ask for a secret, and do not put it on the screen.
43
+ *
44
+ * ── Why the first two attempts failed, and what is different ──────────────
45
+ *
46
+ * Both earlier tries SUPPRESSED what readline wanted to print. Readline does
47
+ * not print characters one at a time; on every keystroke it redraws the whole
48
+ * line, prompt included. Swallowing that output therefore swallowed the
49
+ * question and the cursor with it, and left a person on a blank line unable to
50
+ * tell whether the program was even listening.
51
+ *
52
+ * So this does not suppress the redraw, it PERFORMS it: the prompt is written
53
+ * back out followed by one dot per character actually held in the line. Paste
54
+ * and backspace come out right for free, because the dots are counted from the
55
+ * line itself rather than tallied from keystrokes.
56
+ *
57
+ * With no terminal attached there is nothing to redraw and nothing to hide
58
+ * from, so the plain question is used. That is the piped case, where the only
59
+ * reader is a script.
60
+ */
61
+ function askHidden(prompt) {
62
+ const reader = open();
63
+ if (process.stdin.isTTY !== true)
64
+ return ask(prompt);
65
+ const inner = reader;
66
+ const asItWas = inner._writeToOutput.bind(reader);
67
+ inner._writeToOutput = (chunk) => {
68
+ // A newline is the answer being accepted, and it still has to happen.
69
+ if (chunk.includes("\n") || chunk.includes("\r")) {
70
+ asItWas(chunk);
71
+ return;
72
+ }
73
+ process.stdout.write(`\r${prompt}${"\u2022".repeat(inner.line.length)}`);
74
+ };
75
+ return new Promise((resolve) => {
76
+ reader.question(prompt, (answer) => {
77
+ // Put readline back the way it was, whatever happened, or every later
78
+ // question on this reader would come out as dots.
79
+ inner._writeToOutput = asItWas;
80
+ process.stdout.write("\n");
81
+ resolve(answer.trim());
82
+ });
83
+ });
84
+ }
85
+ /**
86
+ * Everything on the line that is not a flag or a flag's value.
87
+ *
88
+ * A person types `comprism ask fix the failing test`, not
89
+ * `comprism ask --request "fix the failing test"`.
90
+ */
91
+ function freeText(args, flagsTakingValue) {
92
+ return args
93
+ .filter((a, i) => {
94
+ if (a.startsWith('--'))
95
+ return false;
96
+ const prev = args[i - 1];
97
+ return !(prev && flagsTakingValue.includes(prev));
98
+ })
99
+ .join(' ')
100
+ .trim();
101
+ }
102
+ /** The subject of a command: taken from the line, or asked for. */
103
+ async function subject(args, prompt, flagsTakingValue = []) {
104
+ const onTheLine = freeText(args, flagsTakingValue);
105
+ if (onTheLine)
106
+ return onTheLine;
107
+ return ask(prompt);
108
+ }