@raidiant/notifai 10.1.1 → 10.1.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 +35 -0
- package/dist/commands-core.d.ts +4 -1
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.js +2 -7
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.d.ts +12 -0
- package/dist/commands-hooks.js +15 -1
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.js +9 -6
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +10 -11
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/harness-session-title.d.ts +22 -0
- package/dist/harness-session-title.js +74 -0
- package/dist/harness-session-title.js.map +1 -0
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +15 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +2 -1
- package/dist/node-floor.js +9 -5
- package/dist/node-floor.js.map +1 -1
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/session-labels.d.ts +2 -0
- package/dist/session-labels.js +20 -9
- package/dist/session-labels.js.map +1 -1
- package/dist/setup-destinations.js +1 -1
- package/dist/setup-destinations.js.map +1 -1
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -176
- package/src/cli-bin.ts +0 -233
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/cli-update-contract.ts +0 -9
- package/src/cli-update.test.ts +0 -125
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -352
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -301
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -238
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -312
- package/src/commands-devices.ts +0 -180
- package/src/commands-doctor.ts +0 -1501
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2318
- package/src/commands-init.ts +0 -1055
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -875
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands-update.ts +0 -180
- package/src/commands.test.ts +0 -8297
- package/src/commands.ts +0 -77
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -424
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5824
- package/src/hooks.ts +0 -4011
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main-run.ts +0 -45
- package/src/main.ts +0 -12
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/node-floor.test.ts +0 -27
- package/src/node-floor.ts +0 -37
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -317
- package/src/program.ts +0 -842
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-settlement-process.ts +0 -47
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -212
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/setup-destinations.test.ts +0 -41
- package/src/setup-destinations.ts +0 -50
- package/src/skill.test.ts +0 -426
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/interactive.ts
DELETED
|
@@ -1,858 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The interactive app — what `notifai` does when a person runs it.
|
|
3
|
-
*
|
|
4
|
-
* Everything here is a shell around commands that already exist. `init`,
|
|
5
|
-
* `login`, `doctor`, `send`, `hooks install` and `config set` keep their
|
|
6
|
-
* behaviour, their validation and their exit codes; this module decides what to
|
|
7
|
-
* offer, in what order, and how to say it. That is deliberate: a second
|
|
8
|
-
* implementation of "set a config value" would be a second set of bugs, and the
|
|
9
|
-
* menu would slowly stop agreeing with the flags.
|
|
10
|
-
*
|
|
11
|
-
* The module is imported dynamically, only after the caller is known to be a
|
|
12
|
-
* human at a terminal. Nothing an agent runs — `send`, `ask`, and above all the
|
|
13
|
-
* hook that runs in front of every prompt the user types — may pay to load a
|
|
14
|
-
* prompt library it will never draw.
|
|
15
|
-
*
|
|
16
|
-
* Three things shape the layout:
|
|
17
|
-
*
|
|
18
|
-
* First-run is `init`, not a second sign-in/device split. An unsigned machine
|
|
19
|
-
* enters that journey immediately; a remaining blocker is offered as Finish
|
|
20
|
-
* setup. Once send-prerequisites are ready, the menu leads with the test
|
|
21
|
-
* notification, because seeing the thing arrive on a device is what makes the
|
|
22
|
-
* product real.
|
|
23
|
-
*
|
|
24
|
-
* Detail is earned, not given. A settings group shows fourteen one-line
|
|
25
|
-
* summaries, and the paragraph explaining a key appears once the reader has
|
|
26
|
-
* picked that key.
|
|
27
|
-
*
|
|
28
|
-
* Cancelling always goes back one level and never leaves the terminal in a
|
|
29
|
-
* strange state.
|
|
30
|
-
*/
|
|
31
|
-
import * as clack from '@clack/prompts'
|
|
32
|
-
import {
|
|
33
|
-
EXIT,
|
|
34
|
-
assessReadiness,
|
|
35
|
-
canDeviceReceive,
|
|
36
|
-
configSetCommand,
|
|
37
|
-
deviceInventory,
|
|
38
|
-
doctorCommand,
|
|
39
|
-
hooksInstallCommand,
|
|
40
|
-
hooksUninstallCommand,
|
|
41
|
-
initCommand,
|
|
42
|
-
loginCommand,
|
|
43
|
-
logoutCommand,
|
|
44
|
-
sendCommand,
|
|
45
|
-
type CommandDeps,
|
|
46
|
-
} from './commands.js'
|
|
47
|
-
import { CONFIG_KEYS, loadConfig, type CliConfig, type ConfigKey } from './config.js'
|
|
48
|
-
import {
|
|
49
|
-
detectedHarnesses,
|
|
50
|
-
findInstallations,
|
|
51
|
-
type Installation,
|
|
52
|
-
} from './install-hooks.js'
|
|
53
|
-
import {
|
|
54
|
-
CONFIG_GROUPS,
|
|
55
|
-
acceptedValues,
|
|
56
|
-
boundsHint,
|
|
57
|
-
configInfo,
|
|
58
|
-
configKeysByGroup,
|
|
59
|
-
describeSource,
|
|
60
|
-
formatValue,
|
|
61
|
-
type ConfigGroup,
|
|
62
|
-
} from './config-schema.js'
|
|
63
|
-
import {
|
|
64
|
-
firstBlocker,
|
|
65
|
-
refreshAfterMenuAction,
|
|
66
|
-
type Readiness,
|
|
67
|
-
type ReadinessRefresh,
|
|
68
|
-
type ReadinessState,
|
|
69
|
-
} from './readiness.js'
|
|
70
|
-
import { printBanner, tagline } from './ui/banner.js'
|
|
71
|
-
import { card, glyph, mark, style, type Tone } from './ui/theme.js'
|
|
72
|
-
|
|
73
|
-
/** Clack returns a symbol when the user pressed Ctrl-C or Escape. */
|
|
74
|
-
function cancelled(value: unknown): value is symbol {
|
|
75
|
-
return clack.isCancel(value)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type HookScope = 'project' | 'machine'
|
|
79
|
-
|
|
80
|
-
export type RoutingAction = 'install' | 'uninstall' | 'settings' | 'back'
|
|
81
|
-
|
|
82
|
-
export interface MenuOption<Value extends string> {
|
|
83
|
-
value: Value
|
|
84
|
-
label: string
|
|
85
|
-
hint?: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Which hook actions this directory and setup can actually perform.
|
|
90
|
-
*
|
|
91
|
-
* Install is offered only when a harness is detectable here. Uninstall is
|
|
92
|
-
* offered only for a scope that already has Notifai hooks, and the label names
|
|
93
|
-
* that scope so a machine-wide install is never stripped from a random folder
|
|
94
|
-
* under a generic "uninstall".
|
|
95
|
-
*/
|
|
96
|
-
export function routingHookActions(input: {
|
|
97
|
-
canInstall: boolean
|
|
98
|
-
projectInstallations: readonly { harness: string }[]
|
|
99
|
-
machineInstallations: readonly { harness: string }[]
|
|
100
|
-
hooksReady: boolean
|
|
101
|
-
}): MenuOption<RoutingAction>[] {
|
|
102
|
-
const options: MenuOption<RoutingAction>[] = []
|
|
103
|
-
const hasProject = input.projectInstallations.length > 0
|
|
104
|
-
const hasMachine = input.machineInstallations.length > 0
|
|
105
|
-
const hasAny = hasProject || hasMachine
|
|
106
|
-
|
|
107
|
-
if (input.canInstall) {
|
|
108
|
-
options.push({
|
|
109
|
-
value: 'install',
|
|
110
|
-
label: input.hooksReady || hasAny ? 'Re-install hooks' : 'Install hooks',
|
|
111
|
-
hint: 'this project or this machine',
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (hasProject && hasMachine) {
|
|
116
|
-
options.push({
|
|
117
|
-
value: 'uninstall',
|
|
118
|
-
label: 'Uninstall hooks',
|
|
119
|
-
hint: 'this project or this machine',
|
|
120
|
-
})
|
|
121
|
-
} else if (hasProject) {
|
|
122
|
-
options.push({
|
|
123
|
-
value: 'uninstall',
|
|
124
|
-
label: 'Uninstall hooks for this project',
|
|
125
|
-
hint: harnessHint(input.projectInstallations),
|
|
126
|
-
})
|
|
127
|
-
} else if (hasMachine) {
|
|
128
|
-
options.push({
|
|
129
|
-
value: 'uninstall',
|
|
130
|
-
label: 'Uninstall hooks on this machine',
|
|
131
|
-
hint: harnessHint(input.machineInstallations),
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
options.push(
|
|
136
|
-
{ value: 'settings', label: 'Change question settings', hint: 'when a question may leave this terminal' },
|
|
137
|
-
{ value: 'back', label: '← Back' },
|
|
138
|
-
)
|
|
139
|
-
return options
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Scope choices that would actually remove something. Project first. */
|
|
143
|
-
export function uninstallScopeOptions(
|
|
144
|
-
projectInstallations: readonly { harness: string }[],
|
|
145
|
-
machineInstallations: readonly { harness: string }[],
|
|
146
|
-
): MenuOption<HookScope>[] {
|
|
147
|
-
const options: MenuOption<HookScope>[] = []
|
|
148
|
-
if (projectInstallations.length > 0) {
|
|
149
|
-
options.push({
|
|
150
|
-
value: 'project',
|
|
151
|
-
label: 'This project',
|
|
152
|
-
hint: 'remove hooks from this directory',
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
if (machineInstallations.length > 0) {
|
|
156
|
-
options.push({
|
|
157
|
-
value: 'machine',
|
|
158
|
-
label: 'This machine',
|
|
159
|
-
hint: 'remove hooks for every project here',
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
return options
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function harnessHint(installations: readonly { harness: string }[]): string {
|
|
166
|
-
return [...new Set(installations.map((installation) => installation.harness))].join(', ')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
type Screen =
|
|
170
|
-
| 'test'
|
|
171
|
-
| 'devices'
|
|
172
|
-
| 'settings'
|
|
173
|
-
| 'routing'
|
|
174
|
-
| 'doctor'
|
|
175
|
-
| 'account'
|
|
176
|
-
| 'setup'
|
|
177
|
-
| 'quit'
|
|
178
|
-
|
|
179
|
-
export async function interactiveCommand(deps: CommandDeps, version: string): Promise<number> {
|
|
180
|
-
await printBanner()
|
|
181
|
-
process.stdout.write(` ${tagline(version)}\n`)
|
|
182
|
-
|
|
183
|
-
let readiness = await assess(deps)
|
|
184
|
-
if (stateById(readiness, 'credential')?.status !== 'ready') {
|
|
185
|
-
await initCommand(deps, {})
|
|
186
|
-
readiness = await assess(deps)
|
|
187
|
-
}
|
|
188
|
-
let first = true
|
|
189
|
-
|
|
190
|
-
for (;;) {
|
|
191
|
-
renderStatus(readiness, { compact: !first })
|
|
192
|
-
first = false
|
|
193
|
-
|
|
194
|
-
const choice = await clack.select<Screen>({
|
|
195
|
-
message: 'What would you like to do?',
|
|
196
|
-
options: menuFor(readiness),
|
|
197
|
-
})
|
|
198
|
-
// Root is the only place Escape/Ctrl-C quits. Every nested prompt returns.
|
|
199
|
-
if (cancelled(choice)) return farewell()
|
|
200
|
-
|
|
201
|
-
switch (choice) {
|
|
202
|
-
case 'quit':
|
|
203
|
-
return farewell()
|
|
204
|
-
case 'setup':
|
|
205
|
-
await initCommand(deps, {})
|
|
206
|
-
readiness = await refresh(deps, readiness, refreshAfterMenuAction('setup', true))
|
|
207
|
-
break
|
|
208
|
-
case 'account': {
|
|
209
|
-
const changed = await accountScreen(deps, readiness)
|
|
210
|
-
readiness = await refresh(deps, readiness, refreshAfterMenuAction('account', changed))
|
|
211
|
-
break
|
|
212
|
-
}
|
|
213
|
-
case 'test':
|
|
214
|
-
await testNotificationScreen(deps)
|
|
215
|
-
break
|
|
216
|
-
case 'devices':
|
|
217
|
-
await devicesScreen(deps)
|
|
218
|
-
break
|
|
219
|
-
case 'settings': {
|
|
220
|
-
const result = await settingsScreen(deps)
|
|
221
|
-
readiness = await refresh(
|
|
222
|
-
deps,
|
|
223
|
-
readiness,
|
|
224
|
-
refreshAfterMenuAction('settings', result !== 'unchanged', { remote: result === 'remote' }),
|
|
225
|
-
)
|
|
226
|
-
break
|
|
227
|
-
}
|
|
228
|
-
case 'routing': {
|
|
229
|
-
const changed = await routingScreen(deps, readiness)
|
|
230
|
-
readiness = await refresh(deps, readiness, refreshAfterMenuAction('routing', changed))
|
|
231
|
-
break
|
|
232
|
-
}
|
|
233
|
-
case 'doctor':
|
|
234
|
-
// One assessment serves the report and the redraw. Doctor does not
|
|
235
|
-
// change setup, so running the graph again would only wait.
|
|
236
|
-
readiness = await assess(deps)
|
|
237
|
-
await doctorCommand(deps, {}, { readiness })
|
|
238
|
-
break
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function farewell(): number {
|
|
244
|
-
clack.outro(style.dim(`${glyph.bullet} bye`))
|
|
245
|
-
return EXIT.ok
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function assess(
|
|
249
|
-
deps: CommandDeps,
|
|
250
|
-
options: { previous?: Readiness; refresh?: readonly ReadinessRefresh[] } = {},
|
|
251
|
-
): Promise<Readiness> {
|
|
252
|
-
const spinner = clack.spinner()
|
|
253
|
-
spinner.start('Checking your setup')
|
|
254
|
-
try {
|
|
255
|
-
return await assessReadiness(deps, options)
|
|
256
|
-
} finally {
|
|
257
|
-
spinner.stop(style.dim('Setup checked'))
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function refresh(
|
|
262
|
-
deps: CommandDeps,
|
|
263
|
-
previous: Readiness,
|
|
264
|
-
next: readonly ReadinessRefresh[] | null,
|
|
265
|
-
): Promise<Readiness> {
|
|
266
|
-
return next === null ? previous : assess(deps, { previous, refresh: next })
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
// Status
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
|
|
273
|
-
function stateById(readiness: Readiness, id: string): ReadinessState | undefined {
|
|
274
|
-
return readiness.states.find((state) => state.id === id)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function toneOf(state: ReadinessState | undefined): Tone {
|
|
278
|
-
if (state === undefined) return 'pending'
|
|
279
|
-
switch (state.status) {
|
|
280
|
-
case 'ready':
|
|
281
|
-
return 'ok'
|
|
282
|
-
case 'gap':
|
|
283
|
-
return 'bad'
|
|
284
|
-
case 'optional-gap':
|
|
285
|
-
return 'warn'
|
|
286
|
-
case 'unknown':
|
|
287
|
-
return 'pending'
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* The status card: four lines that answer "is this working", with the detail
|
|
293
|
-
* behind `doctor` rather than in front of everyone.
|
|
294
|
-
*
|
|
295
|
-
* `doctor` reports fourteen states because that is what a report is for. Home
|
|
296
|
-
* shows the four that change what you would do next, which is why this reads
|
|
297
|
-
* the same readiness model rather than a second one that could disagree.
|
|
298
|
-
*/
|
|
299
|
-
function renderStatus(readiness: Readiness, options: { compact?: boolean }): void {
|
|
300
|
-
const rows: { label: string; state: ReadinessState | undefined; summary: (s?: ReadinessState) => string }[] = [
|
|
301
|
-
{
|
|
302
|
-
label: 'Account',
|
|
303
|
-
state: stateById(readiness, 'credential'),
|
|
304
|
-
summary: (s) => (s?.status === 'ready' ? accountSummary(readiness) : 'not signed in'),
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
label: 'Devices',
|
|
308
|
-
state: stateById(readiness, 'devices'),
|
|
309
|
-
summary: (s) => shorten(s?.detail ?? 'unknown'),
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
label: 'Project',
|
|
313
|
-
state: stateById(readiness, 'project'),
|
|
314
|
-
summary: (s) => (s?.status === 'ready' ? firstQuoted(s.detail) : 'not named'),
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
label: 'Questions',
|
|
318
|
-
state: stateById(readiness, 'hooks'),
|
|
319
|
-
summary: (s) => (s?.status === 'ready' ? 'routing to your devices' : shorten(s?.detail ?? 'not wired')),
|
|
320
|
-
},
|
|
321
|
-
]
|
|
322
|
-
|
|
323
|
-
if (options.compact === true) {
|
|
324
|
-
const summary = rows.map((row) => `${mark(toneOf(row.state))} ${style.dim(row.label)}`).join(' ')
|
|
325
|
-
process.stdout.write(`\n ${summary}\n\n`)
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const labelWidth = Math.max(...rows.map((row) => row.label.length))
|
|
330
|
-
const body = rows.map(
|
|
331
|
-
(row) => `${mark(toneOf(row.state))} ${style.dim(row.label.padEnd(labelWidth))} ${row.summary(row.state)}`,
|
|
332
|
-
)
|
|
333
|
-
process.stdout.write('\n')
|
|
334
|
-
for (const line of card('Status', body)) process.stdout.write(`${line}\n`)
|
|
335
|
-
process.stdout.write('\n')
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function accountSummary(readiness: Readiness): string {
|
|
339
|
-
const auth = stateById(readiness, 'auth')
|
|
340
|
-
const email = auth?.detail.match(/\(([^)]+@[^)]+)\)/)?.[1]
|
|
341
|
-
return email ?? 'signed in'
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function firstQuoted(text: string): string {
|
|
345
|
-
return text.match(/"([^"]+)"/)?.[1] ?? text
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function shorten(text: string, limit = 46): string {
|
|
349
|
-
const flat = text.replace(/\s+/g, ' ').trim()
|
|
350
|
-
const clipped = flat.split(' — ')[0] ?? flat
|
|
351
|
-
return clipped.length > limit ? `${clipped.slice(0, limit - 1)}…` : clipped
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* The menu, ordered by what is actually in the way.
|
|
356
|
-
*
|
|
357
|
-
* A fixed menu would put "Send a test notification" first for someone who has
|
|
358
|
-
* not signed in — an action that cannot succeed, offered ahead of the one that
|
|
359
|
-
* would unblock it.
|
|
360
|
-
*/
|
|
361
|
-
function menuFor(readiness: Readiness): { value: Screen; label: string; hint?: string }[] {
|
|
362
|
-
const signedIn = stateById(readiness, 'credential')?.status === 'ready'
|
|
363
|
-
const devices = stateById(readiness, 'devices')
|
|
364
|
-
const hasDevice = devices?.status === 'ready'
|
|
365
|
-
const routing = stateById(readiness, 'hooks')
|
|
366
|
-
const blocker = firstBlocker(readiness)
|
|
367
|
-
|
|
368
|
-
const options: { value: Screen; label: string; hint?: string }[] = []
|
|
369
|
-
|
|
370
|
-
if (blocker !== null) {
|
|
371
|
-
options.push({
|
|
372
|
-
value: 'setup',
|
|
373
|
-
label: 'Finish setup',
|
|
374
|
-
hint: shorten(blocker.title.toLowerCase(), 40),
|
|
375
|
-
})
|
|
376
|
-
} else if (signedIn && hasDevice) {
|
|
377
|
-
options.push({
|
|
378
|
-
value: 'test',
|
|
379
|
-
label: 'Send a test notification',
|
|
380
|
-
hint: 'prove the whole path, end to end',
|
|
381
|
-
})
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
options.push(
|
|
385
|
-
{ value: 'settings', label: 'Settings', hint: 'sounds, timing, presence, project' },
|
|
386
|
-
{
|
|
387
|
-
value: 'routing',
|
|
388
|
-
label: 'Question routing',
|
|
389
|
-
hint: routing?.status === 'ready' ? 'wired — review or change' : 'let agents reach you when you step away',
|
|
390
|
-
},
|
|
391
|
-
)
|
|
392
|
-
if (signedIn && hasDevice) {
|
|
393
|
-
options.push({ value: 'devices', label: 'Devices', hint: 'what can receive right now' })
|
|
394
|
-
}
|
|
395
|
-
options.push({ value: 'doctor', label: 'Run diagnostics', hint: 'check every part of the setup' })
|
|
396
|
-
if (signedIn) {
|
|
397
|
-
options.push({ value: 'account', label: 'Account', hint: 'plan, machine, sign out' })
|
|
398
|
-
}
|
|
399
|
-
options.push({ value: 'quit', label: 'Quit' })
|
|
400
|
-
return options
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
// Test notification
|
|
405
|
-
// ---------------------------------------------------------------------------
|
|
406
|
-
|
|
407
|
-
async function testNotificationScreen(deps: CommandDeps): Promise<void> {
|
|
408
|
-
const config = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
409
|
-
const project = config.project.value
|
|
410
|
-
|
|
411
|
-
const title = await clack.text({
|
|
412
|
-
message: 'Title',
|
|
413
|
-
placeholder: 'Test from Notifai',
|
|
414
|
-
defaultValue: 'Test from Notifai',
|
|
415
|
-
})
|
|
416
|
-
if (cancelled(title)) return
|
|
417
|
-
|
|
418
|
-
const body = await clack.text({
|
|
419
|
-
message: 'Body',
|
|
420
|
-
placeholder: 'If you can read this on your device, it works.',
|
|
421
|
-
defaultValue: 'If you can read this on your device, it works.',
|
|
422
|
-
})
|
|
423
|
-
if (cancelled(body)) return
|
|
424
|
-
|
|
425
|
-
const kind = await clack.select<string>({
|
|
426
|
-
message: 'Kind',
|
|
427
|
-
options: [
|
|
428
|
-
{ value: 'update', label: 'Update', hint: 'something happened' },
|
|
429
|
-
{ value: 'done', label: 'Done', hint: 'work finished' },
|
|
430
|
-
],
|
|
431
|
-
initialValue: 'done',
|
|
432
|
-
})
|
|
433
|
-
if (cancelled(kind)) return
|
|
434
|
-
|
|
435
|
-
clack.log.step('Sending, then waiting for your devices to confirm…')
|
|
436
|
-
const code = await sendCommand(deps, {
|
|
437
|
-
title: String(title),
|
|
438
|
-
body: String(body),
|
|
439
|
-
kind: String(kind),
|
|
440
|
-
...(project !== null ? { project } : {}),
|
|
441
|
-
})
|
|
442
|
-
if (code === EXIT.ok) clack.log.success('Delivered. Check the device it landed on.')
|
|
443
|
-
else clack.log.error('That did not get through — `Run diagnostics` will say why.')
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
// Devices
|
|
448
|
-
// ---------------------------------------------------------------------------
|
|
449
|
-
|
|
450
|
-
async function devicesScreen(deps: CommandDeps): Promise<void> {
|
|
451
|
-
const spinner = clack.spinner()
|
|
452
|
-
spinner.start('Loading your devices')
|
|
453
|
-
const devices = await deviceInventory(deps)
|
|
454
|
-
spinner.stop(style.dim('Devices loaded'))
|
|
455
|
-
|
|
456
|
-
if (devices === null) {
|
|
457
|
-
clack.log.warn('Could not reach the service. Sign in, or check the connection.')
|
|
458
|
-
return
|
|
459
|
-
}
|
|
460
|
-
if (devices.length === 0) {
|
|
461
|
-
clack.note(
|
|
462
|
-
'No devices are registered yet.\n\n' +
|
|
463
|
-
'Install a supported Notifai Companion App, sign in with the same\n' +
|
|
464
|
-
'Account, and allow notifications when it asks.',
|
|
465
|
-
'No devices',
|
|
466
|
-
)
|
|
467
|
-
return
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const body = devices
|
|
471
|
-
.map((device) => {
|
|
472
|
-
const ready = canDeviceReceive(device)
|
|
473
|
-
const detail = ready ? 'ready' : `not ready ${glyph.bullet} permission: ${device.permission_status}`
|
|
474
|
-
return (
|
|
475
|
-
`${mark(ready ? 'ok' : 'warn')} ${style.value(device.display_name)} ${style.dim(`(${device.platform})`)}\n` +
|
|
476
|
-
` ${style.dim(detail)}\n` +
|
|
477
|
-
` ${style.dim(device.device_id)}`
|
|
478
|
-
)
|
|
479
|
-
})
|
|
480
|
-
.join('\n\n')
|
|
481
|
-
clack.note(body, `${devices.length} device${devices.length === 1 ? '' : 's'}`)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ---------------------------------------------------------------------------
|
|
485
|
-
// Settings
|
|
486
|
-
// ---------------------------------------------------------------------------
|
|
487
|
-
|
|
488
|
-
async function settingsScreen(deps: CommandDeps): Promise<'unchanged' | 'local' | 'remote'> {
|
|
489
|
-
let result: 'unchanged' | 'local' | 'remote' = 'unchanged'
|
|
490
|
-
for (;;) {
|
|
491
|
-
const config = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
492
|
-
const group = await clack.select<ConfigGroup | 'back'>({
|
|
493
|
-
message: 'Settings',
|
|
494
|
-
options: [
|
|
495
|
-
...CONFIG_GROUPS.map((entry) => ({
|
|
496
|
-
value: entry.id,
|
|
497
|
-
label: entry.title,
|
|
498
|
-
hint: changedCount(config, entry.id),
|
|
499
|
-
})),
|
|
500
|
-
{ value: 'back' as const, label: '← Back' },
|
|
501
|
-
],
|
|
502
|
-
})
|
|
503
|
-
if (cancelled(group) || group === 'back') return result
|
|
504
|
-
const next = await settingsGroupScreen(deps, group as ConfigGroup)
|
|
505
|
-
if (next === 'remote') result = 'remote'
|
|
506
|
-
else if (next === 'local' && result === 'unchanged') result = 'local'
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/** "3 changed" beats repeating the group blurb the reader already saw. */
|
|
511
|
-
function changedCount(config: CliConfig, group: ConfigGroup): string {
|
|
512
|
-
const keys = configKeysByGroup(group)
|
|
513
|
-
const changed = keys.filter((info) => config[info.key].source !== 'default').length
|
|
514
|
-
return changed === 0 ? style.dim('all default') : style.dim(`${changed} changed`)
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async function settingsGroupScreen(
|
|
518
|
-
deps: CommandDeps,
|
|
519
|
-
group: ConfigGroup,
|
|
520
|
-
): Promise<'unchanged' | 'local' | 'remote'> {
|
|
521
|
-
let result: 'unchanged' | 'local' | 'remote' = 'unchanged'
|
|
522
|
-
for (;;) {
|
|
523
|
-
const config = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
524
|
-
const info = CONFIG_GROUPS.find((entry) => entry.id === group)!
|
|
525
|
-
const keys = configKeysByGroup(group)
|
|
526
|
-
|
|
527
|
-
const key = await clack.select<ConfigKey | 'back'>({
|
|
528
|
-
message: info.title,
|
|
529
|
-
options: [
|
|
530
|
-
...keys.map((entry) => {
|
|
531
|
-
const resolved = config[entry.key]
|
|
532
|
-
const source = describeSource(resolved.source)
|
|
533
|
-
const value = formatValue(entry.key, resolved.value)
|
|
534
|
-
return {
|
|
535
|
-
value: entry.key,
|
|
536
|
-
label: entry.label,
|
|
537
|
-
hint: source.label === 'default' ? style.dim(value) : `${value} ${style.dim(`(${source.label})`)}`,
|
|
538
|
-
}
|
|
539
|
-
}),
|
|
540
|
-
{ value: 'back' as const, label: '← Back' },
|
|
541
|
-
],
|
|
542
|
-
})
|
|
543
|
-
if (cancelled(key) || key === 'back') return result
|
|
544
|
-
const next = await settingDetailScreen(deps, key as ConfigKey)
|
|
545
|
-
if (next === 'remote') result = 'remote'
|
|
546
|
-
else if (next === 'local' && result === 'unchanged') result = 'local'
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* One key, fully explained, with the option to change it.
|
|
552
|
-
*
|
|
553
|
-
* This is where the paragraph lives. Everything above stayed one line per key
|
|
554
|
-
* so that arriving here means the reader has already said which key they care
|
|
555
|
-
* about.
|
|
556
|
-
*/
|
|
557
|
-
async function settingDetailScreen(
|
|
558
|
-
deps: CommandDeps,
|
|
559
|
-
key: ConfigKey,
|
|
560
|
-
): Promise<'unchanged' | 'local' | 'remote'> {
|
|
561
|
-
const config = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
562
|
-
const info = configInfo(key)
|
|
563
|
-
const resolved = config[key]
|
|
564
|
-
const source = describeSource(resolved.source)
|
|
565
|
-
|
|
566
|
-
const lines = [
|
|
567
|
-
info.detail,
|
|
568
|
-
'',
|
|
569
|
-
`${style.dim('Now')} ${style.value(formatValue(key, resolved.value))} ${style.dim(`${glyph.bullet} ${source.label}`)}`,
|
|
570
|
-
`${style.dim('Accepts')} ${acceptedValues(key)}`,
|
|
571
|
-
`${style.dim('Key')} ${style.code(key)}`,
|
|
572
|
-
]
|
|
573
|
-
clack.note(lines.join('\n'), info.label)
|
|
574
|
-
|
|
575
|
-
const action = await clack.select<'change' | 'back'>({
|
|
576
|
-
message: 'Change it?',
|
|
577
|
-
options: [
|
|
578
|
-
// Not lower-cased: "Change only when i have stepped away" is what
|
|
579
|
-
// `toLowerCase` makes of a label that starts a sentence.
|
|
580
|
-
{ value: 'change', label: `Change this setting`, hint: style.code(info.key) },
|
|
581
|
-
{ value: 'back', label: '← Back' },
|
|
582
|
-
],
|
|
583
|
-
initialValue: 'back',
|
|
584
|
-
})
|
|
585
|
-
if (cancelled(action) || action === 'back') return 'unchanged'
|
|
586
|
-
|
|
587
|
-
const value = await promptForValue(deps, key)
|
|
588
|
-
if (value === null) return 'unchanged'
|
|
589
|
-
|
|
590
|
-
const layer = await chooseLayer()
|
|
591
|
-
if (layer === null) return 'unchanged'
|
|
592
|
-
|
|
593
|
-
const code = await configSetCommand(deps, key, value, {
|
|
594
|
-
yes: true,
|
|
595
|
-
...(layer === 'project' ? { project: true } : {}),
|
|
596
|
-
...(layer === 'local' ? { local: true } : {}),
|
|
597
|
-
})
|
|
598
|
-
if (code !== EXIT.ok) return 'unchanged'
|
|
599
|
-
clack.log.success(`${info.label} is now ${formatValueRaw(key, value)}`)
|
|
600
|
-
return 'local'
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function formatValueRaw(key: ConfigKey, raw: string): string {
|
|
604
|
-
const info = configInfo(key)
|
|
605
|
-
if (info.kind === 'boolean') return raw === 'true' ? 'yes' : 'no'
|
|
606
|
-
return raw
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/** The prompt shape follows the key's type; validation stays in `config set`. */
|
|
610
|
-
async function promptForValue(deps: CommandDeps, key: ConfigKey): Promise<string | null> {
|
|
611
|
-
const info = configInfo(key)
|
|
612
|
-
|
|
613
|
-
if (info.kind === 'boolean') {
|
|
614
|
-
const answer = await clack.select<string>({
|
|
615
|
-
message: info.label,
|
|
616
|
-
options: [
|
|
617
|
-
{ value: 'true', label: 'Yes' },
|
|
618
|
-
{ value: 'false', label: 'No' },
|
|
619
|
-
],
|
|
620
|
-
})
|
|
621
|
-
return cancelled(answer) ? null : String(answer)
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (info.kind === 'enum') {
|
|
625
|
-
const answer = await clack.select<string>({
|
|
626
|
-
message: info.label,
|
|
627
|
-
options: (info.choices ?? []).map((choice) => ({
|
|
628
|
-
value: choice,
|
|
629
|
-
label: choice,
|
|
630
|
-
...(info.choiceHints?.[choice] !== undefined ? { hint: info.choiceHints[choice]! } : {}),
|
|
631
|
-
})),
|
|
632
|
-
})
|
|
633
|
-
return cancelled(answer) ? null : String(answer)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// The one key whose legal values the machine knows better than the reader.
|
|
637
|
-
if (key === 'devices') return await promptForDevices(deps)
|
|
638
|
-
|
|
639
|
-
const bounds = boundsHint(key)
|
|
640
|
-
const answer = await clack.text({
|
|
641
|
-
message: info.label,
|
|
642
|
-
...(info.example !== undefined ? { placeholder: info.example } : {}),
|
|
643
|
-
validate: (input) => {
|
|
644
|
-
const trimmed = (input ?? '').trim()
|
|
645
|
-
if (trimmed === '') return 'Enter a value, or press Escape to go back.'
|
|
646
|
-
if (info.kind === 'integer') {
|
|
647
|
-
if (!/^-?\d+$/.test(trimmed)) return `${info.label} is a whole number of seconds.`
|
|
648
|
-
const range = boundsRange(key)
|
|
649
|
-
if (bounds !== null && range !== null) {
|
|
650
|
-
const parsed = Number(trimmed)
|
|
651
|
-
if (parsed < range.min || parsed > range.max) return `Must be between ${bounds}.`
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
if (info.kind === 'url' && !/^https?:\/\//.test(trimmed)) return 'Must start with http:// or https://.'
|
|
655
|
-
return undefined
|
|
656
|
-
},
|
|
657
|
-
})
|
|
658
|
-
return cancelled(answer) ? null : String(answer).trim()
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function boundsRange(key: ConfigKey): { min: number; max: number } | null {
|
|
662
|
-
const hint = boundsHint(key)
|
|
663
|
-
if (hint === null) return null
|
|
664
|
-
const [min, max] = hint.replace(/[^\d–]/g, '').split('–')
|
|
665
|
-
if (min === undefined || max === undefined) return null
|
|
666
|
-
return { min: Number(min), max: Number(max) }
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
async function promptForDevices(deps: CommandDeps): Promise<string | null> {
|
|
670
|
-
const spinner = clack.spinner()
|
|
671
|
-
spinner.start('Loading your devices')
|
|
672
|
-
const devices = await deviceInventory(deps)
|
|
673
|
-
spinner.stop(style.dim('Devices loaded'))
|
|
674
|
-
if (devices === null || devices.length === 0) {
|
|
675
|
-
clack.log.warn('No devices to choose from yet.')
|
|
676
|
-
return null
|
|
677
|
-
}
|
|
678
|
-
const picked = await clack.multiselect<string>({
|
|
679
|
-
message: 'Which devices should receive by default?',
|
|
680
|
-
options: devices.map((device) => ({
|
|
681
|
-
value: device.device_id,
|
|
682
|
-
label: device.display_name,
|
|
683
|
-
hint: canDeviceReceive(device) ? device.platform : `${device.platform} ${glyph.bullet} not ready`,
|
|
684
|
-
})),
|
|
685
|
-
required: true,
|
|
686
|
-
})
|
|
687
|
-
return cancelled(picked) ? null : (picked as string[]).join(',')
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Which layer the value lands in.
|
|
692
|
-
*
|
|
693
|
-
* Asked every time rather than defaulting, because the difference between
|
|
694
|
-
* "my machine" and "everyone who clones this repository" is not recoverable by
|
|
695
|
-
* guessing, and the shared file is the one that ends up in a commit.
|
|
696
|
-
*/
|
|
697
|
-
async function chooseLayer(): Promise<'global' | 'project' | 'local' | null> {
|
|
698
|
-
const layer = await clack.select<'global' | 'project' | 'local'>({
|
|
699
|
-
message: 'Where should this apply?',
|
|
700
|
-
options: [
|
|
701
|
-
{ value: 'global', label: 'This machine', hint: 'every project you work on here' },
|
|
702
|
-
{ value: 'project', label: 'This project, shared', hint: '.notifai/config.toml — commit it' },
|
|
703
|
-
{ value: 'local', label: 'This project, just me', hint: 'stored on this machine, not in the repo' },
|
|
704
|
-
],
|
|
705
|
-
initialValue: 'global',
|
|
706
|
-
})
|
|
707
|
-
return cancelled(layer) ? null : (layer as 'global' | 'project' | 'local')
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// ---------------------------------------------------------------------------
|
|
711
|
-
// Question routing
|
|
712
|
-
// ---------------------------------------------------------------------------
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Which harnesses are wired, without the install paths.
|
|
716
|
-
*
|
|
717
|
-
* The underlying state lists every installation with its absolute file path,
|
|
718
|
-
* which is the right level of detail for `doctor` and far too much for a
|
|
719
|
-
* summary card — four paths wrap across six lines and push the settings
|
|
720
|
-
* underneath them out of alignment.
|
|
721
|
-
*/
|
|
722
|
-
function wiringSummary(hooks: ReadinessState | undefined): string {
|
|
723
|
-
if (hooks === undefined) return 'unknown'
|
|
724
|
-
const named = hooks.detail.replace(/\s*\([^)]*\)/g, '').trim()
|
|
725
|
-
return named === '' ? hooks.detail : named
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/** Returns true when something changed and the status card needs refreshing. */
|
|
729
|
-
async function routingScreen(deps: CommandDeps, readiness: Readiness): Promise<boolean> {
|
|
730
|
-
const hooks = stateById(readiness, 'hooks')
|
|
731
|
-
const config = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
732
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome)
|
|
733
|
-
const projectInstallations = installations.filter((installation) => !installation.global)
|
|
734
|
-
const machineInstallations = installations.filter((installation) => installation.global)
|
|
735
|
-
|
|
736
|
-
const row = (label: string, value: string): string => `${style.dim(label.padEnd(16))}${value}`
|
|
737
|
-
clack.note(
|
|
738
|
-
'When an agent registers a question, it appears in your terminal first.\n' +
|
|
739
|
-
'After the grace below, it goes to your Companion devices as a\n' +
|
|
740
|
-
'notification with a supported native or in-app answer surface, and\n' +
|
|
741
|
-
'your answer comes back to the agent —\n' +
|
|
742
|
-
'whether or not you are still at this machine.\n\n' +
|
|
743
|
-
[
|
|
744
|
-
row('Wired to', wiringSummary(hooks)),
|
|
745
|
-
row('Send', config.ask_notifications.value ? 'yes' : 'no — questions stay in the terminal'),
|
|
746
|
-
row('Grace', formatValue('ask_grace_seconds', config.ask_grace_seconds.value)),
|
|
747
|
-
].join('\n'),
|
|
748
|
-
'Question routing',
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
const action = await clack.select<RoutingAction>({
|
|
752
|
-
message: 'Question routing',
|
|
753
|
-
options: routingHookActions({
|
|
754
|
-
canInstall: detectedHarnesses(deps.cwd, deps.env).length > 0,
|
|
755
|
-
projectInstallations,
|
|
756
|
-
machineInstallations,
|
|
757
|
-
hooksReady: hooks?.status === 'ready',
|
|
758
|
-
}),
|
|
759
|
-
})
|
|
760
|
-
if (cancelled(action) || action === 'back') return false
|
|
761
|
-
|
|
762
|
-
if (action === 'settings') {
|
|
763
|
-
await settingsGroupScreen(deps, 'questions')
|
|
764
|
-
return true
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (action === 'install') return await installHooksInteractively(deps)
|
|
768
|
-
return await uninstallHooksInteractively(deps, projectInstallations, machineInstallations)
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
async function installHooksInteractively(deps: CommandDeps): Promise<boolean> {
|
|
772
|
-
const scope = await clack.select<HookScope>({
|
|
773
|
-
message: 'Install hooks for',
|
|
774
|
-
options: [
|
|
775
|
-
{ value: 'project', label: 'This project', hint: 'hooks only in this directory' },
|
|
776
|
-
{ value: 'machine', label: 'This machine', hint: 'hooks for every project here' },
|
|
777
|
-
],
|
|
778
|
-
initialValue: 'project',
|
|
779
|
-
})
|
|
780
|
-
if (cancelled(scope)) return false
|
|
781
|
-
hooksInstallCommand(deps, scope === 'machine' ? { global: true } : {})
|
|
782
|
-
return true
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
async function uninstallHooksInteractively(
|
|
786
|
-
deps: CommandDeps,
|
|
787
|
-
projectInstallations: Installation[],
|
|
788
|
-
machineInstallations: Installation[],
|
|
789
|
-
): Promise<boolean> {
|
|
790
|
-
const scopes = uninstallScopeOptions(projectInstallations, machineInstallations)
|
|
791
|
-
if (scopes.length === 0) return false
|
|
792
|
-
|
|
793
|
-
let scope: HookScope
|
|
794
|
-
if (scopes.length === 1) {
|
|
795
|
-
scope = scopes[0]!.value
|
|
796
|
-
} else {
|
|
797
|
-
const picked = await clack.select<HookScope | 'back'>({
|
|
798
|
-
message: 'Uninstall hooks from',
|
|
799
|
-
options: [...scopes, { value: 'back', label: '← Back' }],
|
|
800
|
-
})
|
|
801
|
-
if (cancelled(picked) || picked === 'back') return false
|
|
802
|
-
scope = picked
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const targets = scope === 'machine' ? machineInstallations : projectInstallations
|
|
806
|
-
for (const installation of targets) {
|
|
807
|
-
hooksUninstallCommand(deps, {
|
|
808
|
-
harness: installation.harness,
|
|
809
|
-
global: installation.global,
|
|
810
|
-
})
|
|
811
|
-
}
|
|
812
|
-
return true
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// ---------------------------------------------------------------------------
|
|
816
|
-
// Account
|
|
817
|
-
// ---------------------------------------------------------------------------
|
|
818
|
-
|
|
819
|
-
/** Returns true when something changed and the status card needs refreshing. */
|
|
820
|
-
async function accountScreen(deps: CommandDeps, readiness: Readiness): Promise<boolean> {
|
|
821
|
-
const signedIn = stateById(readiness, 'credential')?.status === 'ready'
|
|
822
|
-
|
|
823
|
-
if (!signedIn) {
|
|
824
|
-
await initCommand(deps, {})
|
|
825
|
-
return true
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const auth = stateById(readiness, 'auth')
|
|
829
|
-
const credential = stateById(readiness, 'credential')
|
|
830
|
-
clack.note(
|
|
831
|
-
`${style.dim('Machine')} ${credential?.detail ?? 'unknown'}\n${style.dim('Account')} ${auth?.detail ?? 'unknown'}`,
|
|
832
|
-
'Account',
|
|
833
|
-
)
|
|
834
|
-
|
|
835
|
-
const action = await clack.select<'relogin' | 'logout' | 'back'>({
|
|
836
|
-
message: 'Account',
|
|
837
|
-
options: [
|
|
838
|
-
{ value: 'relogin', label: 'Pair this machine again' },
|
|
839
|
-
{ value: 'logout', label: 'Sign out', hint: 'removes the stored credential' },
|
|
840
|
-
{ value: 'back', label: '← Back' },
|
|
841
|
-
],
|
|
842
|
-
initialValue: 'back',
|
|
843
|
-
})
|
|
844
|
-
if (cancelled(action) || action === 'back') return false
|
|
845
|
-
|
|
846
|
-
if (action === 'logout') {
|
|
847
|
-
const sure = await clack.confirm({ message: 'Remove the stored credential?', initialValue: false })
|
|
848
|
-
if (cancelled(sure) || sure !== true) return false
|
|
849
|
-
logoutCommand(deps)
|
|
850
|
-
return true
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
await loginCommand(deps, {})
|
|
854
|
-
return true
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/** Exported for the settings command to reuse the same key vocabulary. */
|
|
858
|
-
export const INTERACTIVE_CONFIG_KEYS = CONFIG_KEYS
|