@raidiant/notifai 10.1.0 → 10.1.2

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 (204) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +29 -11
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +9 -2
  19. package/dist/commands-core.js +11 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +93 -49
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-hooks.js +47 -10
  27. package/dist/commands-hooks.js.map +1 -1
  28. package/dist/commands-init.d.ts +16 -0
  29. package/dist/commands-init.js +254 -93
  30. package/dist/commands-init.js.map +1 -1
  31. package/dist/commands-send-support.js +3 -0
  32. package/dist/commands-send-support.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-skill.js +72 -54
  37. package/dist/commands-skill.js.map +1 -1
  38. package/dist/commands-update.d.ts +10 -0
  39. package/dist/commands-update.js +141 -0
  40. package/dist/commands-update.js.map +1 -0
  41. package/dist/commands.d.ts +2 -1
  42. package/dist/commands.js +2 -1
  43. package/dist/commands.js.map +1 -1
  44. package/dist/hook-adapter.d.ts +2 -0
  45. package/dist/hook-adapter.js +12 -1
  46. package/dist/hook-adapter.js.map +1 -1
  47. package/dist/hooks.d.ts +43 -2
  48. package/dist/hooks.js +163 -21
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/invocation-context.d.ts +1 -0
  51. package/dist/invocation-context.js +9 -1
  52. package/dist/invocation-context.js.map +1 -1
  53. package/dist/main-run.d.ts +1 -0
  54. package/dist/main-run.js +42 -0
  55. package/dist/main-run.js.map +1 -0
  56. package/dist/main.js +10 -31
  57. package/dist/main.js.map +1 -1
  58. package/dist/native-skills.d.ts +13 -3
  59. package/dist/native-skills.js +51 -12
  60. package/dist/native-skills.js.map +1 -1
  61. package/dist/node-floor.d.ts +19 -0
  62. package/dist/node-floor.js +39 -0
  63. package/dist/node-floor.js.map +1 -0
  64. package/dist/orca-session-title.d.ts +5 -4
  65. package/dist/orca-session-title.js +32 -19
  66. package/dist/orca-session-title.js.map +1 -1
  67. package/dist/platform.d.ts +11 -17
  68. package/dist/platform.js +20 -23
  69. package/dist/platform.js.map +1 -1
  70. package/dist/program.d.ts +2 -1
  71. package/dist/program.js +22 -5
  72. package/dist/program.js.map +1 -1
  73. package/dist/question-settlement-process.d.ts +15 -0
  74. package/dist/question-settlement-process.js +35 -0
  75. package/dist/question-settlement-process.js.map +1 -0
  76. package/dist/readiness.d.ts +25 -1
  77. package/dist/readiness.js +32 -2
  78. package/dist/readiness.js.map +1 -1
  79. package/dist/release.d.ts +5 -5
  80. package/dist/release.js +5 -5
  81. package/dist/setup-destinations.d.ts +23 -0
  82. package/dist/setup-destinations.js +44 -0
  83. package/dist/setup-destinations.js.map +1 -0
  84. package/dist/skill-integrity.d.ts +49 -0
  85. package/dist/skill-integrity.js +154 -0
  86. package/dist/skill-integrity.js.map +1 -0
  87. package/dist/skill-source/manifest.json +16 -0
  88. package/dist/skill-source/notifai/SKILL.md +390 -0
  89. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  90. package/package.json +11 -14
  91. package/src/atomic-file.ts +0 -198
  92. package/src/claude-wake.test.ts +0 -321
  93. package/src/claude-wake.ts +0 -499
  94. package/src/cli-bin.test.ts +0 -38
  95. package/src/cli-bin.ts +0 -67
  96. package/src/cli-release.test.ts +0 -46
  97. package/src/cli-release.ts +0 -78
  98. package/src/client.test.ts +0 -390
  99. package/src/client.ts +0 -345
  100. package/src/codex-wake.test.ts +0 -254
  101. package/src/codex-wake.ts +0 -297
  102. package/src/command-session.ts +0 -38
  103. package/src/commands-agent-sessions.ts +0 -72
  104. package/src/commands-auth.ts +0 -205
  105. package/src/commands-config.ts +0 -336
  106. package/src/commands-core.ts +0 -298
  107. package/src/commands-devices.test.ts +0 -15
  108. package/src/commands-devices.ts +0 -173
  109. package/src/commands-doctor.ts +0 -1440
  110. package/src/commands-guidance.ts +0 -181
  111. package/src/commands-harness-context.ts +0 -207
  112. package/src/commands-hook-shape.ts +0 -69
  113. package/src/commands-hooks.ts +0 -2268
  114. package/src/commands-init.ts +0 -878
  115. package/src/commands-io.ts +0 -108
  116. package/src/commands-logs.ts +0 -243
  117. package/src/commands-project.ts +0 -47
  118. package/src/commands-send-support.test.ts +0 -156
  119. package/src/commands-send-support.ts +0 -311
  120. package/src/commands-send.ts +0 -769
  121. package/src/commands-setup-proof.test.ts +0 -72
  122. package/src/commands-setup-proof.ts +0 -124
  123. package/src/commands-skill.test.ts +0 -44
  124. package/src/commands-skill.ts +0 -204
  125. package/src/commands-sounds.test.ts +0 -106
  126. package/src/commands-sounds.ts +0 -49
  127. package/src/commands.test.ts +0 -7828
  128. package/src/commands.ts +0 -75
  129. package/src/config-schema.test.ts +0 -153
  130. package/src/config-schema.ts +0 -349
  131. package/src/config.test.ts +0 -581
  132. package/src/config.ts +0 -475
  133. package/src/credentials.test.ts +0 -403
  134. package/src/credentials.ts +0 -403
  135. package/src/file-lock-process-worker.test.ts +0 -64
  136. package/src/file-lock.test.ts +0 -320
  137. package/src/file-lock.ts +0 -393
  138. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  139. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  140. package/src/generated-session-label.ts +0 -155
  141. package/src/guidance-content.ts +0 -278
  142. package/src/guidance-render.ts +0 -56
  143. package/src/guidance.test.ts +0 -327
  144. package/src/guidance.ts +0 -233
  145. package/src/harness-integration.test.ts +0 -294
  146. package/src/harnesses.test.ts +0 -149
  147. package/src/harnesses.ts +0 -164
  148. package/src/hook-adapter.test.ts +0 -496
  149. package/src/hook-adapter.ts +0 -410
  150. package/src/hook-input.test.ts +0 -39
  151. package/src/hook-input.ts +0 -72
  152. package/src/hooks.test.ts +0 -5549
  153. package/src/hooks.ts +0 -3759
  154. package/src/install-hooks.test.ts +0 -1614
  155. package/src/install-hooks.ts +0 -1501
  156. package/src/interactive.test.ts +0 -387
  157. package/src/interactive.ts +0 -858
  158. package/src/invocation-context.test.ts +0 -352
  159. package/src/invocation-context.ts +0 -197
  160. package/src/logging-process-worker.test.ts +0 -45
  161. package/src/logging.test.ts +0 -544
  162. package/src/logging.ts +0 -698
  163. package/src/main.ts +0 -36
  164. package/src/native-skills.test.ts +0 -115
  165. package/src/native-skills.ts +0 -162
  166. package/src/onboarding-docs.test.ts +0 -25
  167. package/src/openclaw-plugin.ts +0 -409
  168. package/src/opencode-plugin.ts +0 -298
  169. package/src/orca-session-title.test.ts +0 -54
  170. package/src/orca-session-title.ts +0 -103
  171. package/src/pack-feedback-slice.test.ts +0 -212
  172. package/src/pack-feedback-slice.ts +0 -187
  173. package/src/platform.test.ts +0 -152
  174. package/src/platform.ts +0 -173
  175. package/src/program.test.ts +0 -282
  176. package/src/program.ts +0 -818
  177. package/src/project-enablement.test.ts +0 -55
  178. package/src/project-enablement.ts +0 -99
  179. package/src/question-caps.test.ts +0 -49
  180. package/src/question-timing.ts +0 -56
  181. package/src/readiness.test.ts +0 -25
  182. package/src/readiness.ts +0 -179
  183. package/src/release.ts +0 -51
  184. package/src/send-attempts.test.ts +0 -143
  185. package/src/send-attempts.ts +0 -188
  186. package/src/send.test.ts +0 -28
  187. package/src/send.ts +0 -436
  188. package/src/session-activation.ts +0 -61
  189. package/src/session-end-log-lock-worker.test.ts +0 -35
  190. package/src/session-labels.test.ts +0 -490
  191. package/src/session-labels.ts +0 -474
  192. package/src/skill.test.ts +0 -415
  193. package/src/sound-ref.test.ts +0 -31
  194. package/src/sound-ref.ts +0 -34
  195. package/src/trust-policy.test.ts +0 -701
  196. package/src/ui/banner.ts +0 -95
  197. package/src/ui/config-view.test.ts +0 -90
  198. package/src/ui/config-view.ts +0 -138
  199. package/src/ui/help.test.ts +0 -16
  200. package/src/ui/help.ts +0 -128
  201. package/src/ui/theme.test.ts +0 -92
  202. package/src/ui/theme.ts +0 -265
  203. package/src/url-policy.ts +0 -399
  204. package/tsconfig.json +0 -10
