@raidiant/notifai 10.1.1 → 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 (156) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands-core.d.ts +1 -1
  3. package/dist/commands-init.js +9 -6
  4. package/dist/commands-init.js.map +1 -1
  5. package/dist/commands-send-support.js +3 -0
  6. package/dist/commands-send-support.js.map +1 -1
  7. package/dist/commands-skill.js +72 -54
  8. package/dist/commands-skill.js.map +1 -1
  9. package/dist/invocation-context.d.ts +1 -0
  10. package/dist/invocation-context.js +9 -1
  11. package/dist/invocation-context.js.map +1 -1
  12. package/dist/native-skills.d.ts +13 -3
  13. package/dist/native-skills.js +51 -12
  14. package/dist/native-skills.js.map +1 -1
  15. package/dist/node-floor.d.ts +2 -1
  16. package/dist/node-floor.js +9 -5
  17. package/dist/node-floor.js.map +1 -1
  18. package/dist/orca-session-title.d.ts +5 -4
  19. package/dist/orca-session-title.js +32 -19
  20. package/dist/orca-session-title.js.map +1 -1
  21. package/dist/platform.d.ts +11 -17
  22. package/dist/platform.js +20 -23
  23. package/dist/platform.js.map +1 -1
  24. package/dist/release.d.ts +5 -5
  25. package/dist/release.js +5 -5
  26. package/dist/setup-destinations.js +1 -1
  27. package/dist/setup-destinations.js.map +1 -1
  28. package/dist/skill-integrity.d.ts +49 -0
  29. package/dist/skill-integrity.js +154 -0
  30. package/dist/skill-integrity.js.map +1 -0
  31. package/dist/skill-source/manifest.json +16 -0
  32. package/dist/skill-source/notifai/SKILL.md +390 -0
  33. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  34. package/package.json +11 -14
  35. package/src/atomic-file.ts +0 -198
  36. package/src/claude-wake.test.ts +0 -321
  37. package/src/claude-wake.ts +0 -499
  38. package/src/cli-bin.test.ts +0 -176
  39. package/src/cli-bin.ts +0 -233
  40. package/src/cli-release.test.ts +0 -46
  41. package/src/cli-release.ts +0 -78
  42. package/src/cli-update-contract.ts +0 -9
  43. package/src/cli-update.test.ts +0 -125
  44. package/src/client.test.ts +0 -390
  45. package/src/client.ts +0 -352
  46. package/src/codex-wake.test.ts +0 -254
  47. package/src/codex-wake.ts +0 -301
  48. package/src/command-session.ts +0 -38
  49. package/src/commands-agent-sessions.ts +0 -72
  50. package/src/commands-auth.ts +0 -238
  51. package/src/commands-config.ts +0 -336
  52. package/src/commands-core.ts +0 -312
  53. package/src/commands-devices.ts +0 -180
  54. package/src/commands-doctor.ts +0 -1501
  55. package/src/commands-guidance.ts +0 -181
  56. package/src/commands-harness-context.ts +0 -207
  57. package/src/commands-hook-shape.ts +0 -69
  58. package/src/commands-hooks.ts +0 -2318
  59. package/src/commands-init.ts +0 -1055
  60. package/src/commands-io.ts +0 -108
  61. package/src/commands-logs.ts +0 -243
  62. package/src/commands-project.ts +0 -47
  63. package/src/commands-send-support.test.ts +0 -156
  64. package/src/commands-send-support.ts +0 -311
  65. package/src/commands-send.ts +0 -875
  66. package/src/commands-setup-proof.test.ts +0 -72
  67. package/src/commands-setup-proof.ts +0 -124
  68. package/src/commands-skill.test.ts +0 -44
  69. package/src/commands-skill.ts +0 -204
  70. package/src/commands-sounds.test.ts +0 -106
  71. package/src/commands-sounds.ts +0 -49
  72. package/src/commands-update.ts +0 -180
  73. package/src/commands.test.ts +0 -8297
  74. package/src/commands.ts +0 -77
  75. package/src/config-schema.test.ts +0 -153
  76. package/src/config-schema.ts +0 -349
  77. package/src/config.test.ts +0 -581
  78. package/src/config.ts +0 -475
  79. package/src/credentials.test.ts +0 -403
  80. package/src/credentials.ts +0 -403
  81. package/src/file-lock-process-worker.test.ts +0 -64
  82. package/src/file-lock.test.ts +0 -320
  83. package/src/file-lock.ts +0 -393
  84. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  85. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  86. package/src/generated-session-label.ts +0 -155
  87. package/src/guidance-content.ts +0 -278
  88. package/src/guidance-render.ts +0 -56
  89. package/src/guidance.test.ts +0 -327
  90. package/src/guidance.ts +0 -233
  91. package/src/harness-integration.test.ts +0 -294
  92. package/src/harnesses.test.ts +0 -149
  93. package/src/harnesses.ts +0 -164
  94. package/src/hook-adapter.test.ts +0 -496
  95. package/src/hook-adapter.ts +0 -424
  96. package/src/hook-input.test.ts +0 -39
  97. package/src/hook-input.ts +0 -72
  98. package/src/hooks.test.ts +0 -5824
  99. package/src/hooks.ts +0 -4011
  100. package/src/install-hooks.test.ts +0 -1614
  101. package/src/install-hooks.ts +0 -1501
  102. package/src/interactive.test.ts +0 -387
  103. package/src/interactive.ts +0 -858
  104. package/src/invocation-context.test.ts +0 -352
  105. package/src/invocation-context.ts +0 -197
  106. package/src/logging-process-worker.test.ts +0 -45
  107. package/src/logging.test.ts +0 -544
  108. package/src/logging.ts +0 -698
  109. package/src/main-run.ts +0 -45
  110. package/src/main.ts +0 -12
  111. package/src/native-skills.test.ts +0 -115
  112. package/src/native-skills.ts +0 -162
  113. package/src/node-floor.test.ts +0 -27
  114. package/src/node-floor.ts +0 -37
  115. package/src/onboarding-docs.test.ts +0 -25
  116. package/src/openclaw-plugin.ts +0 -409
  117. package/src/opencode-plugin.ts +0 -298
  118. package/src/orca-session-title.test.ts +0 -54
  119. package/src/orca-session-title.ts +0 -103
  120. package/src/pack-feedback-slice.test.ts +0 -212
  121. package/src/pack-feedback-slice.ts +0 -187
  122. package/src/platform.test.ts +0 -152
  123. package/src/platform.ts +0 -173
  124. package/src/program.test.ts +0 -317
  125. package/src/program.ts +0 -842
  126. package/src/project-enablement.test.ts +0 -55
  127. package/src/project-enablement.ts +0 -99
  128. package/src/question-caps.test.ts +0 -49
  129. package/src/question-settlement-process.ts +0 -47
  130. package/src/question-timing.ts +0 -56
  131. package/src/readiness.test.ts +0 -25
  132. package/src/readiness.ts +0 -212
  133. package/src/release.ts +0 -51
  134. package/src/send-attempts.test.ts +0 -143
  135. package/src/send-attempts.ts +0 -188
  136. package/src/send.test.ts +0 -28
  137. package/src/send.ts +0 -436
  138. package/src/session-activation.ts +0 -61
  139. package/src/session-end-log-lock-worker.test.ts +0 -35
  140. package/src/session-labels.test.ts +0 -490
  141. package/src/session-labels.ts +0 -474
  142. package/src/setup-destinations.test.ts +0 -41
  143. package/src/setup-destinations.ts +0 -50
  144. package/src/skill.test.ts +0 -426
  145. package/src/sound-ref.test.ts +0 -31
  146. package/src/sound-ref.ts +0 -34
  147. package/src/trust-policy.test.ts +0 -701
  148. package/src/ui/banner.ts +0 -95
  149. package/src/ui/config-view.test.ts +0 -90
  150. package/src/ui/config-view.ts +0 -138
  151. package/src/ui/help.test.ts +0 -16
  152. package/src/ui/help.ts +0 -128
  153. package/src/ui/theme.test.ts +0 -92
  154. package/src/ui/theme.ts +0 -265
  155. package/src/url-policy.ts +0 -399
  156. 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
- }