agentreceipt-voice 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -20
- package/bin/cli.js +3 -1
- package/package.json +35 -9
- package/src/cli.js +162 -5
package/README.md
CHANGED
|
@@ -1,39 +1,172 @@
|
|
|
1
1
|
# agentreceipt-voice
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**One package for people who dictate.** It sets up your voice-dictation app with ready-made shortcuts, checks your import files before the app silently skips rows, and builds a custom dictionary from your own code.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx agentreceipt-voice
|
|
7
|
-
npx agentreceipt-voice check my-dictionary.csv # see what the app would skip, before you import
|
|
8
|
-
npx agentreceipt-voice vocab ./my-repo # build a dictionary from the names in your code
|
|
9
|
-
npx agentreceipt-voice snippets init # keep snippets as files in git
|
|
6
|
+
npx agentreceipt-voice
|
|
10
7
|
```
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
That is the whole install. It opens a short menu, asks what you want, and does it for you.
|
|
10
|
+
|
|
11
|
+
> **Unofficial.** This is not made by, or affiliated with, any dictation app. Its files follow the bulk-import format that Wispr Flow's help centre describes (a dictionary CSV and a snippets JSON). It has not been tested inside the app itself, so look at the app's own import preview before you confirm.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Which package do I install?
|
|
16
|
+
|
|
17
|
+
You will see five packages on npm with similar names. **You only need this one.**
|
|
18
|
+
|
|
19
|
+
| Package | What it is | Do I install it? |
|
|
13
20
|
|---|---|---|
|
|
14
|
-
|
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
21
|
+
| **`agentreceipt-voice`** (this one) | Everything, in one place, with a guided menu | **Yes. This is the only one.** |
|
|
22
|
+
| `agentreceipt-voice-check` | Just the file checker | No. Already included here. |
|
|
23
|
+
| `agentreceipt-voice-packs` | Just the ready-made packs | No. Already included here. |
|
|
24
|
+
| `agentreceipt-voice-snippets` | Just "snippets as code" | No. Already included here. |
|
|
25
|
+
| `agentreceipt-voice-vocab` | Just the dictionary builder | No. Already included here. |
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
The four smaller packages exist for developers who want to use one tool inside their own code without the rest. When you install `agentreceipt-voice`, npm brings the other four with it automatically. You never type their names.
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Start here (about a minute)
|
|
32
|
+
|
|
33
|
+
Open a terminal (needs [Node.js](https://nodejs.org) 18 or newer) and run:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx agentreceipt-voice
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You will see:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
What do you want to do?
|
|
43
|
+
|
|
44
|
+
1 Get ready-made voice shortcuts (packs)
|
|
45
|
+
2 Check a file before I import it
|
|
46
|
+
3 Build a dictionary from my code
|
|
47
|
+
4 Keep my shortcuts as files in git
|
|
48
|
+
q Quit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Type a number and press Enter. It asks one or two simple questions (press Enter to accept the suggested answer), does the job, checks the result, and tells you the next click to make in your dictation app.
|
|
52
|
+
|
|
53
|
+
**Install it for good (optional).** If you use it often:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm install -g agentreceipt-voice
|
|
57
|
+
voice-kit
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`voice-kit` is a short name for the same command. (With `npx` you type the full `agentreceipt-voice`; the short name works once it is installed.)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## What each option does, step by step
|
|
65
|
+
|
|
66
|
+
### 1. Ready-made voice shortcuts
|
|
67
|
+
|
|
68
|
+
Shortcuts let you say a short phrase and get a whole block of text. Four packs are included:
|
|
69
|
+
|
|
70
|
+
| Pack | Shortcuts | Examples |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `developer` | 8, plus 43 dictionary words | `insert pr checklist`, `insert bug report`, `insert daily standup`, `insert ai change review` |
|
|
73
|
+
| `founder` | 8 | `insert investor update`, `insert warm intro`, `insert polite decline` |
|
|
74
|
+
| `support` | 7 | `insert need more info`, `insert refund processed`, `insert apology` |
|
|
75
|
+
| `sales` | 8 | `insert discovery questions`, `insert demo follow up`, `insert pricing reply` |
|
|
22
76
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
77
|
+
1. Run `npx agentreceipt-voice` and choose **1**.
|
|
78
|
+
2. Pick a pack (a number, a name, or `a` for all of them) and a folder to write the files in.
|
|
79
|
+
3. It writes the files, checks them, and prints what to do next.
|
|
80
|
+
4. In your dictation app on a **Mac or Windows** computer:
|
|
81
|
+
- open **Snippets**, click **Import** (top right, next to *Add new*), choose the `.json` file, and confirm;
|
|
82
|
+
- open **Dictionary**, click **Import**, and choose each `.csv` file (the developer pack has two).
|
|
83
|
+
5. Click into any text box and say, for example, "insert pr checklist".
|
|
84
|
+
|
|
85
|
+
Every shortcut starts with the word **insert**. Dictation apps match shortcuts as whole words, so a plain phrase like "bug report" would expand in the middle of ordinary speech. "insert bug report" will not. The text has `[brackets]` where you fill in details after it appears.
|
|
86
|
+
|
|
87
|
+
### 2. Check a file before you import it
|
|
88
|
+
|
|
89
|
+
Dictation apps skip bad rows without telling you. This tells you first.
|
|
90
|
+
|
|
91
|
+
1. Choose **2** and give it the path to your file (a `.csv` dictionary or a `.json` snippets file; you can drag the file into the terminal).
|
|
92
|
+
2. It lists every problem, for example:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
my-words.csv: 41 dictionary entries will import
|
|
96
|
+
SKIPPED line 7: more than two non-empty columns (quote fields that contain commas)
|
|
97
|
+
REPEAT line 19: "Vercel" already appears on line 3 (only the first is kept)
|
|
98
|
+
NOTE line 1 looks like a header row. The app imports it as an entry, so remove it.
|
|
99
|
+
1 skipped. Fix these before importing.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
3. Fix the file and run it again until it says **OK: nothing will be skipped**.
|
|
103
|
+
|
|
104
|
+
What it checks: no header row, one or two columns, quoting, 60 characters for a word or trigger, 4,000 for the text, repeats ignoring capitals, at most 1,000 entries and 3 MB per file.
|
|
105
|
+
|
|
106
|
+
### 3. Build a dictionary from your code
|
|
107
|
+
|
|
108
|
+
If your app keeps mishearing names such as `useAuthSession`, `drizzle-orm` or `make_receipt_id`, this teaches it.
|
|
109
|
+
|
|
110
|
+
1. Choose **3**, then pick the code folder to scan and where to save the list (default `words.csv`).
|
|
111
|
+
2. It reads your code **on your computer only. Nothing is uploaded.** It collects package names and identifiers written in camelCase, PascalCase or snake_case, most used first, and skips `node_modules`, build folders and hidden folders.
|
|
112
|
+
3. Open `words.csv` and delete any names you do not want, then in your app go to **Dictionary → Import** and choose it.
|
|
113
|
+
|
|
114
|
+
### 4. Keep your shortcuts as files in git (for teams)
|
|
115
|
+
|
|
116
|
+
Good when a team shares shortcuts and wants changes reviewed like code.
|
|
117
|
+
|
|
118
|
+
1. Choose **4**. The first time, it creates a starter folder (`voice-snippets`) with an example shortcut and a `dictionary.txt`.
|
|
119
|
+
2. Edit it: one `.md` file per shortcut (the file name is the trigger, or write `trigger: insert my thing` at the top), and one word per line in `dictionary.txt`, or `what the app wrote => what you meant`.
|
|
120
|
+
3. Run the menu again and choose **4** to build the import files. It **refuses to write anything the app would partly skip**, and tells you why.
|
|
121
|
+
4. Import `snippets.json` and the `.csv` files as above.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Prefer typing commands?
|
|
126
|
+
|
|
127
|
+
Everything in the menu is also a command, which is useful in scripts:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
npx agentreceipt-voice packs list
|
|
131
|
+
npx agentreceipt-voice packs export developer -o voice-packs
|
|
132
|
+
npx agentreceipt-voice check my-words.csv voice-packs/developer.snippets.json
|
|
133
|
+
npx agentreceipt-voice vocab ./my-repo -o words.csv --max 300
|
|
134
|
+
npx agentreceipt-voice snippets init voice-snippets
|
|
135
|
+
npx agentreceipt-voice snippets build voice-snippets -o voice-out
|
|
136
|
+
npx agentreceipt-voice --help
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`check` exits with `0` when everything is fine, `1` when something would be skipped, and `2` when it could not read a file, so it can fail a build.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## If something goes wrong
|
|
144
|
+
|
|
145
|
+
| What you see | What to do |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `command not found: voice-kit` | Use `npx agentreceipt-voice` instead, or install it first with `npm install -g agentreceipt-voice`. |
|
|
148
|
+
| `npx` says the package is not found | Check your internet connection, and that Node.js is version 18 or newer (`node --version`). |
|
|
149
|
+
| No **Import** button in your dictation app | Import is on the Mac and Windows apps only, not on phones. Update the app, then look again. |
|
|
150
|
+
| The app imported fewer entries than the checker said | Entries that already exist in your app are skipped as duplicates. Look at the app's import preview. |
|
|
151
|
+
| Triggers expand while you talk normally | Use the `insert ...` triggers from the packs, or add `insert` before your own triggers. |
|
|
152
|
+
| The menu does not open when you run it from a script | It only opens for a person at a keyboard. In scripts, give a command (`check`, `packs`, `snippets`, `vocab`). |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Good to know
|
|
26
157
|
|
|
27
|
-
|
|
158
|
+
- **Local only.** Nothing you scan, generate or check is sent anywhere.
|
|
159
|
+
- **If your app changes its import rules,** the checker needs updating too.
|
|
160
|
+
- **Use it from code:**
|
|
28
161
|
|
|
29
162
|
```js
|
|
30
163
|
import { check, packs } from 'agentreceipt-voice';
|
|
31
164
|
|
|
32
|
-
const
|
|
33
|
-
console.log(
|
|
165
|
+
const r = check.checkDictionary('Supabase\nsuper base,Supabase\n');
|
|
166
|
+
console.log(r.ok, r.valid.length);
|
|
34
167
|
console.log(packs.listPacks());
|
|
35
168
|
```
|
|
36
169
|
|
|
37
|
-
## Licence
|
|
170
|
+
## Licence and feedback
|
|
38
171
|
|
|
39
|
-
MIT. Made by the team behind [AgentReceipt](https://www.agentreceipt.online), which records what AI coding agents change in your code.
|
|
172
|
+
MIT. Made by the team behind [AgentReceipt](https://www.agentreceipt.online), which records what AI coding agents change in your code. Questions or bugs: founders@agentreceipt.online.
|
package/bin/cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from '../src/cli.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// With no command, a person at a keyboard gets the guided menu; a script gets the help text.
|
|
5
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
6
|
+
process.exitCode = await run(process.argv.slice(2), undefined, { interactive });
|
package/package.json
CHANGED
|
@@ -1,27 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentreceipt-voice",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "One package for voice dictation power users: a guided setup, ready-made snippet packs, an import-file checker, a dictionary builder for your code, and snippets as code. Includes everything; nothing else to install. Unofficial; works with the Wispr Flow import format.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dictation",
|
|
7
|
+
"voice",
|
|
8
|
+
"snippets",
|
|
9
|
+
"dictionary",
|
|
10
|
+
"wispr-flow",
|
|
11
|
+
"speech-to-text",
|
|
12
|
+
"voice-typing",
|
|
13
|
+
"developer-tools",
|
|
14
|
+
"toolkit"
|
|
15
|
+
],
|
|
6
16
|
"license": "MIT",
|
|
7
17
|
"author": "Ayush Pund <founders@agentreceipt.online> (https://www.agentreceipt.online)",
|
|
8
18
|
"homepage": "https://www.agentreceipt.online",
|
|
9
|
-
"bugs": {
|
|
19
|
+
"bugs": {
|
|
20
|
+
"email": "founders@agentreceipt.online"
|
|
21
|
+
},
|
|
10
22
|
"type": "module",
|
|
11
23
|
"main": "src/index.js",
|
|
12
24
|
"exports": {
|
|
13
25
|
".": "./src/index.js",
|
|
14
26
|
"./package.json": "./package.json"
|
|
15
27
|
},
|
|
16
|
-
"bin": {
|
|
17
|
-
|
|
28
|
+
"bin": {
|
|
29
|
+
"agentreceipt-voice": "bin/cli.js",
|
|
30
|
+
"voice-kit": "bin/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
18
38
|
"dependencies": {
|
|
19
39
|
"agentreceipt-voice-check": "^0.1.0",
|
|
20
40
|
"agentreceipt-voice-packs": "^0.1.0",
|
|
21
41
|
"agentreceipt-voice-snippets": "^0.1.0",
|
|
22
42
|
"agentreceipt-voice-vocab": "^0.1.0"
|
|
23
43
|
},
|
|
24
|
-
"engines": {
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"test": "node --test"
|
|
52
|
+
}
|
|
27
53
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,29 +1,186 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
4
|
import { run as check } from 'agentreceipt-voice-check/cli';
|
|
5
|
+
import { getPack, listPacks } from 'agentreceipt-voice-packs';
|
|
3
6
|
import { run as packs } from 'agentreceipt-voice-packs/cli';
|
|
4
7
|
import { run as snippets } from 'agentreceipt-voice-snippets/cli';
|
|
5
8
|
import { run as vocab } from 'agentreceipt-voice-vocab/cli';
|
|
6
9
|
|
|
7
|
-
const USAGE = `agentreceipt-voice
|
|
10
|
+
const USAGE = `agentreceipt-voice [command] [options]
|
|
11
|
+
|
|
12
|
+
(no command) a guided menu that asks what you want and does it for you
|
|
13
|
+
setup the same guided menu, started on purpose
|
|
8
14
|
|
|
9
15
|
check <file>... check a dictionary CSV or snippets JSON before you import it
|
|
10
16
|
packs list | export ready-made snippet and dictionary packs (developer, founder, support, sales)
|
|
11
17
|
snippets init | build keep snippets as files in git and build the import files
|
|
12
18
|
vocab [folder] build a dictionary CSV from the names in a code repository
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
Short name: voice-kit does the same as agentreceipt-voice.
|
|
21
|
+
Each command is also its own package (agentreceipt-voice-check, -packs, -snippets, -vocab), but you do
|
|
22
|
+
not need them: this one package already includes all four.
|
|
15
23
|
Run a command with --help for its options. Unofficial: not made by or affiliated with any dictation app.`;
|
|
16
24
|
|
|
17
25
|
const COMMANDS = { check, packs, snippets, vocab };
|
|
26
|
+
const defaultIo = { out: (s) => process.stdout.write(s), err: (s) => process.stderr.write(s) };
|
|
27
|
+
|
|
28
|
+
const answer = async (ask, question, fallback) => {
|
|
29
|
+
const a = (await ask(question)).trim();
|
|
30
|
+
return a === '' ? fallback : a;
|
|
31
|
+
};
|
|
32
|
+
const unquote = (s) => s.trim().replace(/^["']|["']$/g, '');
|
|
33
|
+
|
|
34
|
+
/** Files in a folder that belong to the given pack (or all packs). */
|
|
35
|
+
function packFilesIn(dir, chosen) {
|
|
36
|
+
return readdirSync(dir)
|
|
37
|
+
.filter((f) => /\.(json|csv)$/.test(f) && (chosen === 'all' || f.startsWith(`${chosen}.`)))
|
|
38
|
+
.sort()
|
|
39
|
+
.map((f) => join(dir, f));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The guided menu. `ask` takes a question and gives back what the person typed. */
|
|
43
|
+
export async function guided(io, ask) {
|
|
44
|
+
io.out(`\nWhat do you want to do?
|
|
45
|
+
|
|
46
|
+
1 Get ready-made voice shortcuts (packs)
|
|
47
|
+
2 Check a file before I import it
|
|
48
|
+
3 Build a dictionary from my code
|
|
49
|
+
4 Keep my shortcuts as files in git
|
|
50
|
+
q Quit
|
|
51
|
+
|
|
52
|
+
`);
|
|
53
|
+
const pick = (await ask('Type 1, 2, 3, 4 or q, then press Enter: ')).trim().toLowerCase();
|
|
54
|
+
if (pick === 'q' || pick === 'quit') return 0;
|
|
55
|
+
|
|
56
|
+
if (pick === '1') {
|
|
57
|
+
const list = listPacks();
|
|
58
|
+
io.out(`\nWhich pack?\n\n${list.map((p, i) => ` ${i + 1} ${p.id.padEnd(10)} ${p.description}`).join('\n')}\n a all of them\n\n`);
|
|
59
|
+
const raw = (await answer(ask, 'Type a number, a name, or a (default: 1): ', '1')).toLowerCase();
|
|
60
|
+
const chosen = raw === 'a' || raw === 'all' ? 'all' : (list[Number(raw) - 1]?.id ?? raw);
|
|
61
|
+
const out = unquote(await answer(ask, 'Folder to write the files in (default: voice-packs): ', 'voice-packs'));
|
|
62
|
+
const code = await packs(['export', chosen, '-o', out], io);
|
|
63
|
+
if (code !== 0) return code;
|
|
64
|
+
io.out('\nChecking the files the way your app would...\n');
|
|
65
|
+
const result = await check(packFilesIn(out, chosen), io);
|
|
66
|
+
let example = 'insert pr checklist';
|
|
67
|
+
try {
|
|
68
|
+
example = getPack(chosen === 'all' ? 'developer' : chosen).snippets[0].name;
|
|
69
|
+
} catch {
|
|
70
|
+
/* keep the default */
|
|
71
|
+
}
|
|
72
|
+
io.out(`
|
|
73
|
+
Next, open your dictation app on a Mac or Windows computer:
|
|
74
|
+
Snippets -> Import -> choose the .json file(s) in ${out}
|
|
75
|
+
Dictionary -> Import -> choose the .csv file(s) in ${out} (the developer pack has two)
|
|
76
|
+
Then say "${example}" in any text box to try it.
|
|
77
|
+
`);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (pick === '2') {
|
|
82
|
+
const file = unquote(await ask('Which file? (a .csv dictionary or a .json snippets file): '));
|
|
83
|
+
if (!file) {
|
|
84
|
+
io.err('No file given.\n');
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
return check([file], io);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (pick === '3') {
|
|
91
|
+
const dir = unquote(await answer(ask, 'Which code folder should I scan? (default: this folder): ', '.'));
|
|
92
|
+
const out = unquote(await answer(ask, 'Where should the dictionary file go? (default: words.csv): ', 'words.csv'));
|
|
93
|
+
const code = await vocab([dir, '-o', out], io);
|
|
94
|
+
if (code !== 0) return code;
|
|
95
|
+
io.out('\nChecking it the way your app would...\n');
|
|
96
|
+
const result = await check([out], io);
|
|
97
|
+
io.out(`\nNext: open your dictation app (Mac or Windows) -> Dictionary -> Import -> choose ${out}.\nOpen the file first and delete any names you do not want.\n`);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (pick === '4') {
|
|
102
|
+
const dir = unquote(await answer(ask, 'Folder for your snippets (default: voice-snippets): ', 'voice-snippets'));
|
|
103
|
+
if (!existsSync(dir)) {
|
|
104
|
+
io.out(`\nThat folder does not exist yet, so I will create a starter one.\n`);
|
|
105
|
+
const code = await snippets(['init', dir], io);
|
|
106
|
+
io.out(`\nEdit the files in ${dir} (one .md file per shortcut), then run this again and choose 4 to build the import files.\n`);
|
|
107
|
+
return code;
|
|
108
|
+
}
|
|
109
|
+
const out = unquote(await answer(ask, 'Folder for the import files (default: voice-out): ', 'voice-out'));
|
|
110
|
+
return snippets(['build', dir, '-o', out], io);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
io.err('Please type 1, 2, 3, 4 or q.\n');
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class EndOfInput extends Error {}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reads typed answers one line at a time. Lines that arrive before a question is asked (pasted or piped
|
|
121
|
+
* input) are kept, not lost, and when input ends (Ctrl+D, or a script with too few answers) the pending
|
|
122
|
+
* question gets `null` instead of waiting forever.
|
|
123
|
+
*/
|
|
124
|
+
function lineReader() {
|
|
125
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
const queue = [];
|
|
127
|
+
const waiters = [];
|
|
128
|
+
let closed = false;
|
|
129
|
+
rl.on('line', (line) => {
|
|
130
|
+
const w = waiters.shift();
|
|
131
|
+
if (w) w(line);
|
|
132
|
+
else queue.push(line);
|
|
133
|
+
});
|
|
134
|
+
rl.on('close', () => {
|
|
135
|
+
closed = true;
|
|
136
|
+
while (waiters.length) waiters.shift()(null);
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
ask(question) {
|
|
140
|
+
process.stdout.write(question);
|
|
141
|
+
if (queue.length) return Promise.resolve(queue.shift());
|
|
142
|
+
if (closed) return Promise.resolve(null);
|
|
143
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
144
|
+
},
|
|
145
|
+
close: () => rl.close(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function withAsk(env, io, fn) {
|
|
150
|
+
if (env.ask) return fn(env.ask);
|
|
151
|
+
const reader = lineReader();
|
|
152
|
+
const ask = async (question) => {
|
|
153
|
+
const a = await reader.ask(question);
|
|
154
|
+
if (a === null) throw new EndOfInput();
|
|
155
|
+
return a;
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
return await fn(ask);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (e instanceof EndOfInput) {
|
|
161
|
+
io.err('\nNo answer was received, so I stopped. Run it again in a normal terminal window.\n');
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
throw e;
|
|
165
|
+
} finally {
|
|
166
|
+
reader.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
18
169
|
|
|
19
|
-
|
|
170
|
+
/**
|
|
171
|
+
* `env.interactive`: the command was started by a person at a keyboard, so with no command the guided menu opens.
|
|
172
|
+
* `env.ask`: supplies the answers instead of the keyboard (used by the tests).
|
|
173
|
+
*/
|
|
174
|
+
export async function run(argv, io = defaultIo, env = {}) {
|
|
20
175
|
const [cmd, ...rest] = argv;
|
|
21
176
|
if (cmd === '--version' || cmd === '-v') {
|
|
22
177
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
23
178
|
io.out(`${pkg.version}\n`);
|
|
24
179
|
return 0;
|
|
25
180
|
}
|
|
26
|
-
if (
|
|
181
|
+
if (cmd === '--help' || cmd === '-h') return io.out(`${USAGE}\n`), 0;
|
|
182
|
+
if (!cmd && !env.interactive) return io.err(`${USAGE}\n`), 2;
|
|
183
|
+
if (!cmd || cmd === 'setup' || cmd === 'start' || cmd === 'guide') return withAsk(env, io, (ask) => guided(io, ask));
|
|
27
184
|
const fn = COMMANDS[cmd];
|
|
28
185
|
if (!fn) return io.err(`Unknown command "${cmd}".\n${USAGE}\n`), 2;
|
|
29
186
|
return fn(rest, io);
|