@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.
Files changed (168) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commands-core.d.ts +4 -1
  3. package/dist/commands-core.js.map +1 -1
  4. package/dist/commands-doctor.js +2 -7
  5. package/dist/commands-doctor.js.map +1 -1
  6. package/dist/commands-hooks.d.ts +12 -0
  7. package/dist/commands-hooks.js +15 -1
  8. package/dist/commands-hooks.js.map +1 -1
  9. package/dist/commands-init.js +9 -6
  10. package/dist/commands-init.js.map +1 -1
  11. package/dist/commands-send-support.js +10 -11
  12. package/dist/commands-send-support.js.map +1 -1
  13. package/dist/commands-skill.js +72 -54
  14. package/dist/commands-skill.js.map +1 -1
  15. package/dist/harness-session-title.d.ts +22 -0
  16. package/dist/harness-session-title.js +74 -0
  17. package/dist/harness-session-title.js.map +1 -0
  18. package/dist/invocation-context.d.ts +1 -0
  19. package/dist/invocation-context.js +15 -1
  20. package/dist/invocation-context.js.map +1 -1
  21. package/dist/native-skills.d.ts +13 -3
  22. package/dist/native-skills.js +51 -12
  23. package/dist/native-skills.js.map +1 -1
  24. package/dist/node-floor.d.ts +2 -1
  25. package/dist/node-floor.js +9 -5
  26. package/dist/node-floor.js.map +1 -1
  27. package/dist/orca-session-title.d.ts +5 -4
  28. package/dist/orca-session-title.js +32 -19
  29. package/dist/orca-session-title.js.map +1 -1
  30. package/dist/platform.d.ts +11 -17
  31. package/dist/platform.js +20 -23
  32. package/dist/platform.js.map +1 -1
  33. package/dist/release.d.ts +5 -5
  34. package/dist/release.js +5 -5
  35. package/dist/session-labels.d.ts +2 -0
  36. package/dist/session-labels.js +20 -9
  37. package/dist/session-labels.js.map +1 -1
  38. package/dist/setup-destinations.js +1 -1
  39. package/dist/setup-destinations.js.map +1 -1
  40. package/dist/skill-integrity.d.ts +49 -0
  41. package/dist/skill-integrity.js +154 -0
  42. package/dist/skill-integrity.js.map +1 -0
  43. package/dist/skill-source/manifest.json +16 -0
  44. package/dist/skill-source/notifai/SKILL.md +390 -0
  45. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  46. package/package.json +11 -14
  47. package/src/atomic-file.ts +0 -198
  48. package/src/claude-wake.test.ts +0 -321
  49. package/src/claude-wake.ts +0 -499
  50. package/src/cli-bin.test.ts +0 -176
  51. package/src/cli-bin.ts +0 -233
  52. package/src/cli-release.test.ts +0 -46
  53. package/src/cli-release.ts +0 -78
  54. package/src/cli-update-contract.ts +0 -9
  55. package/src/cli-update.test.ts +0 -125
  56. package/src/client.test.ts +0 -390
  57. package/src/client.ts +0 -352
  58. package/src/codex-wake.test.ts +0 -254
  59. package/src/codex-wake.ts +0 -301
  60. package/src/command-session.ts +0 -38
  61. package/src/commands-agent-sessions.ts +0 -72
  62. package/src/commands-auth.ts +0 -238
  63. package/src/commands-config.ts +0 -336
  64. package/src/commands-core.ts +0 -312
  65. package/src/commands-devices.ts +0 -180
  66. package/src/commands-doctor.ts +0 -1501
  67. package/src/commands-guidance.ts +0 -181
  68. package/src/commands-harness-context.ts +0 -207
  69. package/src/commands-hook-shape.ts +0 -69
  70. package/src/commands-hooks.ts +0 -2318
  71. package/src/commands-init.ts +0 -1055
  72. package/src/commands-io.ts +0 -108
  73. package/src/commands-logs.ts +0 -243
  74. package/src/commands-project.ts +0 -47
  75. package/src/commands-send-support.test.ts +0 -156
  76. package/src/commands-send-support.ts +0 -311
  77. package/src/commands-send.ts +0 -875
  78. package/src/commands-setup-proof.test.ts +0 -72
  79. package/src/commands-setup-proof.ts +0 -124
  80. package/src/commands-skill.test.ts +0 -44
  81. package/src/commands-skill.ts +0 -204
  82. package/src/commands-sounds.test.ts +0 -106
  83. package/src/commands-sounds.ts +0 -49
  84. package/src/commands-update.ts +0 -180
  85. package/src/commands.test.ts +0 -8297
  86. package/src/commands.ts +0 -77
  87. package/src/config-schema.test.ts +0 -153
  88. package/src/config-schema.ts +0 -349
  89. package/src/config.test.ts +0 -581
  90. package/src/config.ts +0 -475
  91. package/src/credentials.test.ts +0 -403
  92. package/src/credentials.ts +0 -403
  93. package/src/file-lock-process-worker.test.ts +0 -64
  94. package/src/file-lock.test.ts +0 -320
  95. package/src/file-lock.ts +0 -393
  96. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  97. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  98. package/src/generated-session-label.ts +0 -155
  99. package/src/guidance-content.ts +0 -278
  100. package/src/guidance-render.ts +0 -56
  101. package/src/guidance.test.ts +0 -327
  102. package/src/guidance.ts +0 -233
  103. package/src/harness-integration.test.ts +0 -294
  104. package/src/harnesses.test.ts +0 -149
  105. package/src/harnesses.ts +0 -164
  106. package/src/hook-adapter.test.ts +0 -496
  107. package/src/hook-adapter.ts +0 -424
  108. package/src/hook-input.test.ts +0 -39
  109. package/src/hook-input.ts +0 -72
  110. package/src/hooks.test.ts +0 -5824
  111. package/src/hooks.ts +0 -4011
  112. package/src/install-hooks.test.ts +0 -1614
  113. package/src/install-hooks.ts +0 -1501
  114. package/src/interactive.test.ts +0 -387
  115. package/src/interactive.ts +0 -858
  116. package/src/invocation-context.test.ts +0 -352
  117. package/src/invocation-context.ts +0 -197
  118. package/src/logging-process-worker.test.ts +0 -45
  119. package/src/logging.test.ts +0 -544
  120. package/src/logging.ts +0 -698
  121. package/src/main-run.ts +0 -45
  122. package/src/main.ts +0 -12
  123. package/src/native-skills.test.ts +0 -115
  124. package/src/native-skills.ts +0 -162
  125. package/src/node-floor.test.ts +0 -27
  126. package/src/node-floor.ts +0 -37
  127. package/src/onboarding-docs.test.ts +0 -25
  128. package/src/openclaw-plugin.ts +0 -409
  129. package/src/opencode-plugin.ts +0 -298
  130. package/src/orca-session-title.test.ts +0 -54
  131. package/src/orca-session-title.ts +0 -103
  132. package/src/pack-feedback-slice.test.ts +0 -212
  133. package/src/pack-feedback-slice.ts +0 -187
  134. package/src/platform.test.ts +0 -152
  135. package/src/platform.ts +0 -173
  136. package/src/program.test.ts +0 -317
  137. package/src/program.ts +0 -842
  138. package/src/project-enablement.test.ts +0 -55
  139. package/src/project-enablement.ts +0 -99
  140. package/src/question-caps.test.ts +0 -49
  141. package/src/question-settlement-process.ts +0 -47
  142. package/src/question-timing.ts +0 -56
  143. package/src/readiness.test.ts +0 -25
  144. package/src/readiness.ts +0 -212
  145. package/src/release.ts +0 -51
  146. package/src/send-attempts.test.ts +0 -143
  147. package/src/send-attempts.ts +0 -188
  148. package/src/send.test.ts +0 -28
  149. package/src/send.ts +0 -436
  150. package/src/session-activation.ts +0 -61
  151. package/src/session-end-log-lock-worker.test.ts +0 -35
  152. package/src/session-labels.test.ts +0 -490
  153. package/src/session-labels.ts +0 -474
  154. package/src/setup-destinations.test.ts +0 -41
  155. package/src/setup-destinations.ts +0 -50
  156. package/src/skill.test.ts +0 -426
  157. package/src/sound-ref.test.ts +0 -31
  158. package/src/sound-ref.ts +0 -34
  159. package/src/trust-policy.test.ts +0 -701
  160. package/src/ui/banner.ts +0 -95
  161. package/src/ui/config-view.test.ts +0 -90
  162. package/src/ui/config-view.ts +0 -138
  163. package/src/ui/help.test.ts +0 -16
  164. package/src/ui/help.ts +0 -128
  165. package/src/ui/theme.test.ts +0 -92
  166. package/src/ui/theme.ts +0 -265
  167. package/src/url-policy.ts +0 -399
  168. package/tsconfig.json +0 -10
