@raidiant/notifai 7.0.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/README.md +20 -13
- package/dist/atomic-file.d.ts +14 -0
- package/dist/atomic-file.js +27 -2
- package/dist/atomic-file.js.map +1 -1
- package/dist/cli-bin.d.ts +4 -0
- package/dist/cli-bin.js +62 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli-release.d.ts +10 -0
- package/dist/cli-release.js +75 -0
- package/dist/cli-release.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +13 -3
- package/dist/client.js.map +1 -1
- package/dist/commands-auth.d.ts +6 -0
- package/dist/commands-auth.js +28 -3
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +22 -3
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +12 -0
- package/dist/commands-core.js +27 -3
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +2 -1
- package/dist/commands-devices.js +20 -8
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +85 -27
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +27 -0
- package/dist/commands-guidance.js +163 -0
- package/dist/commands-guidance.js.map +1 -0
- package/dist/commands-hooks.d.ts +16 -5
- package/dist/commands-hooks.js +433 -38
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +10 -18
- package/dist/commands-init.js +139 -54
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +66 -8
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +0 -1
- package/dist/commands-send.js +105 -39
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +8 -0
- package/dist/commands-setup-proof.js +31 -2
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands-skill.d.ts +11 -1
- package/dist/commands-skill.js +61 -11
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +1 -0
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +30 -14
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -5
- package/dist/config.js +37 -8
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +10 -1
- package/dist/credentials.js +25 -12
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +50 -0
- package/dist/guidance-content.js +240 -0
- package/dist/guidance-content.js.map +1 -0
- package/dist/guidance.d.ts +69 -0
- package/dist/guidance.js +173 -0
- package/dist/guidance.js.map +1 -0
- package/dist/hook-adapter.js +37 -3
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +68 -7
- package/dist/hooks.js +238 -92
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -18
- package/dist/install-hooks.js +110 -23
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +14 -9
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +8 -1
- package/dist/logging.js +43 -2
- package/dist/logging.js.map +1 -1
- package/dist/main.js +5 -487
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +19 -0
- package/dist/native-skills.js +30 -8
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.js +14 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.d.ts +49 -0
- package/dist/program.js +576 -0
- package/dist/program.js.map +1 -0
- package/dist/send.d.ts +12 -10
- package/dist/send.js +65 -29
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +15 -0
- package/dist/session-activation.js +35 -0
- package/dist/session-activation.js.map +1 -0
- package/dist/session-labels.d.ts +9 -3
- package/dist/session-labels.js +38 -7
- package/dist/session-labels.js.map +1 -1
- package/dist/url-policy.d.ts +85 -0
- package/dist/url-policy.js +369 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +6 -4
- package/src/atomic-file.ts +36 -2
- package/src/cli-bin.test.ts +38 -0
- package/src/cli-bin.ts +67 -0
- package/src/cli-release.test.ts +46 -0
- package/src/cli-release.ts +78 -0
- package/src/client.test.ts +95 -0
- package/src/client.ts +16 -3
- package/src/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-devices.ts +28 -9
- package/src/commands-doctor.ts +116 -25
- package/src/commands-guidance.ts +216 -0
- package/src/commands-hooks.ts +513 -42
- package/src/commands-init.ts +170 -60
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +72 -8
- package/src/commands-send.ts +123 -38
- package/src/commands-setup-proof.ts +36 -2
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1490 -197
- package/src/commands.ts +6 -0
- package/src/config-schema.ts +32 -16
- package/src/config.test.ts +57 -2
- package/src/config.ts +49 -10
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +256 -0
- package/src/guidance.test.ts +299 -0
- package/src/guidance.ts +233 -0
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +783 -170
- package/src/hooks.ts +306 -131
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +14 -9
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +6 -614
- package/src/native-skills.test.ts +41 -1
- package/src/native-skills.ts +40 -6
- package/src/opencode-plugin.ts +14 -1
- package/src/program.test.ts +237 -0
- package/src/program.ts +751 -0
- package/src/send.test.ts +28 -0
- package/src/send.ts +81 -34
- package/src/session-activation.ts +45 -0
- package/src/session-labels.test.ts +120 -3
- package/src/session-labels.ts +38 -7
- package/src/skill.test.ts +153 -12
- package/src/trust-policy.test.ts +701 -0
- package/src/url-policy.ts +399 -0
|
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { nativeSkills } from './native-skills.js'
|
|
5
|
+
import { SKILLS_INSTALLER_SPEC, nativeSkills, skillsAddArgv, skillsRemoveArgv } from './native-skills.js'
|
|
6
6
|
|
|
7
7
|
function writeLock(file: string, ref: string): void {
|
|
8
8
|
mkdirSync(path.dirname(file), { recursive: true })
|
|
@@ -22,6 +22,46 @@ function writeLock(file: string, ref: string): void {
|
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
describe('skillsAddArgv', () => {
|
|
26
|
+
it('pins the installer package to an exact reviewed version', () => {
|
|
27
|
+
const argv = skillsAddArgv({
|
|
28
|
+
source: 'Raidiant-io/notifai#v8.0.0',
|
|
29
|
+
skill: 'notifai',
|
|
30
|
+
cwd: '/tmp',
|
|
31
|
+
env: {},
|
|
32
|
+
})
|
|
33
|
+
expect(SKILLS_INSTALLER_SPEC).toMatch(/^skills@\d+\.\d+\.\d+$/)
|
|
34
|
+
expect(argv).toEqual([
|
|
35
|
+
'-y',
|
|
36
|
+
SKILLS_INSTALLER_SPEC,
|
|
37
|
+
'add',
|
|
38
|
+
'Raidiant-io/notifai#v8.0.0',
|
|
39
|
+
'--skill',
|
|
40
|
+
'notifai',
|
|
41
|
+
])
|
|
42
|
+
expect(argv).not.toContain('skills')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('uninstalls one skill in the named scope without a prompt', () => {
|
|
46
|
+
expect(
|
|
47
|
+
skillsRemoveArgv({
|
|
48
|
+
skill: 'notifai',
|
|
49
|
+
scope: 'project',
|
|
50
|
+
cwd: '/tmp',
|
|
51
|
+
env: {},
|
|
52
|
+
}),
|
|
53
|
+
).toEqual(['-y', SKILLS_INSTALLER_SPEC, 'remove', 'notifai', '--yes'])
|
|
54
|
+
expect(
|
|
55
|
+
skillsRemoveArgv({
|
|
56
|
+
skill: 'notifai',
|
|
57
|
+
scope: 'global',
|
|
58
|
+
cwd: '/tmp',
|
|
59
|
+
env: {},
|
|
60
|
+
}),
|
|
61
|
+
).toEqual(['-y', SKILLS_INSTALLER_SPEC, 'remove', 'notifai', '--global', '--yes'])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
25
65
|
describe('nativeSkills.list', () => {
|
|
26
66
|
it('reads the project lock file instead of spawning npx', async () => {
|
|
27
67
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-skills-project-'))
|
package/src/native-skills.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { accountHome, npxLaunch } from './platform.js'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Exact reviewed version of the external `skills` installer.
|
|
8
|
+
*
|
|
9
|
+
* The GitHub skill source stays the immutable CLI version tag. The installer
|
|
10
|
+
* program that fetches it must not float on `latest`.
|
|
11
|
+
*/
|
|
12
|
+
export const SKILLS_INSTALLER_SPEC = 'skills@1.5.23'
|
|
13
|
+
|
|
6
14
|
/** The two scopes offered by the skills installer. */
|
|
7
15
|
export type SkillScope = 'project' | 'global'
|
|
8
16
|
|
|
@@ -30,9 +38,18 @@ export interface SkillsAddOptions {
|
|
|
30
38
|
env: NodeJS.ProcessEnv
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
export interface SkillsRemoveOptions {
|
|
42
|
+
skill: string
|
|
43
|
+
scope: SkillScope
|
|
44
|
+
cwd: string
|
|
45
|
+
env: NodeJS.ProcessEnv
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export interface NativeSkills {
|
|
34
49
|
/** Launch the native interactive `npx skills add` flow. */
|
|
35
50
|
add(options: SkillsAddOptions): Promise<number>
|
|
51
|
+
/** Uninstall one installer-managed skill in one scope. */
|
|
52
|
+
remove(options: SkillsRemoveOptions): Promise<number>
|
|
36
53
|
/** Read installer-managed inventory from lock files. Does not spawn npx. */
|
|
37
54
|
list(scope: SkillScope, cwd: string, env: NodeJS.ProcessEnv): Promise<SkillsListResult>
|
|
38
55
|
}
|
|
@@ -108,15 +125,32 @@ function run(args: string[], options: { cwd: string; env: NodeJS.ProcessEnv }):
|
|
|
108
125
|
})
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
/** argv for `npx`, including the pinned installer spec. */
|
|
129
|
+
export function skillsAddArgv(options: SkillsAddOptions): string[] {
|
|
130
|
+
const args = ['-y', SKILLS_INSTALLER_SPEC, 'add', options.source, '--skill', options.skill]
|
|
131
|
+
if (options.scope === 'global') args.push('--global')
|
|
132
|
+
// An explicit scope is the unattended contract. Native `--yes` keeps all
|
|
133
|
+
// remaining installer prompts non-interactive after the scope is chosen.
|
|
134
|
+
if (options.scope !== undefined) args.push('--yes')
|
|
135
|
+
return args
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** argv for uninstalling one skill in one installer scope. */
|
|
139
|
+
export function skillsRemoveArgv(options: SkillsRemoveOptions): string[] {
|
|
140
|
+
const args = ['-y', SKILLS_INSTALLER_SPEC, 'remove', options.skill]
|
|
141
|
+
if (options.scope === 'global') args.push('--global')
|
|
142
|
+
args.push('--yes')
|
|
143
|
+
return args
|
|
144
|
+
}
|
|
145
|
+
|
|
111
146
|
/** The only process/filesystem adapter Notifai needs for the external installer. */
|
|
112
147
|
export const nativeSkills: NativeSkills = {
|
|
113
148
|
async add(options) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return run(args, { cwd: options.cwd, env: options.env })
|
|
149
|
+
return run(skillsAddArgv(options), { cwd: options.cwd, env: options.env })
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async remove(options) {
|
|
153
|
+
return run(skillsRemoveArgv(options), { cwd: options.cwd, env: options.env })
|
|
120
154
|
},
|
|
121
155
|
|
|
122
156
|
async list(scope, cwd, env) {
|
package/src/opencode-plugin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { hookHostPlatform, type HookHostPlatform } from './hook-adapter.js'
|
|
3
3
|
import { configHome } from './install-hooks.js'
|
|
4
|
+
import { SESSION_ACTIVATION_CONTEXT } from './session-activation.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The OpenCode adapter.
|
|
@@ -31,7 +32,7 @@ export const OPENCODE_PLUGIN_MARKER = '// notifai managed opencode plugin'
|
|
|
31
32
|
export const OPENCODE_PLUGIN_FILENAME = 'notifai.js'
|
|
32
33
|
|
|
33
34
|
/** Bump when an installed generated file must be rewritten to remain functional. */
|
|
34
|
-
const OPENCODE_ADAPTER_VERSION =
|
|
35
|
+
const OPENCODE_ADAPTER_VERSION = 10
|
|
35
36
|
|
|
36
37
|
export function opencodePluginDir(
|
|
37
38
|
global: boolean,
|
|
@@ -103,6 +104,7 @@ import { spawn } from "node:child_process"
|
|
|
103
104
|
${nodeConstant}const ADAPTER = ${JSON.stringify(adapterPath)}
|
|
104
105
|
const TIMEOUT_MS = ${timeoutSeconds * 1000}
|
|
105
106
|
const ADAPTER_VERSION = ${OPENCODE_ADAPTER_VERSION}
|
|
107
|
+
const SESSION_ACTIVATION_CONTEXT = ${JSON.stringify(SESSION_ACTIVATION_CONTEXT)}
|
|
106
108
|
const PENDING_SESSION_TITLE =
|
|
107
109
|
/^New session(?: *- *[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]+)?Z)?$/i
|
|
108
110
|
|
|
@@ -158,6 +160,17 @@ export const NotifAIPlugin = async ({ directory, client }) => {
|
|
|
158
160
|
const cwd = typeof directory === "string" && directory.length > 0 ? directory : process.cwd()
|
|
159
161
|
|
|
160
162
|
return {
|
|
163
|
+
/** Model-visible activation, independent of CLI setup and delivery state. */
|
|
164
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
165
|
+
const sessionID = input?.sessionID
|
|
166
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return
|
|
167
|
+
if (output.system.length === 0) {
|
|
168
|
+
output.system.push(SESSION_ACTIVATION_CONTEXT)
|
|
169
|
+
} else {
|
|
170
|
+
output.system[0] = output.system[0] + "\\n\\n" + SESSION_ACTIVATION_CONTEXT
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
161
174
|
/** Exact active-harness identity and first-party title for agent shell commands. */
|
|
162
175
|
"shell.env": async (input, output) => {
|
|
163
176
|
output.env.NOTIFAI_ACTIVE_HARNESS = "opencode"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { buildProgram, type ProgramRunners } from './program.js'
|
|
6
|
+
import type { CommandDeps } from './commands.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The layer these tests own is the argv boundary: what a real command line
|
|
10
|
+
* turns into by the time a command implementation receives it. Collector
|
|
11
|
+
* defaults, `--no-wait` disentangling, `--body-file` reading — none of that is
|
|
12
|
+
* visible to the unit tests on the commands themselves, and it used to be
|
|
13
|
+
* covered by nothing but reading `program.ts` with human eyes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
class ExitSentinel extends Error {
|
|
17
|
+
constructor(readonly code: number) {
|
|
18
|
+
super(`exit ${code}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeDeps(): CommandDeps & { outLines: string[]; errLines: string[] } {
|
|
23
|
+
const outLines: string[] = []
|
|
24
|
+
const errLines: string[] = []
|
|
25
|
+
return {
|
|
26
|
+
io: {
|
|
27
|
+
out: (line: string) => outLines.push(line),
|
|
28
|
+
err: (line: string) => errLines.push(line),
|
|
29
|
+
confirm: async () => false,
|
|
30
|
+
openUrl: () => {},
|
|
31
|
+
},
|
|
32
|
+
store: {
|
|
33
|
+
load: () => null,
|
|
34
|
+
save: () => {},
|
|
35
|
+
clear: () => {},
|
|
36
|
+
describe: () => 'test credential store',
|
|
37
|
+
},
|
|
38
|
+
env: {},
|
|
39
|
+
cwd: os.tmpdir(),
|
|
40
|
+
outLines,
|
|
41
|
+
errLines,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a real command line and capture what would have been dispatched.
|
|
47
|
+
* `exit` throws so post-exit code cannot run — the same property
|
|
48
|
+
* `process.exit` gives production.
|
|
49
|
+
*/
|
|
50
|
+
async function parse(
|
|
51
|
+
argv: string[],
|
|
52
|
+
runners: Partial<ProgramRunners>,
|
|
53
|
+
deps = makeDeps(),
|
|
54
|
+
): Promise<{ exitCode: number | undefined; deps: ReturnType<typeof makeDeps> }> {
|
|
55
|
+
let exitCode: number | undefined
|
|
56
|
+
const program = buildProgram(deps, {
|
|
57
|
+
runners,
|
|
58
|
+
exit: (code: number) => {
|
|
59
|
+
exitCode = code
|
|
60
|
+
throw new ExitSentinel(code)
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
try {
|
|
64
|
+
await program.parseAsync(['node', 'notifai', ...argv])
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (!(err instanceof ExitSentinel)) throw err
|
|
67
|
+
}
|
|
68
|
+
return { exitCode, deps }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('program argv parsing', () => {
|
|
72
|
+
it('maps a full send command line onto sendCommand flags', async () => {
|
|
73
|
+
let seen: Record<string, unknown> | undefined
|
|
74
|
+
const { exitCode } = await parse(
|
|
75
|
+
[
|
|
76
|
+
'send',
|
|
77
|
+
'--kind', 'done',
|
|
78
|
+
'--title', 'Build finished',
|
|
79
|
+
'--body', 'All green.',
|
|
80
|
+
'--choice', 'Ship it',
|
|
81
|
+
'--choice', 'Hold',
|
|
82
|
+
'--multi',
|
|
83
|
+
'--reply',
|
|
84
|
+
'--reply-timeout', '30',
|
|
85
|
+
],
|
|
86
|
+
{ send: (async (_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['send'] },
|
|
87
|
+
)
|
|
88
|
+
expect(exitCode).toBe(0)
|
|
89
|
+
expect(seen).toMatchObject({
|
|
90
|
+
kind: 'done',
|
|
91
|
+
title: 'Build finished',
|
|
92
|
+
body: 'All green.',
|
|
93
|
+
choice: ['Ship it', 'Hold'],
|
|
94
|
+
multi: true,
|
|
95
|
+
reply: true,
|
|
96
|
+
replyTimeout: 30,
|
|
97
|
+
noWait: false,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('drops empty collector defaults so "not passed" stays absent', async () => {
|
|
102
|
+
let seen: Record<string, unknown> | undefined
|
|
103
|
+
await parse(
|
|
104
|
+
['send', '--kind', 'update', '--title', 'T', '--body', 'B'],
|
|
105
|
+
{ send: (async (_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['send'] },
|
|
106
|
+
)
|
|
107
|
+
expect(seen).toBeDefined()
|
|
108
|
+
expect(seen).not.toHaveProperty('choice')
|
|
109
|
+
expect(seen).not.toHaveProperty('image')
|
|
110
|
+
expect(seen).not.toHaveProperty('imageAlt')
|
|
111
|
+
expect(seen).not.toHaveProperty('wait')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('maps init --setup-scope onto the command flags', async () => {
|
|
115
|
+
let seen: Record<string, unknown> | undefined
|
|
116
|
+
const { exitCode } = await parse(
|
|
117
|
+
['init', '--skills', '--setup-scope', 'global', '--no-hooks'],
|
|
118
|
+
{ init: (async (_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['init'] },
|
|
119
|
+
)
|
|
120
|
+
expect(exitCode).toBe(0)
|
|
121
|
+
expect(seen).toMatchObject({ skills: true, setupScope: 'global', hooks: false })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('disentangles --no-wait and --wait from their shared commander flag', async () => {
|
|
125
|
+
const capture = (sink: Record<string, unknown>[]): ProgramRunners['send'] =>
|
|
126
|
+
(async (_deps, flags) => (sink.push(flags as Record<string, unknown>), 0)) as ProgramRunners['send']
|
|
127
|
+
const seen: Record<string, unknown>[] = []
|
|
128
|
+
await parse(['send', '--kind', 'update', '--title', 'T', '--body', 'B', '--no-wait'], { send: capture(seen) })
|
|
129
|
+
await parse(['send', '--kind', 'update', '--title', 'T', '--body', 'B', '--wait', '5'], { send: capture(seen) })
|
|
130
|
+
expect(seen[0]).toMatchObject({ noWait: true })
|
|
131
|
+
expect(seen[0]).not.toHaveProperty('wait')
|
|
132
|
+
expect(seen[1]).toMatchObject({ noWait: false, wait: 5 })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('reads --body-file and refuses it alongside --body', async () => {
|
|
136
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'notifai-program-test-'))
|
|
137
|
+
const file = path.join(dir, 'body.md')
|
|
138
|
+
writeFileSync(file, 'From a file.\n')
|
|
139
|
+
let seen: Record<string, unknown> | undefined
|
|
140
|
+
const send = (async (_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['send']
|
|
141
|
+
|
|
142
|
+
const ok = await parse(['send', '--kind', 'update', '--title', 'T', '--body-file', file], { send })
|
|
143
|
+
expect(ok.exitCode).toBe(0)
|
|
144
|
+
expect(seen).toMatchObject({ body: 'From a file.\n' })
|
|
145
|
+
expect(seen).not.toHaveProperty('bodyFile')
|
|
146
|
+
|
|
147
|
+
const both = await parse(
|
|
148
|
+
['send', '--kind', 'update', '--title', 'T', '--body', 'B', '--body-file', file],
|
|
149
|
+
{ send },
|
|
150
|
+
)
|
|
151
|
+
expect(both.exitCode).toBe(2)
|
|
152
|
+
expect(both.deps.errLines.join('\n')).toContain('either --body or --body-file')
|
|
153
|
+
|
|
154
|
+
const missing = await parse(
|
|
155
|
+
['send', '--kind', 'update', '--title', 'T', '--body-file', path.join(dir, 'absent.md')],
|
|
156
|
+
{ send },
|
|
157
|
+
)
|
|
158
|
+
expect(missing.exitCode).toBe(2)
|
|
159
|
+
expect(missing.deps.errLines.join('\n')).toContain('Could not read')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('builds ask flags without empty collectors and passes the question through', async () => {
|
|
163
|
+
let seenQuestion: string | undefined
|
|
164
|
+
let seenFlags: Record<string, unknown> | undefined
|
|
165
|
+
const ask = (async (_deps, question, flags) => {
|
|
166
|
+
seenQuestion = question
|
|
167
|
+
seenFlags = flags as Record<string, unknown>
|
|
168
|
+
return 0
|
|
169
|
+
}) as ProgramRunners['ask']
|
|
170
|
+
|
|
171
|
+
await parse(
|
|
172
|
+
['ask', 'Deploy where?', '--choice', 'Staging', '--choice', 'Production', '--session-label', 'release run', '--json'],
|
|
173
|
+
{ ask },
|
|
174
|
+
)
|
|
175
|
+
expect(seenQuestion).toBe('Deploy where?')
|
|
176
|
+
expect(seenFlags).toEqual({
|
|
177
|
+
choice: ['Staging', 'Production'],
|
|
178
|
+
sessionLabel: 'release run',
|
|
179
|
+
json: true,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
await parse(['ask', 'Free text?'], { ask })
|
|
183
|
+
expect(seenFlags).toEqual({})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('rejects --no-block as unknown: the flag is gone, not tombstoned', async () => {
|
|
187
|
+
const { exitCode, deps } = await parse(
|
|
188
|
+
['send', '--kind', 'update', '--title', 'T', '--body', 'B', '--no-block'],
|
|
189
|
+
{ send: (async () => 0) as ProgramRunners['send'] },
|
|
190
|
+
)
|
|
191
|
+
expect(exitCode).toBe(2)
|
|
192
|
+
expect(deps.errLines).toHaveLength(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('names an unknown command and exits 2', async () => {
|
|
196
|
+
const { exitCode, deps } = await parse(['sendd'], {})
|
|
197
|
+
expect(exitCode).toBe(2)
|
|
198
|
+
expect(deps.errLines.join('\n')).toContain('Unknown command "sendd"')
|
|
199
|
+
expect(deps.errLines.join('\n')).toContain('send')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('rejects send --event now that the public event field is gone', async () => {
|
|
203
|
+
const { exitCode, deps } = await parse(
|
|
204
|
+
['send', '--kind', 'done', '--title', 'T', '--body', 'B', '--event', 'tests_passed'],
|
|
205
|
+
{ send: (async () => 0) as ProgramRunners['send'] },
|
|
206
|
+
)
|
|
207
|
+
expect(exitCode).toBe(2)
|
|
208
|
+
expect(deps.errLines).toHaveLength(0)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('drops the empty --event collector for logs', async () => {
|
|
212
|
+
let seen: Record<string, unknown> | undefined
|
|
213
|
+
const logs = ((_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['logs']
|
|
214
|
+
await parse(['logs'], { logs })
|
|
215
|
+
expect(seen).not.toHaveProperty('event')
|
|
216
|
+
await parse(['logs', '--event', 'send.submitted'], { logs })
|
|
217
|
+
expect(seen).toMatchObject({ event: ['send.submitted'] })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('passes close --pending without inventing a request id', async () => {
|
|
221
|
+
let seenId: string | undefined
|
|
222
|
+
let seenFlags: Record<string, unknown> | undefined
|
|
223
|
+
const close = (async (_deps, requestId, flags) => {
|
|
224
|
+
seenId = requestId
|
|
225
|
+
seenFlags = flags as Record<string, unknown>
|
|
226
|
+
return 0
|
|
227
|
+
}) as ProgramRunners['close']
|
|
228
|
+
|
|
229
|
+
await parse(['close', '--pending', '--json'], { close })
|
|
230
|
+
expect(seenId).toBeUndefined()
|
|
231
|
+
expect(seenFlags).toEqual({ pending: true, json: true })
|
|
232
|
+
|
|
233
|
+
await parse(['close', 'req_abc'], { close })
|
|
234
|
+
expect(seenId).toBe('req_abc')
|
|
235
|
+
expect(seenFlags).toEqual({})
|
|
236
|
+
})
|
|
237
|
+
})
|