wheems 0.0.2 → 0.0.3
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/CHANGELOG.md +22 -0
- package/README.md +18 -0
- package/dist/cli/main.js +1 -0
- package/dist/cli/program.d.ts +2 -0
- package/dist/cli/program.js +35 -4
- package/dist/cli/welcome.d.ts +48 -5
- package/dist/cli/welcome.js +83 -25
- package/dist/drivers/channel/slack/connect.d.ts +5 -0
- package/dist/drivers/channel/slack/connect.js +53 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
Wheems follows [semantic versioning](https://semver.org). While it's 0.0.x, any release may
|
|
4
4
|
change anything, including commands and config formats, without a migration path.
|
|
5
5
|
|
|
6
|
+
## 0.0.3
|
|
7
|
+
|
|
8
|
+
A new look for the cloud in the terminal, and clearer help when a Slack token is wrong. Still an
|
|
9
|
+
early preview: not ready for real use. The first release published from GitHub.
|
|
10
|
+
|
|
11
|
+
**Changed**
|
|
12
|
+
|
|
13
|
+
- The cloud on the welcome screen and in `wheems setup` is now the mascot's outline, with its eye
|
|
14
|
+
and smile, instead of a solid shape whose face got lost. It's drawn in the terminal's text colour
|
|
15
|
+
(white on a dark terminal, near-black on a light one) rather than blue. It's left out when the
|
|
16
|
+
output isn't a terminal (piped or saved to a file) and when the terminal is narrower than 53
|
|
17
|
+
columns, so only the name, tagline and status are shown.
|
|
18
|
+
- The cloud winks once when it appears, in an interactive terminal. Set `WHEEMS_NO_ANIMATION` to
|
|
19
|
+
stop it; it never winks in CI, in a dumb terminal, or when the output isn't a terminal.
|
|
20
|
+
|
|
21
|
+
**Fixed**
|
|
22
|
+
|
|
23
|
+
- When a pasted Slack token isn't right, setup says why instead of only "The bot token starts with
|
|
24
|
+
xoxb-": the app-level token pasted for the bot token (or the other way round), a user or refresh
|
|
25
|
+
token, two pastes run together, something in front of the token, or a space in it, each with
|
|
26
|
+
where in Slack to find the right one. It names tokens only by their standard prefix.
|
|
27
|
+
|
|
6
28
|
## 0.0.2
|
|
7
29
|
|
|
8
30
|
Fixes from the first preview, a welcome screen, and checks that catch common set-up problems.
|
package/README.md
CHANGED
|
@@ -665,6 +665,7 @@ skills:
|
|
|
665
665
|
| `WHEEMS_SECRETS` | The secrets file (default `~/.config/wheems/secrets.json`, or under `$XDG_CONFIG_HOME`) |
|
|
666
666
|
| `WHEEMS_WORKSPACE` | The workspace `wheems kb search` searches. Wheems sets it in every agent session |
|
|
667
667
|
| `WHEEMS_CONVERSATION` | The conversation an agent's commands belong to. Wheems sets it in every agent session |
|
|
668
|
+
| `WHEEMS_NO_ANIMATION` | Set to anything to stop the cloud winking on the welcome screen and in `wheems setup` |
|
|
668
669
|
|
|
669
670
|
### Config the schemas accept but nothing uses yet
|
|
670
671
|
|
|
@@ -798,6 +799,23 @@ also run it against a fake `hc`, which is what CI uses. Set `WHEEMS_TEST_OLLAMA_
|
|
|
798
799
|
| `examples/install/` | A complete example install |
|
|
799
800
|
| `docs/` | Design documents |
|
|
800
801
|
|
|
802
|
+
### Releasing
|
|
803
|
+
|
|
804
|
+
GitHub publishes each release. To release, merge a pull request to `main` that:
|
|
805
|
+
|
|
806
|
+
1. changes `version` in `package.json` (and `package-lock.json`);
|
|
807
|
+
2. adds a `## <version>` section to `CHANGELOG.md`.
|
|
808
|
+
|
|
809
|
+
The [release workflow](.github/workflows/release.yml) then runs the full check on Linux and
|
|
810
|
+
macOS and publishes to npm. It then tags the commit `v<version>` and creates a GitHub release
|
|
811
|
+
with that changelog section as its notes. A push to `main` whose version is already published and
|
|
812
|
+
tagged does nothing.
|
|
813
|
+
|
|
814
|
+
npm trusts the workflow through
|
|
815
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm token is stored
|
|
816
|
+
anywhere. The package's settings on npmjs.com name this repository and `release.yml` as its
|
|
817
|
+
trusted publisher.
|
|
818
|
+
|
|
801
819
|
## More documentation
|
|
802
820
|
|
|
803
821
|
The documents in [`docs/`](docs/) go deeper, and some describe the design of milestones not built
|
package/dist/cli/main.js
CHANGED
|
@@ -14,6 +14,7 @@ process.exitCode = await run(process.argv.slice(2), {
|
|
|
14
14
|
err: (text) => process.stderr.write(text),
|
|
15
15
|
env: process.env,
|
|
16
16
|
color,
|
|
17
|
+
columns: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
17
18
|
...defaultIo(),
|
|
18
19
|
});
|
|
19
20
|
//# sourceMappingURL=main.js.map
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface Io {
|
|
|
8
8
|
err: (text: string) => void;
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
color: boolean;
|
|
11
|
+
/** The terminal's width, or undefined when the output isn't a terminal. */
|
|
12
|
+
columns?: number | undefined;
|
|
11
13
|
registry: () => DriverRegistry;
|
|
12
14
|
stdin: () => Promise<string>;
|
|
13
15
|
}
|
package/dist/cli/program.js
CHANGED
|
@@ -17,7 +17,7 @@ import { registerReply } from './commands/reply.js';
|
|
|
17
17
|
import { registerService } from './commands/service.js';
|
|
18
18
|
import { ensureInstall, setup } from './commands/setup.js';
|
|
19
19
|
import { clackUi } from './clack-ui.js';
|
|
20
|
-
import {
|
|
20
|
+
import { canWink, renderWelcomeHead, renderWelcomeTail, SHOW_CURSOR, showCloud, welcomeStatus, wink, } from './welcome.js';
|
|
21
21
|
import { installAndWait, serviceAnswers } from './commands/service.js';
|
|
22
22
|
import { runCommand, runInteractive } from '../util/process.js';
|
|
23
23
|
import { SetupCancelled } from '../contracts/setup.js';
|
|
@@ -99,7 +99,13 @@ export function createProgram(io) {
|
|
|
99
99
|
const where = paths(home, io.env);
|
|
100
100
|
const registry = io.registry();
|
|
101
101
|
const ui = clackUi();
|
|
102
|
-
io.out(
|
|
102
|
+
io.out(renderWelcomeHead({
|
|
103
|
+
version: packageVersion(),
|
|
104
|
+
color: io.color,
|
|
105
|
+
cloud: showCloud(io.columns),
|
|
106
|
+
}));
|
|
107
|
+
if (canWink(io.columns, io.env))
|
|
108
|
+
await winkOnce(io);
|
|
103
109
|
ui.intro(' Wheems setup ');
|
|
104
110
|
try {
|
|
105
111
|
await setup({
|
|
@@ -213,18 +219,43 @@ export function createProgram(io) {
|
|
|
213
219
|
function shellPath(path) {
|
|
214
220
|
return /^[\w./-]+$/.test(path) ? path : `'${path.replaceAll("'", `'\\''`)}'`;
|
|
215
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The cloud winks once. Ctrl+C meanwhile still gives the terminal its cursor back before
|
|
224
|
+
* stopping, as it would have done without the wink.
|
|
225
|
+
*/
|
|
226
|
+
async function winkOnce(io) {
|
|
227
|
+
const interrupted = () => {
|
|
228
|
+
io.out(SHOW_CURSOR);
|
|
229
|
+
process.exit(130);
|
|
230
|
+
};
|
|
231
|
+
process.once('SIGINT', interrupted);
|
|
232
|
+
try {
|
|
233
|
+
await wink({ version: packageVersion(), color: io.color, write: io.out });
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
process.off('SIGINT', interrupted);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
216
239
|
/** Runs the CLI and returns the exit code. With no command, it shows the welcome screen. */
|
|
217
240
|
export async function run(argv, io) {
|
|
218
241
|
if (argv.length === 0) {
|
|
219
242
|
const home = resolve(defaultHome(io.env));
|
|
220
243
|
// The welcome never fails: a problem reading the install becomes one of its lines.
|
|
221
|
-
|
|
244
|
+
// The status is read while the cloud is drawn and winks.
|
|
245
|
+
const reading = welcomeStatus(home, io.env).catch((error) => [
|
|
222
246
|
{
|
|
223
247
|
mark: '!',
|
|
224
248
|
text: `Couldn't read the install at ${home}: ${error instanceof Error ? error.message : String(error)}. Run wheems doctor.`,
|
|
225
249
|
},
|
|
226
250
|
]);
|
|
227
|
-
io.out(
|
|
251
|
+
io.out(renderWelcomeHead({
|
|
252
|
+
version: packageVersion(),
|
|
253
|
+
color: io.color,
|
|
254
|
+
cloud: showCloud(io.columns),
|
|
255
|
+
}));
|
|
256
|
+
if (canWink(io.columns, io.env))
|
|
257
|
+
await winkOnce(io);
|
|
258
|
+
io.out(renderWelcomeTail({ status: await reading, color: io.color }));
|
|
228
259
|
return 0;
|
|
229
260
|
}
|
|
230
261
|
const program = createProgram(io);
|
package/dist/cli/welcome.d.ts
CHANGED
|
@@ -1,25 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The cloud
|
|
3
|
-
*
|
|
2
|
+
* The cloud: the mascot's outline (the brand kit's mascot.svg), traced in Unicode half blocks,
|
|
3
|
+
* with its eye and smile placed by hand so they read at this size. Half blocks are in every
|
|
4
|
+
* monospace font and most terminals draw them as exact rectangles, so it looks the same
|
|
5
|
+
* everywhere; an outline also reads on light and dark backgrounds alike. It has no colour of
|
|
6
|
+
* its own: it's drawn in the terminal's text colour, like the brand's ink app icon, so it's white
|
|
7
|
+
* on a dark terminal and near-black on a light one.
|
|
4
8
|
*/
|
|
5
9
|
export declare const CLOUD: string[];
|
|
6
10
|
/** The brand line under the name. */
|
|
7
11
|
export declare const TAGLINE = "Your crew, ready for work.";
|
|
12
|
+
/** The columns the cloud and the words beside it need. */
|
|
13
|
+
export declare const WELCOME_WIDTH: number;
|
|
14
|
+
/**
|
|
15
|
+
* Whether to draw the cloud: only in a terminal wide enough for it and the words beside it.
|
|
16
|
+
* Piped into a file or another program, or read out by a screen reader, it would only be
|
|
17
|
+
* noise. `columns` is undefined when the output isn't a terminal.
|
|
18
|
+
*/
|
|
19
|
+
export declare function showCloud(columns: number | undefined): boolean;
|
|
8
20
|
export interface WelcomeStatus {
|
|
9
21
|
mark: '✓' | '!' | '-';
|
|
10
22
|
text: string;
|
|
11
23
|
}
|
|
12
|
-
|
|
13
|
-
export declare function renderWelcome(options: {
|
|
24
|
+
interface WelcomeOptions {
|
|
14
25
|
version: string;
|
|
15
26
|
status: WelcomeStatus[];
|
|
16
27
|
color: boolean;
|
|
17
28
|
/** Ends with the commands to try next. On by default. */
|
|
18
29
|
hints?: boolean;
|
|
19
|
-
|
|
30
|
+
/** Draws the cloud. On by default; see showCloud for when to leave it out. */
|
|
31
|
+
cloud?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The welcome screen: the cloud with the name and tagline beside it, then the install's status. */
|
|
34
|
+
export declare function renderWelcome(options: WelcomeOptions): string;
|
|
35
|
+
/** The top of the welcome screen: the cloud and the words beside it, or just the words. */
|
|
36
|
+
export declare function renderWelcomeHead(options: Omit<WelcomeOptions, 'status'>): string;
|
|
37
|
+
/** The rest of the welcome screen: the install's status and the commands to try. */
|
|
38
|
+
export declare function renderWelcomeTail(options: Omit<WelcomeOptions, 'version' | 'cloud'>): string;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the cloud may wink: only when it's drawn, so in a wide enough terminal, and not in
|
|
41
|
+
* CI, a dumb terminal, or for someone who has set WHEEMS_NO_ANIMATION.
|
|
42
|
+
*/
|
|
43
|
+
export declare function canWink(columns: number | undefined, env: NodeJS.ProcessEnv): boolean;
|
|
44
|
+
/** How long the eye stays open before the wink, and how long it stays shut. */
|
|
45
|
+
export declare const WINK_TIMING: {
|
|
46
|
+
readonly before: 400;
|
|
47
|
+
readonly shut: 150;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The cloud winks once: run straight after renderWelcomeHead is written, with the cursor on the
|
|
51
|
+
* line under the cloud. It redraws only the eye's row, hides the cursor meanwhile and always
|
|
52
|
+
* shows it again.
|
|
53
|
+
*/
|
|
54
|
+
export declare function wink(options: {
|
|
55
|
+
version: string;
|
|
56
|
+
color: boolean;
|
|
57
|
+
write: (text: string) => void;
|
|
58
|
+
sleep?: (ms: number) => Promise<void>;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
export declare const HIDE_CURSOR = "\u001B[?25l";
|
|
61
|
+
export declare const SHOW_CURSOR = "\u001B[?25h";
|
|
20
62
|
/**
|
|
21
63
|
* What the welcome screen says about this install: its workspaces and agents, the service,
|
|
22
64
|
* search, and anything waiting for review. Quick: it reads files and pings the service, and
|
|
23
65
|
* never starts a session.
|
|
24
66
|
*/
|
|
25
67
|
export declare function welcomeStatus(home: string, env: NodeJS.ProcessEnv, ping?: (socket: string) => Promise<boolean>): Promise<WelcomeStatus[]>;
|
|
68
|
+
export {};
|
package/dist/cli/welcome.js
CHANGED
|
@@ -6,51 +6,70 @@ import { listAboutYou } from '../review/review.js';
|
|
|
6
6
|
import { request } from '../service/control.js';
|
|
7
7
|
import { listProposals } from '../skills/proposals.js';
|
|
8
8
|
/**
|
|
9
|
-
* The cloud
|
|
10
|
-
*
|
|
9
|
+
* The cloud: the mascot's outline (the brand kit's mascot.svg), traced in Unicode half blocks,
|
|
10
|
+
* with its eye and smile placed by hand so they read at this size. Half blocks are in every
|
|
11
|
+
* monospace font and most terminals draw them as exact rectangles, so it looks the same
|
|
12
|
+
* everywhere; an outline also reads on light and dark backgrounds alike. It has no colour of
|
|
13
|
+
* its own: it's drawn in the terminal's text colour, like the brand's ink app icon, so it's white
|
|
14
|
+
* on a dark terminal and near-black on a light one.
|
|
11
15
|
*/
|
|
12
16
|
// Every character is one UTF-16 unit, so .length is the width in columns.
|
|
13
17
|
export const CLOUD = [
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
18
|
+
' ▄█████▄▄',
|
|
19
|
+
' ▄█▀ ▀▀█▄',
|
|
20
|
+
' ▄█████▀ ▀█▄',
|
|
21
|
+
' ██ ██',
|
|
22
|
+
' ▄██ █▀',
|
|
23
|
+
' ██▀ ██ ▀█▄▄',
|
|
24
|
+
'██ ▀█▄',
|
|
25
|
+
'██ ▀▄▄▀ ██',
|
|
26
|
+
' ██▄ ▄█▀',
|
|
27
|
+
' ▀▀█████████████████▀',
|
|
24
28
|
];
|
|
25
29
|
/** The brand line under the name. */
|
|
26
30
|
export const TAGLINE = 'Your crew, ready for work.';
|
|
31
|
+
const CLOUD_WIDTH = Math.max(...CLOUD.map((line) => line.length));
|
|
32
|
+
/** The columns the cloud and the words beside it need. */
|
|
33
|
+
export const WELCOME_WIDTH = CLOUD_WIDTH + 3 + TAGLINE.length;
|
|
27
34
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
35
|
+
* Whether to draw the cloud: only in a terminal wide enough for it and the words beside it.
|
|
36
|
+
* Piped into a file or another program, or read out by a screen reader, it would only be
|
|
37
|
+
* noise. `columns` is undefined when the output isn't a terminal.
|
|
30
38
|
*/
|
|
31
|
-
|
|
39
|
+
export function showCloud(columns) {
|
|
40
|
+
return columns !== undefined && columns >= WELCOME_WIDTH;
|
|
41
|
+
}
|
|
32
42
|
const BOLD = '\x1b[1m';
|
|
33
43
|
const DIM = '\x1b[2m';
|
|
34
44
|
const RESET = '\x1b[0m';
|
|
35
45
|
/** The welcome screen: the cloud with the name and tagline beside it, then the install's status. */
|
|
36
46
|
export function renderWelcome(options) {
|
|
47
|
+
return renderWelcomeHead(options) + renderWelcomeTail(options);
|
|
48
|
+
}
|
|
49
|
+
/** The row of the cloud with the eye in it, and the eye's columns. */
|
|
50
|
+
const EYE = { row: 5, from: 6, to: 8 };
|
|
51
|
+
function cloudRow(options, row, line) {
|
|
37
52
|
const paint = (code, text) => (options.color ? `${code}${text}${RESET}` : text);
|
|
38
|
-
const width = Math.max(...CLOUD.map((line) => line.length));
|
|
39
53
|
const beside = {
|
|
40
54
|
4: paint(BOLD, 'wheems') + ' ' + paint(DIM, options.version),
|
|
41
55
|
5: TAGLINE,
|
|
42
56
|
};
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
const text = beside[row];
|
|
58
|
+
return text === undefined ? line : `${line.padEnd(CLOUD_WIDTH)} ${text}`;
|
|
59
|
+
}
|
|
60
|
+
/** The top of the welcome screen: the cloud and the words beside it, or just the words. */
|
|
61
|
+
export function renderWelcomeHead(options) {
|
|
62
|
+
const paint = (code, text) => (options.color ? `${code}${text}${RESET}` : text);
|
|
63
|
+
const lines = options.cloud === false
|
|
64
|
+
? [paint(BOLD, 'wheems') + ' ' + paint(DIM, options.version), TAGLINE]
|
|
65
|
+
: CLOUD.map((line, row) => cloudRow(options, row, line));
|
|
66
|
+
return ['', ...lines].join('\n') + '\n';
|
|
67
|
+
}
|
|
68
|
+
/** The rest of the welcome screen: the install's status and the commands to try. */
|
|
69
|
+
export function renderWelcomeTail(options) {
|
|
70
|
+
const paint = (code, text) => (options.color ? `${code}${text}${RESET}` : text);
|
|
50
71
|
const marks = { '✓': '\x1b[32m', '!': '\x1b[33m', '-': DIM };
|
|
51
72
|
return [
|
|
52
|
-
'',
|
|
53
|
-
...lines,
|
|
54
73
|
'',
|
|
55
74
|
...options.status.map((s) => `${paint(marks[s.mark], s.mark)} ${s.text}`),
|
|
56
75
|
...(options.status.length > 0 ? [''] : []),
|
|
@@ -59,6 +78,45 @@ export function renderWelcome(options) {
|
|
|
59
78
|
: [paint(DIM, 'wheems setup · wheems doctor · wheems --help'), '']),
|
|
60
79
|
].join('\n');
|
|
61
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether the cloud may wink: only when it's drawn, so in a wide enough terminal, and not in
|
|
83
|
+
* CI, a dumb terminal, or for someone who has set WHEEMS_NO_ANIMATION.
|
|
84
|
+
*/
|
|
85
|
+
export function canWink(columns, env) {
|
|
86
|
+
return (showCloud(columns) &&
|
|
87
|
+
env.TERM !== 'dumb' &&
|
|
88
|
+
(env.CI ?? '') === '' &&
|
|
89
|
+
(env.WHEEMS_NO_ANIMATION ?? '') === '');
|
|
90
|
+
}
|
|
91
|
+
/** How long the eye stays open before the wink, and how long it stays shut. */
|
|
92
|
+
export const WINK_TIMING = { before: 400, shut: 150 };
|
|
93
|
+
/**
|
|
94
|
+
* The cloud winks once: run straight after renderWelcomeHead is written, with the cursor on the
|
|
95
|
+
* line under the cloud. It redraws only the eye's row, hides the cursor meanwhile and always
|
|
96
|
+
* shows it again.
|
|
97
|
+
*/
|
|
98
|
+
export async function wink(options) {
|
|
99
|
+
const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
100
|
+
const up = CLOUD.length - EYE.row;
|
|
101
|
+
const open = CLOUD[EYE.row] ?? '';
|
|
102
|
+
const shut = open.slice(0, EYE.from) + '▄▄' + open.slice(EYE.to);
|
|
103
|
+
// Up to the eye's row, clear it, draw it again, and back down to where the cursor was.
|
|
104
|
+
const redraw = (line) => {
|
|
105
|
+
options.write(`\x1b[${String(up)}A\r\x1b[2K${cloudRow(options, EYE.row, line)}\x1b[${String(up)}B\r`);
|
|
106
|
+
};
|
|
107
|
+
options.write(HIDE_CURSOR);
|
|
108
|
+
try {
|
|
109
|
+
await sleep(WINK_TIMING.before);
|
|
110
|
+
redraw(shut);
|
|
111
|
+
await sleep(WINK_TIMING.shut);
|
|
112
|
+
redraw(open);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
options.write(SHOW_CURSOR);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export const HIDE_CURSOR = '\x1b[?25l';
|
|
119
|
+
export const SHOW_CURSOR = '\x1b[?25h';
|
|
62
120
|
/**
|
|
63
121
|
* What the welcome screen says about this install: its workspaces and agents, the service,
|
|
64
122
|
* search, and anything waiting for review. Quick: it reads files and pings the service, and
|
|
@@ -11,3 +11,8 @@ export interface SlackConnectOptions {
|
|
|
11
11
|
* its two tokens (checked with Slack), and say who may talk to agents.
|
|
12
12
|
*/
|
|
13
13
|
export declare function slackConnectFlow(options?: SlackConnectOptions): ConnectFlow;
|
|
14
|
+
/**
|
|
15
|
+
* What's wrong with a pasted token, said so the person can fix it, or undefined if it looks
|
|
16
|
+
* right. It names a token only by its standard prefix, never by any other part of it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function tokenProblem(kind: 'bot' | 'app', value: string): string | undefined;
|
|
@@ -93,7 +93,8 @@ async function newTokens(ui, context, verify) {
|
|
|
93
93
|
copied
|
|
94
94
|
? '2. Paste the manifest: it is on your clipboard.'
|
|
95
95
|
: `2. Paste the manifest from ${manifestFile}`,
|
|
96
|
-
'3. Install the app to the workspace
|
|
96
|
+
'3. Install the app to the workspace. Under OAuth & Permissions, copy the',
|
|
97
|
+
' Bot User OAuth Token (xoxb-…).',
|
|
97
98
|
'4. Under Basic Information → App-Level Tokens, generate a token with the',
|
|
98
99
|
' connections:write scope (xapp-…).',
|
|
99
100
|
'',
|
|
@@ -106,11 +107,11 @@ async function newTokens(ui, context, verify) {
|
|
|
106
107
|
for (;;) {
|
|
107
108
|
const botToken = await ui.secret({
|
|
108
109
|
message: 'Bot User OAuth Token',
|
|
109
|
-
validate: (value) =>
|
|
110
|
+
validate: (value) => tokenProblem('bot', value),
|
|
110
111
|
});
|
|
111
112
|
const appToken = await ui.secret({
|
|
112
113
|
message: 'App-Level Token',
|
|
113
|
-
validate: (value) =>
|
|
114
|
+
validate: (value) => tokenProblem('app', value),
|
|
114
115
|
});
|
|
115
116
|
try {
|
|
116
117
|
await ui.task('Checking the tokens with Slack', () => verify(botToken.trim(), appToken.trim()), (id) => `Connected as ${id.botUser} in ${id.team}`);
|
|
@@ -123,4 +124,53 @@ async function newTokens(ui, context, verify) {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
/** What each token is called in Slack, where to find it, and how it starts. */
|
|
128
|
+
const TOKENS = {
|
|
129
|
+
bot: {
|
|
130
|
+
name: 'bot token',
|
|
131
|
+
prefix: 'xoxb-',
|
|
132
|
+
where: 'Under OAuth & Permissions, copy the Bot User OAuth Token',
|
|
133
|
+
},
|
|
134
|
+
app: {
|
|
135
|
+
name: 'app-level token',
|
|
136
|
+
prefix: 'xapp-',
|
|
137
|
+
where: 'Under Basic Information → App-Level Tokens, copy the token',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
/** Other Slack tokens people paste by mistake, by their standard prefix. */
|
|
141
|
+
const OTHER_TOKENS = {
|
|
142
|
+
'xoxp-': 'a user token',
|
|
143
|
+
'xoxe-': 'a refresh token',
|
|
144
|
+
'xoxa-': 'a workspace token',
|
|
145
|
+
};
|
|
146
|
+
const CLEAR = 'Clear the field with Backspace, then paste it once.';
|
|
147
|
+
/**
|
|
148
|
+
* What's wrong with a pasted token, said so the person can fix it, or undefined if it looks
|
|
149
|
+
* right. It names a token only by its standard prefix, never by any other part of it.
|
|
150
|
+
*/
|
|
151
|
+
export function tokenProblem(kind, value) {
|
|
152
|
+
const want = TOKENS[kind];
|
|
153
|
+
const other = TOKENS[kind === 'bot' ? 'app' : 'bot'];
|
|
154
|
+
const token = value.trim();
|
|
155
|
+
const fix = `${want.where} (${want.prefix}…).`;
|
|
156
|
+
if (token === '')
|
|
157
|
+
return `Paste the ${want.name}. ${fix}`;
|
|
158
|
+
if (token.split(want.prefix).length > 2) {
|
|
159
|
+
return `That looks like more than one paste. ${CLEAR}`;
|
|
160
|
+
}
|
|
161
|
+
if (token.startsWith(want.prefix)) {
|
|
162
|
+
return /\s/.test(token) ? `There's a space or line break in it. ${CLEAR}` : undefined;
|
|
163
|
+
}
|
|
164
|
+
if (token.includes(want.prefix)) {
|
|
165
|
+
return `There's something before the ${want.prefix} at the start. ${CLEAR}`;
|
|
166
|
+
}
|
|
167
|
+
if (token.startsWith(other.prefix)) {
|
|
168
|
+
return `That starts with ${other.prefix}, so it's the ${other.name}. ${fix}`;
|
|
169
|
+
}
|
|
170
|
+
const known = Object.entries(OTHER_TOKENS).find(([prefix]) => token.startsWith(prefix));
|
|
171
|
+
if (known !== undefined) {
|
|
172
|
+
return `That starts with ${known[0]}, so it's ${known[1]}, not the ${want.name}. ${fix}`;
|
|
173
|
+
}
|
|
174
|
+
return `That isn't the ${want.name}: it starts with ${want.prefix}. ${fix}`;
|
|
175
|
+
}
|
|
126
176
|
//# sourceMappingURL=connect.js.map
|