package/src/cli-bin.ts DELETED
@@ -1,233 +0,0 @@
1
- import {
2
- accessSync,
3
- constants,
4
- existsSync,
5
- lstatSync,
6
- readFileSync,
7
- realpathSync,
8
- } from 'node:fs'
9
- import path from 'node:path'
10
- import type { ReadinessState } from './readiness.js'
11
- import { packageVersion } from './release.js'
12
- import { cliUpdateRecoveryCommand } from './cli-update-contract.js'
13
-
14
- const POSIX_NAMES = ['notifai']
15
- const WINDOWS_NAMES = ['notifai.cmd', 'notifai.exe', 'notifai']
16
-
17
- export interface CliBinReadinessOptions {
18
- runningArtifactPath?: string
19
- currentVersion?: string | null
20
- }
21
-
22
- export interface CliPathEntry {
23
- command_path: string
24
- executable: boolean
25
- artifact_path: string | null
26
- version: string | null
27
- install_prefix: string | null
28
- }
29
-
30
- export interface CliInstallationInspection {
31
- current: { artifact_path: string; version: string | null }
32
- effective: CliPathEntry | null
33
- entries: CliPathEntry[]
34
- }
35
-
36
- export function pathNotifaiEntries(
37
- env: NodeJS.ProcessEnv,
38
- platform: NodeJS.Platform = process.platform,
39
- ): string[] {
40
- const raw = platform === 'win32' ? (env['Path'] ?? env['PATH'] ?? '') : (env['PATH'] ?? '')
41
- const delimiter = platform === 'win32' ? ';' : ':'
42
- const names = platform === 'win32' ? WINDOWS_NAMES : POSIX_NAMES
43
- const found: string[] = []
44
- for (const directory of raw.split(delimiter)) {
45
- if (directory === '') continue
46
- for (const name of names) {
47
- const candidate = path.join(directory, name)
48
- if (!existsSync(candidate)) continue
49
- if (!found.includes(candidate)) found.push(candidate)
50
- }
51
- }
52
- return found
53
- }
54
-
55
- export function isExecutablePath(file: string, platform: NodeJS.Platform = process.platform): boolean {
56
- if (!existsSync(file)) return false
57
- if (platform === 'win32') return true
58
- try {
59
- const target = lstatSync(file).isSymbolicLink() ? realpathSync(file) : file
60
- accessSync(target, constants.X_OK)
61
- return true
62
- } catch {
63
- return false
64
- }
65
- }
66
-
67
- function canonicalPath(file: string): string {
68
- try {
69
- return realpathSync(file)
70
- } catch {
71
- return path.resolve(file)
72
- }
73
- }
74
-
75
- function windowsShimArtifact(file: string): string | null {
76
- if (path.extname(file).toLowerCase() !== '.cmd') return null
77
- try {
78
- const source = readFileSync(file, 'utf8')
79
- const match = /(?:%dp0%|%~dp0)?([^"\r\n]*node_modules[\\/]@raidiant[\\/]notifai[\\/]dist[\\/]main\.js)/i.exec(source)
80
- if (match?.[0] === undefined) return null
81
- const expanded = match[0]
82
- .replace(/^%~?dp0%/i, `${path.dirname(file)}${path.sep}`)
83
- .replaceAll('\\', path.sep)
84
- return canonicalPath(expanded)
85
- } catch {
86
- return null
87
- }
88
- }
89
-
90
- function artifactForCommand(file: string, platform: NodeJS.Platform): string | null {
91
- const shim = platform === 'win32' ? windowsShimArtifact(file) : null
92
- if (shim !== null) return shim
93
- try {
94
- return canonicalPath(file)
95
- } catch {
96
- return null
97
- }
98
- }
99
-
100
- function artifactVersion(artifact: string | null): string | null {
101
- if (artifact === null) return null
102
- try {
103
- const parsed: unknown = JSON.parse(
104
- readFileSync(path.join(path.dirname(artifact), '..', 'package.json'), 'utf8'),
105
- )
106
- if (typeof parsed !== 'object' || parsed === null) return null
107
- const version = (parsed as { version?: unknown }).version
108
- return typeof version === 'string' && version !== '' ? version : null
109
- } catch {
110
- return null
111
- }
112
- }
113
-
114
- function installPrefix(artifact: string | null): string | null {
115
- if (artifact !== null) {
116
- const marker = /[\\/]lib[\\/]node_modules[\\/]@raidiant[\\/]notifai[\\/]/i.exec(artifact)
117
- ?? /[\\/]node_modules[\\/]@raidiant[\\/]notifai[\\/]/i.exec(artifact)
118
- if (marker?.index !== undefined) return artifact.slice(0, marker.index)
119
- }
120
- return null
121
- }
122
-
123
- export function inspectCliInstallations(
124
- env: NodeJS.ProcessEnv,
125
- platform: NodeJS.Platform = process.platform,
126
- options: CliBinReadinessOptions = {},
127
- ): CliInstallationInspection {
128
- const runningArtifact = canonicalPath(options.runningArtifactPath ?? process.argv[1] ?? 'notifai')
129
- const entries = pathNotifaiEntries(env, platform).map((command): CliPathEntry => {
130
- const artifact = artifactForCommand(command, platform)
131
- return {
132
- command_path: command,
133
- executable: isExecutablePath(command, platform),
134
- artifact_path: artifact,
135
- version: artifactVersion(artifact),
136
- install_prefix: installPrefix(artifact),
137
- }
138
- })
139
- return {
140
- current: {
141
- artifact_path: runningArtifact,
142
- version: options.currentVersion === undefined ? packageVersion() : options.currentVersion,
143
- },
144
- effective: entries.find((entry) => entry.executable) ?? null,
145
- entries,
146
- }
147
- }
148
-
149
- export function cliBinReadiness(
150
- env: NodeJS.ProcessEnv,
151
- platform: NodeJS.Platform = process.platform,
152
- options: CliBinReadinessOptions = {},
153
- ): ReadinessState {
154
- const inspection = inspectCliInstallations(env, platform, options)
155
- const { current, effective, entries } = inspection
156
- const updateCommand = cliUpdateRecoveryCommand()
157
- if (effective === null && entries.length > 0) {
158
- return {
159
- id: 'cli-bin',
160
- title: 'notifai command',
161
- status: 'gap',
162
- detail: 'the notifai command is on PATH but cannot run',
163
- technical: inspection,
164
- remedy: {
165
- by: 'user-here',
166
- summary: 'repair the global notifai command so it can run',
167
- command: updateCommand,
168
- },
169
- }
170
- }
171
- // A global install that landed outside PATH used to report `ready` with
172
- // "this process can run" — which is true of the running process and false of
173
- // every instruction it goes on to print. Hooks embed absolute paths, so
174
- // nothing visibly breaks until the reader types `notifai` themselves.
175
- //
176
- // Not a blocker: this process is already running, so the whole setup —
177
- // pairing, the app, the delivery proof — still completes. It is the later
178
- // `notifai …` lines that will not be found, and saying so is the fix.
179
- if (effective === null) {
180
- return {
181
- id: 'cli-bin',
182
- title: 'notifai command',
183
- status: 'optional-gap',
184
- detail:
185
- 'no `notifai` on PATH — this process runs, but a typed `notifai …` command will not be found',
186
- remedy: {
187
- by: 'user-here',
188
- summary: 'install notifai globally so the command is on your PATH',
189
- command: updateCommand,
190
- },
191
- }
192
- }
193
-
194
- const effectiveIsCurrent =
195
- effective.artifact_path === current.artifact_path ||
196
- (effective.version !== null && current.version !== null && effective.version === current.version)
197
- if (!effectiveIsCurrent) {
198
- return {
199
- id: 'cli-bin',
200
- title: 'notifai command',
201
- status: 'gap',
202
- detail: 'the notifai command resolves to a different installation than this current CLI',
203
- technical: inspection,
204
- remedy: {
205
- by: 'user-here',
206
- summary: 'update the notifai command that wins PATH',
207
- command: updateCommand,
208
- },
209
- }
210
- }
211
-
212
- const duplicateNeedsCleanup = entries.some(
213
- (entry) =>
214
- entry !== effective &&
215
- (!entry.executable || entry.artifact_path !== effective.artifact_path),
216
- )
217
- if (duplicateNeedsCleanup) {
218
- return {
219
- id: 'cli-bin',
220
- title: 'notifai command',
221
- status: 'optional-gap',
222
- detail: 'the notifai command is ready; another installation remains for cleanup',
223
- technical: inspection,
224
- }
225
- }
226
- return {
227
- id: 'cli-bin',
228
- title: 'notifai command',
229
- status: 'ready',
230
- detail: 'the notifai command is ready',
231
- technical: inspection,
232
- }
233
- }
@@ -1,46 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'vitest'
2
- import {
3
- compareCliSemVer,
4
- latestPublishedCliVersion,
5
- newerPublishedCli,
6
- resetLatestPublishedCliVersionForTest,
7
- shouldConsultCliRegistry,
8
- } from './cli-release.js'
9
-
10
- afterEach(() => {
11
- resetLatestPublishedCliVersionForTest()
12
- })
13
-
14
- describe('CLI registry recommendation', () => {
15
- it('is suppressed for JSON, non-interactive, and CI callers', () => {
16
- expect(shouldConsultCliRegistry({ interactive: true })).toBe(true)
17
- expect(shouldConsultCliRegistry({ interactive: true, json: true })).toBe(false)
18
- expect(shouldConsultCliRegistry({ interactive: false })).toBe(false)
19
- expect(shouldConsultCliRegistry({ interactive: true, env: { CI: 'true' } })).toBe(false)
20
- })
21
-
22
- it('never throws when the registry is unreachable', async () => {
23
- const latest = await latestPublishedCliVersion(async () => {
24
- throw new Error('network down')
25
- })
26
- expect(latest).toBeNull()
27
- })
28
-
29
- it('caches a successful dist-tag read', async () => {
30
- let calls = 0
31
- const fetchImpl: typeof fetch = async () => {
32
- calls += 1
33
- return new Response(JSON.stringify({ latest: '9.1.0' }), { status: 200 })
34
- }
35
- expect(await latestPublishedCliVersion(fetchImpl)).toBe('9.1.0')
36
- expect(await latestPublishedCliVersion(fetchImpl)).toBe('9.1.0')
37
- expect(calls).toBe(1)
38
- })
39
-
40
- it('only recommends a strictly newer published version', () => {
41
- expect(newerPublishedCli('8.0.0', '8.0.1')).toBe('8.0.1')
42
- expect(newerPublishedCli('8.0.0', '8.0.0')).toBeNull()
43
- expect(newerPublishedCli('8.0.1', '8.0.0')).toBeNull()
44
- expect(compareCliSemVer('8.0.0', '7.0.2')).toBeGreaterThan(0)
45
- })
46
- })
@@ -1,78 +0,0 @@
1
- import { packageVersion } from './release.js'
2
-
3
- /**
4
- * The newest published CLI, read from the registry that publishes it.
5
- *
6
- * Which version is newest is a registry fact. Doctor consults it as a
7
- * best-effort, cached, short-timeout hint: never fatal, and never on a
8
- * non-interactive or CI invocation — those callers already get structured
9
- * support state from the server when they are signed in.
10
- */
11
- const DIST_TAGS_URL = 'https://registry.npmjs.org/-/package/@raidiant/notifai/dist-tags'
12
- const REQUEST_TIMEOUT_MS = 2_000
13
- const CACHE_TTL_MS = 60 * 60 * 1000
14
-
15
- let cached: { value: string | null; at: number } | null = null
16
-
17
- export function compareCliSemVer(left: string, right: string): number | null {
18
- const parse = (value: string): [number, number, number] | null => {
19
- const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value)
20
- if (!match) return null
21
- return [Number(match[1]), Number(match[2]), Number(match[3])]
22
- }
23
- const a = parse(left)
24
- const b = parse(right)
25
- if (a === null || b === null) return null
26
- for (let index = 0; index < 3; index += 1) {
27
- if (a[index] !== b[index]) return a[index]! - b[index]!
28
- }
29
- return 0
30
- }
31
-
32
- export function shouldConsultCliRegistry(input: {
33
- interactive?: boolean
34
- json?: boolean
35
- env?: NodeJS.ProcessEnv
36
- }): boolean {
37
- if (input.json === true) return false
38
- if (input.interactive !== true) return false
39
- const ci = input.env?.['CI']
40
- return ci !== '1' && ci !== 'true'
41
- }
42
-
43
- export async function latestPublishedCliVersion(
44
- fetchImpl: typeof fetch = fetch,
45
- ): Promise<string | null> {
46
- if (cached !== null && Date.now() - cached.at < CACHE_TTL_MS) return cached.value
47
- try {
48
- const response = await fetchImpl(DIST_TAGS_URL, {
49
- headers: { accept: 'application/json' },
50
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
51
- })
52
- if (!response.ok) return cached?.value ?? null
53
- const body: unknown = await response.json()
54
- if (typeof body !== 'object' || body === null) return cached?.value ?? null
55
- const latest = (body as { latest?: unknown }).latest
56
- if (typeof latest !== 'string' || compareCliSemVer(latest, '0.0.0') === null) {
57
- return cached?.value ?? null
58
- }
59
- cached = { value: latest, at: Date.now() }
60
- return latest
61
- } catch {
62
- return cached?.value ?? null
63
- }
64
- }
65
-
66
- export function newerPublishedCli(local: string | null, latest: string | null): string | null {
67
- if (local === null || latest === null) return null
68
- const comparison = compareCliSemVer(latest, local)
69
- return comparison !== null && comparison > 0 ? latest : null
70
- }
71
-
72
- export function thisCliVersion(): string | null {
73
- return packageVersion()
74
- }
75
-
76
- export function resetLatestPublishedCliVersionForTest(): void {
77
- cached = null
78
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Resolve the current published CLI independently of every installed
3
- * `notifai` command. The fetched build owns the locally derived prefix repair,
4
- * so this action neither invokes the stale PATH winner nor exposes local
5
- * installation paths.
6
- */
7
- export function cliUpdateRecoveryCommand(): string {
8
- return 'npx --yes @raidiant/notifai@latest update'
9
- }
@@ -1,125 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import {
3
- chmodSync,
4
- mkdirSync,
5
- mkdtempSync,
6
- readFileSync,
7
- realpathSync,
8
- symlinkSync,
9
- writeFileSync,
10
- } from 'node:fs'
11
- import os from 'node:os'
12
- import path from 'node:path'
13
- import { describe, expect, it } from 'vitest'
14
- import type { CommandDeps, CommandIo } from './commands-core.js'
15
- import { cliUpdateCommand } from './commands-update.js'
16
- import { installHookAdapter, inspectHookAdapter } from './hook-adapter.js'
17
- import { packageVersion } from './release.js'
18
-
19
- class CapturedIo implements CommandIo {
20
- outLines: string[] = []
21
- errLines: string[] = []
22
- out(line: string) { this.outLines.push(line) }
23
- err(line: string) { this.errLines.push(line) }
24
- async confirm() { return false }
25
- openUrl() {}
26
- }
27
-
28
- function npmInstall(root: string, name: string, version: string) {
29
- const prefix = path.join(root, name)
30
- const packageRoot = path.join(prefix, 'lib', 'node_modules', '@raidiant', 'notifai')
31
- const artifact = path.join(packageRoot, 'dist', 'main.js')
32
- const command = path.join(prefix, 'bin', 'notifai')
33
- mkdirSync(path.dirname(artifact), { recursive: true })
34
- mkdirSync(path.dirname(command), { recursive: true })
35
- writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ version }))
36
- writeFileSync(artifact, `#!${process.execPath}\nprocess.stdout.write(${JSON.stringify(version)} + "\\n")\n`)
37
- chmodSync(artifact, 0o755)
38
- symlinkSync(path.relative(path.dirname(command), artifact), command)
39
- return { prefix, packageRoot, artifact, command }
40
- }
41
-
42
- describe('CLI update recovery', () => {
43
- it('updates the PATH winner prefix and retargets the shared hook adapter in one action', () => {
44
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-update-loop-'))
45
- const currentVersion = packageVersion()
46
- if (currentVersion === null) throw new Error('test build has no package version')
47
- const stale = npmInstall(root, 'stale-prefix', '3.0.1')
48
- const current = npmInstall(root, 'current-prefix', currentVersion)
49
- const managerPrefix = path.join(root, 'manager-prefix')
50
- const managerBin = path.join(managerPrefix, 'bin')
51
- const npm = path.join(managerBin, 'npm')
52
- const calls = path.join(root, 'npm-calls.jsonl')
53
- mkdirSync(managerBin, { recursive: true })
54
- writeFileSync(
55
- npm,
56
- `#!${process.execPath}
57
- const fs = require('node:fs');
58
- const path = require('node:path');
59
- const args = process.argv.slice(2);
60
- fs.appendFileSync(${JSON.stringify(calls)}, JSON.stringify(args) + '\\n');
61
- if (args[0] === 'prefix') {
62
- process.stdout.write(${JSON.stringify(managerPrefix)} + '\\n');
63
- process.exit(0);
64
- }
65
- const prefix = args[args.indexOf('--prefix') + 1];
66
- const packageRoot = path.join(prefix, 'lib', 'node_modules', '@raidiant', 'notifai');
67
- const artifact = path.join(packageRoot, 'dist', 'main.js');
68
- fs.mkdirSync(path.dirname(artifact), { recursive: true });
69
- fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ version: ${JSON.stringify(currentVersion)} }));
70
- fs.writeFileSync(artifact, '#!${process.execPath}\\nprocess.stdout.write(${JSON.stringify(`${currentVersion}\\n`)})\\n', { mode: 0o755 });
71
- `,
72
- )
73
- chmodSync(npm, 0o755)
74
-
75
- const home = path.join(root, 'home')
76
- installHookAdapter({ execPath: process.execPath, scriptPath: stale.artifact }, home)
77
- const io = new CapturedIo()
78
- const deps: CommandDeps = {
79
- io,
80
- store: { load: () => null, save: () => {}, clear: () => {}, describe: () => 'test' },
81
- env: {
82
- PATH: [managerBin, path.dirname(stale.command), path.dirname(current.command)].join(':'),
83
- },
84
- cwd: root,
85
- hookAdapterHome: home,
86
- hookInstallTarget: { execPath: process.execPath, scriptPath: current.artifact },
87
- hookPlatform: 'darwin',
88
- }
89
-
90
- expect(cliUpdateCommand(deps, {})).toBe(0)
91
- expect(JSON.parse(readFileSync(path.join(stale.packageRoot, 'package.json'), 'utf8'))).toMatchObject({
92
- version: currentVersion,
93
- })
94
- expect(
95
- readFileSync(calls, 'utf8').trim().split('\n').map((line) => JSON.parse(line)),
96
- ).toEqual([
97
- ['prefix', '--global'],
98
- ['install', '--global', '--prefix', realpathSync(stale.prefix), '@raidiant/notifai'],
99
- ])
100
- expect(spawnSync(stale.command, ['--version'], { encoding: 'utf8' }).stdout.trim()).toBe(currentVersion)
101
- expect(inspectHookAdapter(home).target).toMatchObject({ scriptPath: realpathSync(stale.artifact) })
102
- expect(io.outLines).toEqual(['Notifai is updated. Re-run `notifai init` to continue setup.'])
103
- expect(io.outLines.join('\n')).not.toContain(root)
104
- expect(io.errLines).toEqual([])
105
-
106
- io.outLines = []
107
- expect(cliUpdateCommand(deps, { json: true })).toBe(0)
108
- const result = JSON.parse(io.outLines[0] ?? '{}') as Record<string, unknown>
109
- expect(result).toMatchObject({
110
- ok: true,
111
- package_manager_prefix: managerPrefix,
112
- update_prefix: realpathSync(stale.prefix),
113
- after: {
114
- effective: {
115
- command_path: stale.command,
116
- artifact_path: realpathSync(stale.artifact),
117
- version: currentVersion,
118
- },
119
- },
120
- hook_adapter: {
121
- target: { scriptPath: realpathSync(stale.artifact) },
122
- },
123
- })
124
- })
125
- })