@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/ui/banner.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The wordmark shown when a human opens the interactive app.
|
|
3
|
-
*
|
|
4
|
-
* Three tiers, chosen by what the terminal can actually render rather than by
|
|
5
|
-
* what looks best in a screenshot: box-drawing blocks when the terminal is wide
|
|
6
|
-
* and Unicode-capable, a pure-ASCII form that survives anywhere, and a plain
|
|
7
|
-
* word when neither fits. A banner that wraps is worse than no banner, so the
|
|
8
|
-
* width check is not optional.
|
|
9
|
-
*
|
|
10
|
-
* The art is embedded rather than rendered at run time. It is one fixed word
|
|
11
|
-
* that will never change, and shipping a font engine to redraw a constant on
|
|
12
|
-
* every launch would cost every install — including the agents that never see
|
|
13
|
-
* it — for no benefit.
|
|
14
|
-
*/
|
|
15
|
-
import { gradientBlock, style, terminalWidth, unicode } from './theme.js'
|
|
16
|
-
|
|
17
|
-
/** ANSI Shadow. 50 columns, 6 rows. */
|
|
18
|
-
const BLOCK = [
|
|
19
|
-
'███╗ ██╗ ██████╗ ████████╗██╗███████╗ █████╗ ██╗',
|
|
20
|
-
'████╗ ██║██╔═══██╗╚══██╔══╝██║██╔════╝██╔══██╗██║',
|
|
21
|
-
'██╔██╗ ██║██║ ██║ ██║ ██║█████╗ ███████║██║',
|
|
22
|
-
'██║╚██╗██║██║ ██║ ██║ ██║██╔══╝ ██╔══██║██║',
|
|
23
|
-
'██║ ╚████║╚██████╔╝ ██║ ██║██║ ██║ ██║██║',
|
|
24
|
-
'╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝',
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
/** Pure ASCII. 34 columns, 4 rows — safe on any terminal, any code page. */
|
|
28
|
-
const ASCII = [
|
|
29
|
-
' _ _ ___ _____ ___ ___ _ ___ ',
|
|
30
|
-
' | \\| |/ _ \\_ _|_ _| __/_\\ |_ _|',
|
|
31
|
-
' | .` | (_) || | | || _/ _ \\ | | ',
|
|
32
|
-
' |_|\\_|\\___/ |_| |___|_/_/ \\_\\___|',
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
export type BannerSize = 'block' | 'ascii' | 'word'
|
|
36
|
-
|
|
37
|
-
export function bannerSize(columns = terminalWidth()): BannerSize {
|
|
38
|
-
if (unicode && columns >= 54) return 'block'
|
|
39
|
-
if (columns >= 38) return 'ascii'
|
|
40
|
-
return 'word'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** The wordmark as rendered lines, gradient included. */
|
|
44
|
-
export function bannerLines(columns = terminalWidth()): string[] {
|
|
45
|
-
switch (bannerSize(columns)) {
|
|
46
|
-
case 'block':
|
|
47
|
-
return gradientBlock(BLOCK)
|
|
48
|
-
case 'ascii':
|
|
49
|
-
return gradientBlock(ASCII)
|
|
50
|
-
case 'word':
|
|
51
|
-
return gradientBlock(['N O T I F A I'])
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Reveal the wordmark one row at a time.
|
|
57
|
-
*
|
|
58
|
-
* Deliberately brief and deliberately skippable: this runs before someone can
|
|
59
|
-
* do anything, so it may not become a wait. It is skipped entirely when output
|
|
60
|
-
* is not a TTY, when `NOTIFAI_NO_ANIMATION` is set, and under `CI`.
|
|
61
|
-
*/
|
|
62
|
-
export async function printBanner(
|
|
63
|
-
options: { animate?: boolean; env?: NodeJS.ProcessEnv; write?: (line: string) => void } = {},
|
|
64
|
-
): Promise<void> {
|
|
65
|
-
const env = options.env ?? process.env
|
|
66
|
-
const write = options.write ?? ((line: string) => process.stdout.write(`${line}\n`))
|
|
67
|
-
const lines = bannerLines()
|
|
68
|
-
const animate =
|
|
69
|
-
options.animate !== false &&
|
|
70
|
-
process.stdout.isTTY === true &&
|
|
71
|
-
(env['NOTIFAI_NO_ANIMATION'] ?? '') === '' &&
|
|
72
|
-
(env['CI'] ?? '') === ''
|
|
73
|
-
|
|
74
|
-
write('')
|
|
75
|
-
for (const line of lines) {
|
|
76
|
-
// Two columns, matching the tagline and the status card beneath it. Flush
|
|
77
|
-
// left, the wordmark reads as a separate thing sitting above the app.
|
|
78
|
-
write(` ${line}`)
|
|
79
|
-
if (animate) await new Promise((resolve) => setTimeout(resolve, 26))
|
|
80
|
-
}
|
|
81
|
-
write('')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* One line of orientation under the wordmark — and it must stay one line.
|
|
86
|
-
* Wrapped onto two it stops reading as a subtitle and starts reading as the
|
|
87
|
-
* first paragraph of something, so a narrow terminal gets the version alone.
|
|
88
|
-
*/
|
|
89
|
-
export function tagline(version: string, columns = terminalWidth()): string {
|
|
90
|
-
const full = 'Native notifications for agents and local programs'
|
|
91
|
-
// Two leading columns of indent, two trailing for the version.
|
|
92
|
-
return columns >= full.length + 12
|
|
93
|
-
? `${style.dim(full)} ${style.dim(`v${version}`)}`
|
|
94
|
-
: style.dim(`v${version}`)
|
|
95
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { CONFIG_KEYS, loadConfig } from '../config.js'
|
|
5
|
-
import { configInfo } from '../config-schema.js'
|
|
6
|
-
import { renderConfigExplain, renderConfigList, renderConfigPlain } from './config-view.js'
|
|
7
|
-
import { width } from './theme.js'
|
|
8
|
-
|
|
9
|
-
const config = loadConfig({
|
|
10
|
-
cwd: os.tmpdir(),
|
|
11
|
-
env: { XDG_CONFIG_HOME: path.join(os.tmpdir(), 'notifai-config-view-tests') },
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe('renderConfigPlain', () => {
|
|
15
|
-
it('is the flat form scripts already parse', () => {
|
|
16
|
-
// This is the agent-facing shape and the reason the pretty renderer is
|
|
17
|
-
// gated on a human terminal. Restyling it would be a breaking change for
|
|
18
|
-
// an audience that cannot see the styling.
|
|
19
|
-
const lines = renderConfigPlain(config, false)
|
|
20
|
-
expect(lines).toHaveLength(CONFIG_KEYS.length)
|
|
21
|
-
expect(lines[0]).toBe(`wait_seconds = ${JSON.stringify(config.wait_seconds.value)}`)
|
|
22
|
-
for (const line of lines) expect(line).toMatch(/^[a-z_]+ = /)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('appends the raw source tag under --explain', () => {
|
|
26
|
-
const lines = renderConfigPlain(config, true)
|
|
27
|
-
expect(lines[0]).toBe(
|
|
28
|
-
`wait_seconds = ${JSON.stringify(config.wait_seconds.value)} [${config.wait_seconds.source}]`,
|
|
29
|
-
)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('carries no colour, so a pipe gets no escape bytes', () => {
|
|
33
|
-
for (const line of renderConfigPlain(config, true)) {
|
|
34
|
-
expect(line).toBe(line.replace(/\[[0-9;]*m/g, ''))
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('renderConfigList', () => {
|
|
40
|
-
it('names every non-advanced key and explains it', () => {
|
|
41
|
-
const text = renderConfigList(config).join('\n')
|
|
42
|
-
for (const key of CONFIG_KEYS) {
|
|
43
|
-
// Derived, not listed: a hardcoded set of advanced keys silently becomes
|
|
44
|
-
// an assertion about yesterday's key list the moment a key is added.
|
|
45
|
-
if (configInfo(key).advanced === true) continue
|
|
46
|
-
expect(text, key).toContain(key)
|
|
47
|
-
}
|
|
48
|
-
expect(text).toContain('Questions')
|
|
49
|
-
expect(text).toContain('notifai config explain')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('reveals advanced keys only when asked', () => {
|
|
53
|
-
expect(renderConfigList(config).join('\n')).not.toContain('collapse_key')
|
|
54
|
-
expect(renderConfigList(config, { showAdvanced: true }).join('\n')).toContain('collapse_key')
|
|
55
|
-
expect(renderConfigList(config, { showAdvanced: true }).join('\n')).not.toContain('base_url')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('fits the terminal, so no row wraps into the next', () => {
|
|
59
|
-
const columns = process.stdout.columns
|
|
60
|
-
Object.defineProperty(process.stdout, 'columns', { value: 80, configurable: true })
|
|
61
|
-
try {
|
|
62
|
-
for (const line of renderConfigList(config, { showAdvanced: true })) {
|
|
63
|
-
expect(width(line), line).toBeLessThanOrEqual(80)
|
|
64
|
-
}
|
|
65
|
-
} finally {
|
|
66
|
-
Object.defineProperty(process.stdout, 'columns', { value: columns, configurable: true })
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('renderConfigExplain', () => {
|
|
72
|
-
it('answers what it is, what it is now, and what it accepts', () => {
|
|
73
|
-
const text = renderConfigExplain('ask_notifications', config).join('\n')
|
|
74
|
-
expect(text).toContain('ask_notifications')
|
|
75
|
-
expect(text).toContain('Send questions to my devices')
|
|
76
|
-
expect(text).toContain('Accepts')
|
|
77
|
-
expect(text).toContain('true or false')
|
|
78
|
-
// The copyable next step; the old output stopped at the value.
|
|
79
|
-
expect(text).toContain('notifai config set ask_notifications')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('lists what each choice of an enum means', () => {
|
|
83
|
-
const text = renderConfigExplain('sound', config).join('\n')
|
|
84
|
-
for (const choice of ['default', 'done', 'attention', 'alert', 'none']) {
|
|
85
|
-
expect(text, choice).toContain(choice)
|
|
86
|
-
}
|
|
87
|
-
expect(text).toContain('the completion chime')
|
|
88
|
-
expect(text).toContain('Device default')
|
|
89
|
-
})
|
|
90
|
-
})
|
package/src/ui/config-view.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rendering for the configuration surfaces a person reads.
|
|
3
|
-
*
|
|
4
|
-
* The agent-facing shapes are untouched and live elsewhere: `--json` is the
|
|
5
|
-
* contract, and the flat `key = value` form stays exactly as it was for
|
|
6
|
-
* anything that is not a terminal. What is new is only what a human sees.
|
|
7
|
-
*/
|
|
8
|
-
import { CONFIG_KEYS, type CliConfig, type ConfigKey } from '../config.js'
|
|
9
|
-
import {
|
|
10
|
-
CONFIG_GROUPS,
|
|
11
|
-
acceptedValues,
|
|
12
|
-
configInfo,
|
|
13
|
-
configKeysByGroup,
|
|
14
|
-
describeSource,
|
|
15
|
-
formatValue,
|
|
16
|
-
} from '../config-schema.js'
|
|
17
|
-
import { glyph, pad, style, terminalWidth, width, wrap } from './theme.js'
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The full settings listing, grouped and explained.
|
|
21
|
-
*
|
|
22
|
-
* Each key gets its name (what you type), its value (what is in force), where
|
|
23
|
-
* that value came from, and one line on what it does. The name leads rather
|
|
24
|
-
* than the friendly label because the next thing a reader does with it is type
|
|
25
|
-
* it into `config set`.
|
|
26
|
-
*/
|
|
27
|
-
export function renderConfigList(
|
|
28
|
-
config: CliConfig,
|
|
29
|
-
options: { showAdvanced?: boolean } = {},
|
|
30
|
-
): string[] {
|
|
31
|
-
const columns = terminalWidth()
|
|
32
|
-
const lines: string[] = []
|
|
33
|
-
const nameWidth = Math.max(...CONFIG_KEYS.map((key) => key.length))
|
|
34
|
-
const SUMMARY_INDENT = ' '
|
|
35
|
-
|
|
36
|
-
for (const group of CONFIG_GROUPS) {
|
|
37
|
-
const keys = configKeysByGroup(group.id).filter(
|
|
38
|
-
(info) => options.showAdvanced === true || info.advanced !== true,
|
|
39
|
-
)
|
|
40
|
-
if (keys.length === 0) continue
|
|
41
|
-
|
|
42
|
-
lines.push('')
|
|
43
|
-
lines.push(style.heading(group.title))
|
|
44
|
-
for (const line of wrap(group.blurb, columns - 2)) lines.push(style.dim(` ${line}`))
|
|
45
|
-
lines.push('')
|
|
46
|
-
|
|
47
|
-
for (const info of keys) {
|
|
48
|
-
const entry = config[info.key]
|
|
49
|
-
const source = describeSource(entry.source)
|
|
50
|
-
const value = formatValue(info.key, entry.value)
|
|
51
|
-
const isSet = source.label !== 'default'
|
|
52
|
-
// The provenance mark is only drawn when there is provenance to show;
|
|
53
|
-
// an empty dim segment still emits escape codes, so a `trimEnd` on the
|
|
54
|
-
// finished string cannot remove it.
|
|
55
|
-
const provenance = isSet ? ` ${style.dim(`${glyph.bullet} ${source.label}`)}` : ''
|
|
56
|
-
lines.push(
|
|
57
|
-
` ${style.code(pad(info.key, nameWidth))} ` +
|
|
58
|
-
`${isSet ? style.value(value) : style.unset(value)}${provenance}`,
|
|
59
|
-
)
|
|
60
|
-
for (const line of wrap(info.summary, columns - SUMMARY_INDENT.length)) {
|
|
61
|
-
lines.push(style.dim(`${SUMMARY_INDENT}${line}`))
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
lines.push('')
|
|
67
|
-
const command = 'notifai config explain <key>'
|
|
68
|
-
const help = `${glyph.arrow} ${command} for what one of these means and how to change it.`
|
|
69
|
-
for (const line of wrap(help, columns)) {
|
|
70
|
-
lines.push(style.dim(line.replace(command, style.code(command))))
|
|
71
|
-
}
|
|
72
|
-
return lines
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Everything about one key, for someone who has asked about it specifically.
|
|
77
|
-
*
|
|
78
|
-
* This is the payoff of the listing above staying terse: the reader arrives
|
|
79
|
-
* here already knowing which key they care about, so the full paragraph, the
|
|
80
|
-
* legal values, and the layer the current value came from can all be spent on
|
|
81
|
-
* that one key.
|
|
82
|
-
*/
|
|
83
|
-
export function renderConfigExplain(key: ConfigKey, config: CliConfig): string[] {
|
|
84
|
-
const info = configInfo(key)
|
|
85
|
-
const entry = config[key]
|
|
86
|
-
const source = describeSource(entry.source)
|
|
87
|
-
const columns = terminalWidth()
|
|
88
|
-
const lines: string[] = []
|
|
89
|
-
|
|
90
|
-
lines.push('')
|
|
91
|
-
lines.push(`${style.code(key)} ${style.dim(glyph.bullet)} ${style.heading(info.label)}`)
|
|
92
|
-
lines.push('')
|
|
93
|
-
|
|
94
|
-
for (const paragraph of info.detail.split('\n\n')) {
|
|
95
|
-
for (const line of wrap(paragraph, columns - 2)) lines.push(` ${line}`)
|
|
96
|
-
lines.push('')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const valueLabel = formatValue(key, entry.value)
|
|
100
|
-
lines.push(
|
|
101
|
-
` ${style.dim(pad('Now', 10))}${style.value(valueLabel)} ${style.dim(`${glyph.bullet} ${source.label}`)}`,
|
|
102
|
-
)
|
|
103
|
-
if (source.path !== null) lines.push(` ${style.dim(pad('', 10))}${style.dim(source.path)}`)
|
|
104
|
-
lines.push(` ${style.dim(pad('Accepts', 10))}${acceptedValues(key)}`)
|
|
105
|
-
|
|
106
|
-
if (info.choices !== undefined && info.choiceHints !== undefined) {
|
|
107
|
-
lines.push('')
|
|
108
|
-
for (const choice of info.choices) {
|
|
109
|
-
const hint = info.choiceHints[choice]
|
|
110
|
-
lines.push(
|
|
111
|
-
` ${style.dim(pad('', 10))}${style.code(pad(choice, 14))}${style.dim(hint ?? '')}`.trimEnd(),
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
lines.push('')
|
|
117
|
-
const example = info.example ?? (info.choices?.[0] ?? 'value')
|
|
118
|
-
lines.push(` ${style.dim('Change it')} ${style.code(`notifai config set ${key} ${example}`)}`)
|
|
119
|
-
if (entry.source !== 'default' && entry.source !== 'flag') {
|
|
120
|
-
lines.push(` ${style.dim('Inherit it')} ${style.code(`notifai config unset ${key}`)}`)
|
|
121
|
-
}
|
|
122
|
-
lines.push('')
|
|
123
|
-
return lines
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** The flat form, unchanged, for anything that is not a person at a terminal. */
|
|
127
|
-
export function renderConfigPlain(config: CliConfig, explain: boolean): string[] {
|
|
128
|
-
return CONFIG_KEYS.map((key) => {
|
|
129
|
-
const entry = config[key]
|
|
130
|
-
const provenance = explain ? ` [${entry.source}]` : ''
|
|
131
|
-
return `${key} = ${JSON.stringify(entry.value)}${provenance}`
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Longest visible line, for callers that need to size a rule or a box. */
|
|
136
|
-
export function widestLine(lines: readonly string[]): number {
|
|
137
|
-
return Math.max(0, ...lines.map((line) => width(line)))
|
|
138
|
-
}
|
package/src/ui/help.test.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { rootHelpFooter } from './help.js'
|
|
3
|
-
|
|
4
|
-
function stripAnsi(text: string): string {
|
|
5
|
-
return text.replace(/\u001B\[[0-9;]*m/g, '')
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
describe('rootHelpFooter', () => {
|
|
9
|
-
it('leads first-run with init, not the interactive app', () => {
|
|
10
|
-
const footer = stripAnsi(rootHelpFooter())
|
|
11
|
-
const start = footer.slice(footer.indexOf('Start here'))
|
|
12
|
-
const firstCommand = start.match(/notifai(?: init)?/)?.[0]
|
|
13
|
-
expect(firstCommand).toBe('notifai init')
|
|
14
|
-
expect(start.indexOf('notifai init')).toBeLessThan(start.search(/notifai\s{2,}/))
|
|
15
|
-
})
|
|
16
|
-
})
|
package/src/ui/help.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Help that answers "what can I do here", not "what strings does argv accept".
|
|
3
|
-
*
|
|
4
|
-
* The old help listed seventeen commands in one flat column, with the one a new
|
|
5
|
-
* user needs (`init`) sitting between a platform capability dump and
|
|
6
|
-
* an entry point marked "Internal:". Everything was equally prominent, which is
|
|
7
|
-
* the same as nothing being prominent.
|
|
8
|
-
*
|
|
9
|
-
* Grouping and colour are Commander's own (`commandsGroup`, `optionsGroup`, and
|
|
10
|
-
* the `styleX` hooks it calls when the output has colours); this module only
|
|
11
|
-
* supplies the palette and the group vocabulary. Commander turns its styling
|
|
12
|
-
* off by itself when the output is not a terminal, so piped help stays plain
|
|
13
|
-
* without a second code path.
|
|
14
|
-
*/
|
|
15
|
-
import { Help, type Command, type HelpConfiguration, type Option } from 'commander'
|
|
16
|
-
import { glyph, style } from './theme.js'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Group headings, in the order a reader meets them.
|
|
20
|
-
*
|
|
21
|
-
* Ordering is by who needs it and when: first run, then daily use, then the
|
|
22
|
-
* commands agents drive, then the things touched once or never. Commander
|
|
23
|
-
* renders groups in first-use order, so these constants are also the layout.
|
|
24
|
-
*/
|
|
25
|
-
export const GROUP = {
|
|
26
|
-
start: 'Getting started:',
|
|
27
|
-
daily: 'Everyday:',
|
|
28
|
-
agent: 'For agents and scripts:',
|
|
29
|
-
advanced: 'Account and wiring:',
|
|
30
|
-
} as const
|
|
31
|
-
|
|
32
|
-
/** Option groups for `send`, which carries thirty flags. */
|
|
33
|
-
export const SEND_GROUP = {
|
|
34
|
-
content: 'What it says:',
|
|
35
|
-
routing: 'Where it goes:',
|
|
36
|
-
presentation: 'How it arrives:',
|
|
37
|
-
reply: 'Asking for an answer:',
|
|
38
|
-
advanced: 'Advanced:',
|
|
39
|
-
} as const
|
|
40
|
-
|
|
41
|
-
/** Group order, independent of the order commands happen to be registered in. */
|
|
42
|
-
const GROUP_ORDER: readonly string[] = [
|
|
43
|
-
GROUP.start,
|
|
44
|
-
GROUP.daily,
|
|
45
|
-
GROUP.agent,
|
|
46
|
-
GROUP.advanced,
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
function groupRank(heading: string): number {
|
|
50
|
-
const index = GROUP_ORDER.indexOf(heading)
|
|
51
|
-
// Anything not named above keeps its position relative to its peers, because
|
|
52
|
-
// the sort is stable — which is what lets the same override run over option
|
|
53
|
-
// groups without needing to know their headings.
|
|
54
|
-
return index === -1 ? GROUP_ORDER.length : index
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const helpConfiguration: HelpConfiguration = {
|
|
58
|
-
// Groups already carry the intended order; alphabetising would scatter the
|
|
59
|
-
// dependency between `login` and everything that needs a credential.
|
|
60
|
-
sortSubcommands: false,
|
|
61
|
-
sortOptions: false,
|
|
62
|
-
showGlobalOptions: false,
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Commander seeds group order from the raw registration order, which ties
|
|
66
|
-
* how the help reads to where a `program.command(…)` block happens to sit in
|
|
67
|
-
* the file — move one and the help silently reorders. Sorting the finished
|
|
68
|
-
* grouping makes `GROUP_ORDER` the single place that decision is made.
|
|
69
|
-
*
|
|
70
|
-
* The same override runs over option groups, whose headings are not in
|
|
71
|
-
* `GROUP_ORDER` at all. They all rank equal, and `sort` is stable, so they
|
|
72
|
-
* keep the order they were declared in.
|
|
73
|
-
*/
|
|
74
|
-
groupItems<T extends Command | Option>(
|
|
75
|
-
this: Help,
|
|
76
|
-
unsortedItems: T[],
|
|
77
|
-
visibleItems: T[],
|
|
78
|
-
getGroup: (item: T) => string,
|
|
79
|
-
): Map<string, T[]> {
|
|
80
|
-
// `.call` cannot carry the method's own generic through, so the signature
|
|
81
|
-
// is restated once here instead of widening every caller to
|
|
82
|
-
// `Command | Option`.
|
|
83
|
-
const base = Help.prototype.groupItems as unknown as (
|
|
84
|
-
this: Help,
|
|
85
|
-
unsorted: T[],
|
|
86
|
-
visible: T[],
|
|
87
|
-
group: (item: T) => string,
|
|
88
|
-
) => Map<string, T[]>
|
|
89
|
-
const grouped = base.call(this, unsortedItems, visibleItems, getGroup)
|
|
90
|
-
return new Map([...grouped.entries()].sort((a, b) => groupRank(a[0]) - groupRank(b[0])))
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
styleTitle: (title: string): string => style.heading(title),
|
|
94
|
-
styleCommandText: (text: string): string => style.accent(text),
|
|
95
|
-
styleSubcommandTerm: (text: string): string => style.code(text),
|
|
96
|
-
styleOptionTerm: (text: string): string => style.code(text),
|
|
97
|
-
styleArgumentTerm: (text: string): string => style.code(text),
|
|
98
|
-
styleDescriptionText: (text: string): string => text,
|
|
99
|
-
styleUsage: (text: string): string => text,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Footer for the root help.
|
|
104
|
-
*
|
|
105
|
-
* First-run is `notifai init`. The interactive app is what a human opens
|
|
106
|
-
* later, once setup is no longer the thing in the way.
|
|
107
|
-
*/
|
|
108
|
-
export function rootHelpFooter(): string {
|
|
109
|
-
const lines = [
|
|
110
|
-
'',
|
|
111
|
-
style.heading('Start here'),
|
|
112
|
-
` ${style.code('notifai init')} ${style.dim(`${glyph.arrow} set this project up, step by step`)}`,
|
|
113
|
-
` ${style.code('notifai')} ${style.dim(
|
|
114
|
-
`${glyph.arrow} after setup: status, settings, devices, test sends`,
|
|
115
|
-
)}`,
|
|
116
|
-
'',
|
|
117
|
-
style.heading('First call, for an agent'),
|
|
118
|
-
` ${style.code('notifai send --kind done --title "All 42 tests passed" --body "Finished in 3m 10s. Next: review the release candidate."')}`,
|
|
119
|
-
` ${style.code('notifai ask "Deploy migration 0007 to production?" --choice Deploy --choice Hold')}`,
|
|
120
|
-
` ${style.dim(`${glyph.arrow} ${style.code('ask')} needs question routing installed; ${style.code('notifai doctor --json')} says whether it is.`)}`,
|
|
121
|
-
'',
|
|
122
|
-
style.dim(
|
|
123
|
-
`Every command takes ${style.code('--help')}. Where a command has machine-readable output it takes ${style.code('--json')}.`,
|
|
124
|
-
),
|
|
125
|
-
style.dim(`Settings are explained by ${style.code('notifai config show')} and ${style.code('notifai config explain <key>')}.`),
|
|
126
|
-
]
|
|
127
|
-
return lines.join('\n')
|
|
128
|
-
}
|
package/src/ui/theme.test.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { card, clip, pad, width, wrap } from './theme.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* These run with colour off — vitest's stdout is not a TTY, so `picocolors`
|
|
6
|
-
* resolves to identity functions. That is the interesting case: it is the one
|
|
7
|
-
* every agent, pipe and CI log gets, and the one where a layout bug is
|
|
8
|
-
* invisible to a human developer reading a colourful terminal.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const RED = '[31m'
|
|
12
|
-
const RESET = '[39m'
|
|
13
|
-
|
|
14
|
-
describe('width', () => {
|
|
15
|
-
it('counts printable characters, not escape bytes', () => {
|
|
16
|
-
expect(width('hello')).toBe(5)
|
|
17
|
-
expect(width(`${RED}hello${RESET}`)).toBe(5)
|
|
18
|
-
})
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('pad', () => {
|
|
22
|
-
it('pads by visible width so colour cannot break a column', () => {
|
|
23
|
-
expect(width(pad(`${RED}ab${RESET}`, 6))).toBe(6)
|
|
24
|
-
expect(pad('ab', 4)).toBe('ab ')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('never truncates when the text already exceeds the column', () => {
|
|
28
|
-
expect(pad('abcdef', 3)).toBe('abcdef')
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
describe('wrap', () => {
|
|
33
|
-
it('breaks on whitespace within the limit', () => {
|
|
34
|
-
expect(wrap('one two three four', 9)).toEqual(['one two', 'three', 'four'])
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('returns bare lines, leaving indentation to the caller', () => {
|
|
38
|
-
// The regression this guards: wrap used to indent continuation lines
|
|
39
|
-
// itself while callers indented every line, so each wrapped paragraph
|
|
40
|
-
// stepped further right down the page.
|
|
41
|
-
for (const line of wrap('alpha beta gamma delta', 11)) {
|
|
42
|
-
expect(line.startsWith(' ')).toBe(false)
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('keeps a word that is longer than the limit rather than losing it', () => {
|
|
47
|
-
expect(wrap('supercalifragilistic', 5)).toEqual(['supercalifragilistic'])
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('clip', () => {
|
|
52
|
-
it('leaves text that already fits', () => {
|
|
53
|
-
expect(clip('short', 10)).toBe('short')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('truncates by visible width and never mid-escape', () => {
|
|
57
|
-
const clipped = clip(`${RED}abcdefghij${RESET}`, 5)
|
|
58
|
-
expect(width(clipped)).toBeLessThanOrEqual(5)
|
|
59
|
-
// A slice by String.length would cut through `[31m` and spill the
|
|
60
|
-
// raw bytes into the terminal.
|
|
61
|
-
expect(clipped).toContain(RED)
|
|
62
|
-
expect(clipped.endsWith(RESET)).toBe(true)
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe('card', () => {
|
|
67
|
-
it('draws a closed box whose rows all share one width', () => {
|
|
68
|
-
const lines = card('Status', ['a', 'bbbbbbbbbb'], 40)
|
|
69
|
-
const widths = new Set(lines.map((line) => width(line)))
|
|
70
|
-
expect(widths.size).toBe(1)
|
|
71
|
-
expect(lines[0]).toContain('Status')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('measures rows by visible width, so colour does not skew the border', () => {
|
|
75
|
-
const plain = card('T', ['abcdef'], 40)
|
|
76
|
-
const coloured = card('T', [`${RED}abcdef${RESET}`], 40)
|
|
77
|
-
expect(plain.map(width)).toEqual(coloured.map(width))
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('survives a terminal that reports no usable width', () => {
|
|
81
|
-
// A pty reporting zero columns made the obvious library call compute a
|
|
82
|
-
// negative pad and throw, taking the whole interactive app with it. A
|
|
83
|
-
// status card may degrade; it must never be the thing that crashes.
|
|
84
|
-
expect(() => card('Status', ['something reasonably long here'], 0)).not.toThrow()
|
|
85
|
-
expect(card('Status', ['x'], 0).length).toBe(3)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('clips a row too wide for the terminal instead of wrapping it', () => {
|
|
89
|
-
const lines = card('T', ['x'.repeat(200)], 40)
|
|
90
|
-
for (const line of lines) expect(width(line)).toBeLessThanOrEqual(40)
|
|
91
|
-
})
|
|
92
|
-
})
|