@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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2268
- package/src/commands-init.ts +0 -878
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -769
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands.test.ts +0 -7828
- package/src/commands.ts +0 -75
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -282
- package/src/program.ts +0 -818
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
|
@@ -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
|
-
}
|
package/src/platform.test.ts
DELETED
|
@@ -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
|
-
}
|