@@ -1,187 +0,0 @@
1
- /**
2
- * Consented, bounded slice of the local log for a later feedback send.
3
- *
4
- * This module packs. It does not upload. A caller that wants a report on
5
- * the wire asks here for the bytes and the consent copy, then decides
6
- * whether to send them.
7
- */
8
- import { gzipSync } from 'node:zlib'
9
- import {
10
- LOG_SCHEMA_VERSION,
11
- readLogRecords,
12
- type LogQuery,
13
- type LogRecord,
14
- } from './logging.js'
15
-
16
- export const FEEDBACK_SLICE_UNCOMPRESSED_CAP = 256 * 1024
17
- export const FEEDBACK_SLICE_COMPRESSED_CAP = 128 * 1024
18
- export const FEEDBACK_SLICE_DEFAULT_WINDOW_MS = 24 * 60 * 60 * 1000
19
-
20
- /** High enough that on-disk rotation, not this number, bounds a full-window read. */
21
- const FULL_WINDOW_READ_LIMIT = 1_000_000
22
-
23
- export interface PackFeedbackSliceOptions extends LogQuery {
24
- /** Epoch ms. Defaults to now. Tests freeze the window with this. */
25
- now?: number
26
- }
27
-
28
- export interface PackedFeedbackSlice {
29
- records: LogRecord[]
30
- jsonl: string
31
- gzip?: Buffer
32
- uncompressed_bytes: number
33
- compressed_bytes: number
34
- record_count: number
35
- truncated: boolean
36
- since: number
37
- until: number
38
- schema_version: number
39
- consentSummary: string
40
- }
41
-
42
- export interface FeedbackSliceWire {
43
- encoding: 'gzip+base64'
44
- bytes: string
45
- uncompressed_bytes: number
46
- compressed_bytes: number
47
- record_count: number
48
- truncated: boolean
49
- since: string
50
- until: string
51
- schema_version: number
52
- }
53
-
54
- /** Wire form. Empty slices have nothing to encode. */
55
- export function encodeFeedbackSlice(slice: PackedFeedbackSlice): FeedbackSliceWire | undefined {
56
- if (slice.gzip === undefined) return undefined
57
- return {
58
- encoding: 'gzip+base64',
59
- bytes: slice.gzip.toString('base64'),
60
- uncompressed_bytes: slice.uncompressed_bytes,
61
- compressed_bytes: slice.compressed_bytes,
62
- record_count: slice.record_count,
63
- truncated: slice.truncated,
64
- since: new Date(slice.since).toISOString(),
65
- until: new Date(slice.until).toISOString(),
66
- schema_version: slice.schema_version,
67
- }
68
- }
69
-
70
- export function packFeedbackSlice(
71
- env: NodeJS.ProcessEnv = process.env,
72
- options: PackFeedbackSliceOptions = {},
73
- ): PackedFeedbackSlice {
74
- const { now, since: sinceOption, limit, ...filters } = options
75
- const until = now ?? Date.now()
76
- const since = sinceOption ?? until - FEEDBACK_SLICE_DEFAULT_WINDOW_MS
77
- const { records } = readLogRecords(env, {
78
- ...filters,
79
- since,
80
- limit: limit ?? FULL_WINDOW_READ_LIMIT,
81
- })
82
-
83
- if (records.length === 0) {
84
- return emptySlice(since, until)
85
- }
86
-
87
- const packed = fitToCaps(records)
88
- if (packed === null) {
89
- return emptySlice(since, until)
90
- }
91
-
92
- return {
93
- records: packed.records,
94
- jsonl: packed.jsonl,
95
- gzip: packed.gzip,
96
- uncompressed_bytes: packed.uncompressedBytes,
97
- compressed_bytes: packed.gzip.length,
98
- record_count: packed.records.length,
99
- truncated: packed.truncated,
100
- since,
101
- until,
102
- schema_version: LOG_SCHEMA_VERSION,
103
- consentSummary: consentSummary({
104
- since,
105
- until,
106
- recordCount: packed.records.length,
107
- uncompressedBytes: packed.uncompressedBytes,
108
- compressedBytes: packed.gzip.length,
109
- }),
110
- }
111
- }
112
-
113
- function fitToCaps(records: LogRecord[]): {
114
- records: LogRecord[]
115
- jsonl: string
116
- gzip: Buffer
117
- uncompressedBytes: number
118
- truncated: boolean
119
- } | null {
120
- const lines = records.map((record) => JSON.stringify(record))
121
- let start = 0
122
- let truncated = false
123
- while (start < lines.length) {
124
- const jsonl = toJsonl(lines.slice(start))
125
- const uncompressedBytes = Buffer.byteLength(jsonl)
126
- if (uncompressedBytes > FEEDBACK_SLICE_UNCOMPRESSED_CAP) {
127
- start += 1
128
- truncated = true
129
- continue
130
- }
131
- const gzip = gzipSync(jsonl)
132
- if (gzip.length <= FEEDBACK_SLICE_COMPRESSED_CAP) {
133
- return {
134
- records: records.slice(start),
135
- jsonl,
136
- gzip,
137
- uncompressedBytes,
138
- truncated,
139
- }
140
- }
141
- start += 1
142
- truncated = true
143
- }
144
- return null
145
- }
146
-
147
- function emptySlice(since: number, until: number): PackedFeedbackSlice {
148
- return {
149
- records: [],
150
- jsonl: '',
151
- uncompressed_bytes: 0,
152
- compressed_bytes: 0,
153
- record_count: 0,
154
- truncated: false,
155
- since,
156
- until,
157
- schema_version: LOG_SCHEMA_VERSION,
158
- consentSummary: consentSummary({
159
- since,
160
- until,
161
- recordCount: 0,
162
- uncompressedBytes: 0,
163
- compressedBytes: 0,
164
- }),
165
- }
166
- }
167
-
168
- function toJsonl(lines: string[]): string {
169
- if (lines.length === 0) return ''
170
- return `${lines.join('\n')}\n`
171
- }
172
-
173
- function consentSummary(input: {
174
- since: number
175
- until: number
176
- recordCount: number
177
- uncompressedBytes: number
178
- compressedBytes: number
179
- }): string {
180
- const from = new Date(input.since).toISOString()
181
- const to = new Date(input.until).toISOString()
182
- return (
183
- `About to send ${String(input.recordCount)} log records from ${from} to ${to} ` +
184
- `(${String(input.uncompressedBytes)} bytes uncompressed, ${String(input.compressedBytes)} bytes compressed). ` +
185
- 'Credentials are already removed. Notification titles and the user\'s answers are in the log.'
186
- )
187
- }
@@ -1,152 +0,0 @@
1
- import os from 'node:os'
2
- import path from 'node:path'
3
- import { describe, expect, it } from 'vitest'
4
- import {
5
- accountHome,
6
- cmdScript,
7
- configHome,
8
- isWindowsAbsolute,
9
- npxLaunch,
10
- quoteCmdArgument,
11
- stateHome,
12
- urlOpenLaunch,
13
- } from './platform.js'
14
- import { globalConfigDir, stateDir } from './config.js'
15
-
16
- const PAIRING_URL = 'https://notifai.example/approve?code=AB&next=1|calc'
17
-
18
- describe('urlOpenLaunch', () => {
19
- it('keeps a metacharacter pairing URL as one Windows argument and hides the console', () => {
20
- const launch = urlOpenLaunch(PAIRING_URL, 'win32')
21
- expect(launch.file.toLowerCase()).not.toMatch(/^cmd(\.exe)?$/)
22
- expect(launch.args).toEqual(['url.dll,FileProtocolHandler', PAIRING_URL])
23
- expect(launch.options.windowsHide).toBe(true)
24
- expect(launch.options.detached).toBe(true)
25
- expect(launch.options.stdio).toBe('ignore')
26
- })
27
-
28
- it('preserves best-effort open and xdg-open on POSIX', () => {
29
- expect(urlOpenLaunch(PAIRING_URL, 'darwin')).toEqual({
30
- file: 'open',
31
- args: [PAIRING_URL],
32
- options: { stdio: 'ignore', detached: true },
33
- })
34
- expect(urlOpenLaunch(PAIRING_URL, 'linux')).toEqual({
35
- file: 'xdg-open',
36
- args: [PAIRING_URL],
37
- options: { stdio: 'ignore', detached: true },
38
- })
39
- })
40
- })
41
-
42
- describe('npxLaunch', () => {
43
- const source = 'Raidiant-io/notifai#v1.0.1'
44
- const args = ['-y', 'skills', 'add', source, '--skill', 'notifai', '--yes']
45
-
46
- it('runs the .cmd shim through cmd on Windows with quoted arguments', () => {
47
- const launch = npxLaunch(args, {
48
- cwd: 'C:\\proj',
49
- env: { ComSpec: 'C:\\Windows\\system32\\cmd.exe' },
50
- platform: 'win32',
51
- })
52
- expect(launch.file).toBe('C:\\Windows\\system32\\cmd.exe')
53
- expect(launch.args.slice(0, 4)).toEqual(['/d', '/s', '/v:off', '/c'])
54
- expect(launch.args).toHaveLength(5)
55
- expect(launch.args[4]).toBe(cmdScript('npx.cmd', args))
56
- expect(launch.args[4]).toContain('npx.cmd')
57
- expect(launch.args[4]).toContain(quoteCmdArgument(source))
58
- expect(launch.options.windowsVerbatimArguments).toBe(true)
59
- expect(launch.options.windowsHide).toBe(true)
60
- expect(launch.options.stdio).toBe('inherit')
61
- expect(launch.options.cwd).toBe('C:\\proj')
62
- })
63
-
64
- it('quotes a hostile source so cmd cannot treat & as a second command', () => {
65
- const hostile = 'evil&calc.exe'
66
- const launch = npxLaunch(['-y', 'skills', 'add', hostile, '--skill', 'notifai'], {
67
- cwd: 'C:\\proj',
68
- env: {},
69
- platform: 'win32',
70
- })
71
- const script = launch.args[4] ?? ''
72
- expect(script).toContain(quoteCmdArgument(hostile))
73
- expect(script.startsWith('"') && script.endsWith('"')).toBe(true)
74
- expect(launch.file).toBe('cmd.exe')
75
- })
76
-
77
- it('spawns npx directly on POSIX without a shell', () => {
78
- const launch = npxLaunch(args, { cwd: '/tmp/proj', env: { PATH: '/usr/bin' }, platform: 'linux' })
79
- expect(launch).toEqual({
80
- file: 'npx',
81
- args,
82
- options: { cwd: '/tmp/proj', env: { PATH: '/usr/bin' }, stdio: 'inherit' },
83
- })
84
- expect(launch.options.windowsVerbatimArguments).toBeUndefined()
85
- })
86
- })
87
-
88
- describe('accountHome', () => {
89
- it('rejects an MSYS HOME on Windows and uses a Windows-absolute USERPROFILE', () => {
90
- expect(
91
- accountHome({ HOME: '/c/Users/rafael', USERPROFILE: 'C:\\Users\\rafael' }, 'win32'),
92
- ).toBe('C:\\Users\\rafael')
93
- expect(accountHome({ HOME: '/home/rafael', USERPROFILE: 'C:/Users/rafael' }, 'win32')).toBe(
94
- 'C:/Users/rafael',
95
- )
96
- expect(isWindowsAbsolute('/c/Users/rafael')).toBe(false)
97
- expect(isWindowsAbsolute('C:\\Users\\rafael')).toBe(true)
98
- })
99
-
100
- it('honours a Windows-absolute HOME and falls back to os.homedir()', () => {
101
- expect(accountHome({ HOME: 'D:\\homes\\dev', USERPROFILE: 'C:\\Users\\rafael' }, 'win32')).toBe(
102
- 'D:\\homes\\dev',
103
- )
104
- expect(accountHome({ HOME: '/c/Users/rafael' }, 'win32')).toBe(os.homedir())
105
- })
106
-
107
- it('preserves POSIX HOME, including a POSIX-looking value', () => {
108
- expect(accountHome({ HOME: '/home/rafael', USERPROFILE: 'C:\\Users\\rafael' }, 'linux')).toBe(
109
- '/home/rafael',
110
- )
111
- expect(accountHome({ HOME: '/c/Users/rafael' }, 'darwin')).toBe('/c/Users/rafael')
112
- expect(accountHome({}, 'linux')).toBe(os.homedir())
113
- })
114
- })
115
-
116
- describe('known-folder roots', () => {
117
- it('defaults Windows config to APPDATA/notifai and state to LOCALAPPDATA/notifai', () => {
118
- const env = {
119
- APPDATA: 'C:\\Users\\rafael\\AppData\\Roaming',
120
- LOCALAPPDATA: 'C:\\Users\\rafael\\AppData\\Local',
121
- HOME: '/c/Users/rafael',
122
- USERPROFILE: 'C:\\Users\\rafael',
123
- }
124
- expect(configHome(env, 'win32')).toBe(env.APPDATA)
125
- expect(stateHome(env, 'win32')).toBe(env.LOCALAPPDATA)
126
- expect(globalConfigDir(env, 'win32')).toBe(path.join(env.APPDATA, 'notifai'))
127
- expect(stateDir(env, 'win32')).toBe(path.join(env.LOCALAPPDATA, 'notifai'))
128
- })
129
-
130
- it('lets explicit XDG roots override Windows known folders', () => {
131
- const env = {
132
- XDG_CONFIG_HOME: 'D:\\xdg\\config',
133
- XDG_STATE_HOME: 'D:\\xdg\\state',
134
- APPDATA: 'C:\\Users\\rafael\\AppData\\Roaming',
135
- LOCALAPPDATA: 'C:\\Users\\rafael\\AppData\\Local',
136
- }
137
- expect(globalConfigDir(env, 'win32')).toBe(path.join('D:\\xdg\\config', 'notifai'))
138
- expect(stateDir(env, 'win32')).toBe(path.join('D:\\xdg\\state', 'notifai'))
139
- })
140
-
141
- it('keeps POSIX XDG defaults under the account home', () => {
142
- const env = { HOME: '/home/rafael' }
143
- expect(globalConfigDir(env, 'linux')).toBe(path.join('/home/rafael', '.config', 'notifai'))
144
- expect(stateDir(env, 'linux')).toBe(path.join('/home/rafael', '.local', 'state', 'notifai'))
145
- expect(globalConfigDir({ XDG_CONFIG_HOME: '/custom/config' }, 'darwin')).toBe(
146
- path.join('/custom/config', 'notifai'),
147
- )
148
- expect(stateDir({ XDG_STATE_HOME: '/custom/state' }, 'linux')).toBe(
149
- path.join('/custom/state', 'notifai'),
150
- )
151
- })
152
- })
package/src/platform.ts DELETED
@@ -1,173 +0,0 @@
1
- import { spawn, type SpawnOptions } from 'node:child_process'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
-
5
- /**
6
- * How this process should launch one child. Tests inspect the plan instead of
7
- * opening a browser or running npx; production hands it to `spawn`.
8
- */
9
- export interface ProcessLaunch {
10
- file: string
11
- args: string[]
12
- options: SpawnOptions
13
- }
14
-
15
- /** Drive letter or UNC. MSYS `/c/Users/…` is not a Windows account home. */
16
- export function isWindowsAbsolute(value: string): boolean {
17
- return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\')
18
- }
19
-
20
- /**
21
- * The account home used for Notifai's own defaults.
22
- *
23
- * On Windows, Git Bash and MSYS overwrite `HOME` with a POSIX path that is not
24
- * a usable NTFS root. Those values are ignored; a Windows-absolute
25
- * `USERPROFILE` or `os.homedir()` wins. A Windows-absolute `HOME` is still
26
- * honoured. POSIX keeps `HOME` as the override it has always been.
27
- */
28
- export function accountHome(
29
- env: NodeJS.ProcessEnv = process.env,
30
- platform: NodeJS.Platform = process.platform,
31
- ): string {
32
- if (platform === 'win32') {
33
- const home = env['HOME']
34
- if (typeof home === 'string' && isWindowsAbsolute(home)) return home
35
- const profile = env['USERPROFILE']
36
- if (typeof profile === 'string' && isWindowsAbsolute(profile)) return profile
37
- return os.homedir()
38
- }
39
- const home = env['HOME']
40
- if (typeof home === 'string' && home !== '') return home
41
- return os.homedir()
42
- }
43
-
44
- /**
45
- * Directory that contains the `notifai/` configuration folder.
46
- *
47
- * An explicit `XDG_CONFIG_HOME` always wins, including on Windows. Otherwise
48
- * Windows uses `%APPDATA%` and POSIX uses `~/.config`.
49
- */
50
- export function configHome(
51
- env: NodeJS.ProcessEnv = process.env,
52
- platform: NodeJS.Platform = process.platform,
53
- ): string {
54
- const xdg = env['XDG_CONFIG_HOME']
55
- if (typeof xdg === 'string' && xdg !== '') return xdg
56
- if (platform === 'win32') {
57
- const appdata = env['APPDATA']
58
- if (typeof appdata === 'string' && appdata !== '') return appdata
59
- }
60
- return path.join(accountHome(env, platform), '.config')
61
- }
62
-
63
- /**
64
- * Directory that contains the `notifai/` machine-local state folder.
65
- *
66
- * An explicit `XDG_STATE_HOME` always wins. Otherwise Windows uses
67
- * `%LOCALAPPDATA%` and POSIX uses `~/.local/state`.
68
- */
69
- export function stateHome(
70
- env: NodeJS.ProcessEnv = process.env,
71
- platform: NodeJS.Platform = process.platform,
72
- ): string {
73
- const xdg = env['XDG_STATE_HOME']
74
- if (typeof xdg === 'string' && xdg !== '') return xdg
75
- if (platform === 'win32') {
76
- const local = env['LOCALAPPDATA']
77
- if (typeof local === 'string' && local !== '') return local
78
- }
79
- return path.join(accountHome(env, platform), '.local', 'state')
80
- }
81
-
82
- /**
83
- * Open a pairing or support URL.
84
- *
85
- * Windows goes through `rundll32` so the URL is one CreateProcess argument and
86
- * `cmd` never reparses `?a=1&b=2`. `windowsHide` keeps the helper from flashing
87
- * a console. macOS and Linux stay best-effort `open` / `xdg-open`.
88
- */
89
- export function urlOpenLaunch(
90
- url: string,
91
- platform: NodeJS.Platform = process.platform,
92
- ): ProcessLaunch {
93
- if (platform === 'darwin') {
94
- return { file: 'open', args: [url], options: { stdio: 'ignore', detached: true } }
95
- }
96
- if (platform === 'win32') {
97
- return {
98
- file: 'rundll32.exe',
99
- args: ['url.dll,FileProtocolHandler', url],
100
- options: { stdio: 'ignore', detached: true, windowsHide: true },
101
- }
102
- }
103
- return { file: 'xdg-open', args: [url], options: { stdio: 'ignore', detached: true } }
104
- }
105
-
106
- /** Best-effort browser open. Failure is silent: the URL is printed either way. */
107
- export function openUrl(url: string, platform: NodeJS.Platform = process.platform): void {
108
- try {
109
- const launch = urlOpenLaunch(url, platform)
110
- spawn(launch.file, launch.args, launch.options).unref()
111
- } catch {
112
- // The caller already printed the URL.
113
- }
114
- }
115
-
116
- /**
117
- * Quote one cmd.exe argument so it cannot start a second command.
118
- *
119
- * `%` expands even inside quotes, so it is doubled. Quotes themselves are
120
- * doubled. Everything else is inert once quoted, including `& | > < ^`.
121
- */
122
- export function quoteCmdArgument(value: string): string {
123
- return `"${value.replace(/%/g, '%%').replace(/"/g, '""')}"`
124
- }
125
-
126
- /**
127
- * A single `/c` argument for `cmd /d /s /c`.
128
- *
129
- * `/s` strips the first and last quote when both ends are quotes, so the
130
- * payload is wrapped once more around the already-quoted argv. What cmd then
131
- * runs is `"npx.cmd" "-y" …` with no unquoted metacharacters.
132
- */
133
- export function cmdScript(command: string, args: readonly string[]): string {
134
- return `"${[command, ...args].map(quoteCmdArgument).join(' ')}"`
135
- }
136
-
137
- /**
138
- * Launch `npx` so Windows can run the `.cmd` shim.
139
- *
140
- * `spawn('npx', …)` is CreateProcess of a file named `npx`, which does not
141
- * exist — npm installs `npx.cmd`. cmd.exe can run that shim; CreateProcess
142
- * cannot. Arguments stay in one quoted `/c` string rather than `shell: true`.
143
- */
144
- export function npxLaunch(
145
- args: readonly string[],
146
- options: {
147
- cwd: string
148
- env: NodeJS.ProcessEnv
149
- platform?: NodeJS.Platform
150
- stdio?: SpawnOptions['stdio']
151
- },
152
- ): ProcessLaunch {
153
- const platform = options.platform ?? process.platform
154
- const stdio = options.stdio ?? 'inherit'
155
- if (platform === 'win32') {
156
- return {
157
- file: options.env['ComSpec'] && options.env['ComSpec'] !== '' ? options.env['ComSpec'] : 'cmd.exe',
158
- args: ['/d', '/s', '/v:off', '/c', cmdScript('npx.cmd', args)],
159
- options: {
160
- cwd: options.cwd,
161
- env: options.env,
162
- stdio,
163
- windowsHide: true,
164
- windowsVerbatimArguments: true,
165
- },
166
- }
167
- }
168
- return {
169
- file: 'npx',
170
- args: [...args],
171
- options: { cwd: options.cwd, env: options.env, stdio },
172
- }
173
- }