@raidiant/notifai 0.5.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +165 -0
- package/README.md +11 -4
- package/dist/claude-wake.d.ts +101 -0
- package/dist/claude-wake.js +397 -0
- package/dist/claude-wake.js.map +1 -0
- package/dist/codex-wake.d.ts +84 -0
- package/dist/codex-wake.js +238 -0
- package/dist/codex-wake.js.map +1 -0
- package/dist/commands.d.ts +31 -11
- package/dist/commands.js +679 -232
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +3 -3
- package/dist/config-schema.js +11 -49
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +23 -45
- package/dist/config.js +61 -39
- package/dist/config.js.map +1 -1
- package/dist/harnesses.d.ts +4 -13
- package/dist/harnesses.js +12 -17
- package/dist/harnesses.js.map +1 -1
- package/dist/hook-adapter.d.ts +10 -1
- package/dist/hook-adapter.js +79 -10
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +119 -65
- package/dist/hooks.js +330 -285
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +76 -24
- package/dist/install-hooks.js +301 -125
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +32 -1
- package/dist/interactive.js +165 -51
- package/dist/interactive.js.map +1 -1
- package/dist/main.js +14 -21
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +1 -1
- package/dist/native-skills.js +23 -58
- package/dist/native-skills.js.map +1 -1
- package/dist/pack-feedback-slice.d.ts +35 -0
- package/dist/pack-feedback-slice.js +126 -0
- package/dist/pack-feedback-slice.js.map +1 -0
- package/dist/readiness.d.ts +20 -0
- package/dist/readiness.js +24 -0
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +31 -0
- package/dist/release.js +52 -0
- package/dist/release.js.map +1 -0
- package/dist/ui/theme.d.ts +1 -1
- package/package.json +11 -10
- package/src/commands.test.ts +386 -36
- package/src/commands.ts +473 -182
- package/src/config-schema.ts +2 -17
- package/src/config.test.ts +29 -4
- package/src/config.ts +67 -11
- package/src/harnesses.ts +7 -0
- package/src/hook-adapter.test.ts +43 -1
- package/src/hook-adapter.ts +83 -11
- package/src/hooks.test.ts +4 -5
- package/src/install-hooks.test.ts +154 -2
- package/src/install-hooks.ts +231 -44
- package/src/interactive.test.ts +338 -0
- package/src/interactive.ts +221 -49
- package/src/main.ts +14 -20
- package/src/native-skills.test.ts +75 -0
- package/src/native-skills.ts +31 -75
- package/src/pack-feedback-slice.test.ts +212 -0
- package/src/pack-feedback-slice.ts +187 -0
- package/src/readiness.test.ts +25 -0
- package/src/readiness.ts +38 -0
- package/src/release.ts +51 -0
- package/src/skill.test.ts +43 -0
- package/src/ui/config-view.test.ts +5 -4
- package/dist/idle.d.ts +0 -16
- package/dist/idle.js +0 -44
- package/dist/idle.js.map +0 -1
package/src/install-hooks.ts
CHANGED
|
@@ -3,12 +3,13 @@ import {
|
|
|
3
3
|
lstatSync,
|
|
4
4
|
readFileSync,
|
|
5
5
|
realpathSync,
|
|
6
|
+
rmSync,
|
|
6
7
|
statSync,
|
|
7
8
|
} from 'node:fs'
|
|
8
9
|
import { createHash } from 'node:crypto'
|
|
9
10
|
import os from 'node:os'
|
|
10
11
|
import path from 'node:path'
|
|
11
|
-
import { parse as parseToml } from 'smol-toml'
|
|
12
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'
|
|
12
13
|
import { atomicWriteFileSync } from './atomic-file.js'
|
|
13
14
|
import { hookAdapterPath } from './hook-adapter.js'
|
|
14
15
|
import { opencodePluginPath, opencodePluginTarget } from './opencode-plugin.js'
|
|
@@ -30,10 +31,12 @@ const OPENCODE_EVENTS = [
|
|
|
30
31
|
/**
|
|
31
32
|
* Harness hook installation.
|
|
32
33
|
*
|
|
33
|
-
* Claude Code's `settings.json` and Codex's
|
|
34
|
+
* Claude Code's `settings.json` and Codex's hook files use the same shape —
|
|
34
35
|
* `hooks` maps an event name to matcher groups, each holding command handlers
|
|
35
36
|
* with an optional timeout — so one generator serves both. Only the file
|
|
36
|
-
* location and the event set differ.
|
|
37
|
+
* location and the event set differ. Codex accepts that shape as `hooks.json`
|
|
38
|
+
* or as inline `[hooks]` tables in the same layer's `config.toml`; one
|
|
39
|
+
* representation per layer, never both.
|
|
37
40
|
*
|
|
38
41
|
* Cursor's native format is flat and lower-camel-cased, while OpenCode's
|
|
39
42
|
* extension point is a JavaScript plugin module. Each therefore has a bounded
|
|
@@ -260,9 +263,7 @@ export function settingsFile(
|
|
|
260
263
|
? path.join(configHome(env, 'CLAUDE_CONFIG_DIR', '.claude'), 'settings.json')
|
|
261
264
|
: path.join(cwd, '.claude', 'settings.local.json')
|
|
262
265
|
case 'codex':
|
|
263
|
-
return global
|
|
264
|
-
? path.join(configHome(env, 'CODEX_HOME', '.codex'), 'hooks.json')
|
|
265
|
-
: path.join(codexProjectRoot(cwd), '.codex', 'hooks.json')
|
|
266
|
+
return inspectCodexLayer(codexLayerPaths(global, cwd, env)).writeTarget
|
|
266
267
|
default:
|
|
267
268
|
return assertNeverHarness(harness)
|
|
268
269
|
}
|
|
@@ -363,6 +364,159 @@ export function configHome(env: NodeJS.ProcessEnv, variable: string, fallback: s
|
|
|
363
364
|
return override !== undefined && override !== '' ? override : path.join(os.homedir(), fallback)
|
|
364
365
|
}
|
|
365
366
|
|
|
367
|
+
export interface CodexLayerPaths {
|
|
368
|
+
dir: string
|
|
369
|
+
hooksJson: string
|
|
370
|
+
configToml: string
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The two files Codex will look at for one config layer. */
|
|
374
|
+
export function codexLayerPaths(
|
|
375
|
+
global: boolean,
|
|
376
|
+
cwd: string,
|
|
377
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
378
|
+
): CodexLayerPaths {
|
|
379
|
+
const dir = global
|
|
380
|
+
? configHome(env, 'CODEX_HOME', '.codex')
|
|
381
|
+
: path.join(codexProjectRoot(cwd), '.codex')
|
|
382
|
+
return {
|
|
383
|
+
dir,
|
|
384
|
+
hooksJson: path.join(dir, 'hooks.json'),
|
|
385
|
+
configToml: path.join(dir, 'config.toml'),
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface CodexLayerInspection {
|
|
390
|
+
paths: CodexLayerPaths
|
|
391
|
+
jsonEvents: string[]
|
|
392
|
+
tomlEvents: string[]
|
|
393
|
+
writeTarget: string
|
|
394
|
+
dual: boolean
|
|
395
|
+
overlappingEvents: string[]
|
|
396
|
+
canCollapse: boolean
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Which representation this Codex layer already uses.
|
|
401
|
+
*
|
|
402
|
+
* Codex loads `hooks.json` and inline `[hooks]` side by side, runs every
|
|
403
|
+
* matching handler, and warns when both exist. `[hooks.state]` is the trust
|
|
404
|
+
* store, not a hook definition, so it does not count as a representation.
|
|
405
|
+
*/
|
|
406
|
+
export function inspectCodexLayer(paths: CodexLayerPaths): CodexLayerInspection {
|
|
407
|
+
const jsonDocument = tryLoadSettings(paths.hooksJson)
|
|
408
|
+
const tomlDocument = tryLoadSettings(paths.configToml)
|
|
409
|
+
const jsonEvents = hookEventNames(jsonDocument?.hooks)
|
|
410
|
+
const tomlEvents = hookEventNames(tomlDocument?.hooks)
|
|
411
|
+
const jsonForeign = documentHasForeignHandlers(jsonDocument)
|
|
412
|
+
const tomlForeign = documentHasForeignHandlers(tomlDocument)
|
|
413
|
+
const jsonOurs = documentHasOurHandlers(jsonDocument)
|
|
414
|
+
const tomlOurs = documentHasOurHandlers(tomlDocument)
|
|
415
|
+
const dual = jsonEvents.length > 0 && tomlEvents.length > 0
|
|
416
|
+
let writeTarget = paths.hooksJson
|
|
417
|
+
if (tomlEvents.length > 0 && jsonEvents.length === 0) writeTarget = paths.configToml
|
|
418
|
+
else if (jsonEvents.length > 0 && tomlEvents.length === 0) writeTarget = paths.hooksJson
|
|
419
|
+
else if (dual) {
|
|
420
|
+
if (tomlForeign && !jsonForeign) writeTarget = paths.configToml
|
|
421
|
+
else if (jsonForeign && !tomlForeign) writeTarget = paths.hooksJson
|
|
422
|
+
else if (!jsonForeign && !tomlForeign) writeTarget = paths.hooksJson
|
|
423
|
+
else if (tomlOurs && !jsonOurs) writeTarget = paths.configToml
|
|
424
|
+
else if (jsonOurs && !tomlOurs) writeTarget = paths.hooksJson
|
|
425
|
+
else writeTarget = paths.hooksJson
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
paths,
|
|
429
|
+
jsonEvents,
|
|
430
|
+
tomlEvents,
|
|
431
|
+
writeTarget,
|
|
432
|
+
dual,
|
|
433
|
+
overlappingEvents: jsonEvents.filter((event) => tomlEvents.includes(event)),
|
|
434
|
+
canCollapse: dual && !(jsonForeign && tomlForeign),
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Every file in this layer Codex might already be reading hooks from. */
|
|
439
|
+
export function hookDefinitionFiles(
|
|
440
|
+
harness: Harness,
|
|
441
|
+
global: boolean,
|
|
442
|
+
cwd: string,
|
|
443
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
444
|
+
): string[] {
|
|
445
|
+
if (harness !== 'codex') return [settingsFile(harness, global, cwd, env)]
|
|
446
|
+
const paths = codexLayerPaths(global, cwd, env)
|
|
447
|
+
return [paths.hooksJson, paths.configToml]
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Drop the unused representation after writing the one this layer already had.
|
|
452
|
+
*
|
|
453
|
+
* Only safe when the discarded file has no one else's handlers. `hooks.json`
|
|
454
|
+
* is deleted once it has no events left so Codex stops warning; `config.toml`
|
|
455
|
+
* is never deleted.
|
|
456
|
+
*/
|
|
457
|
+
export function collapseUnusedCodexRepresentation(
|
|
458
|
+
keepFile: string,
|
|
459
|
+
scriptPath: string,
|
|
460
|
+
paths: CodexLayerPaths,
|
|
461
|
+
): string | null {
|
|
462
|
+
const other = keepFile === paths.hooksJson ? paths.configToml : paths.hooksJson
|
|
463
|
+
if (!existsSync(other)) return null
|
|
464
|
+
const document = tryLoadSettings(other)
|
|
465
|
+
if (document === null || documentHasForeignHandlers(document)) return null
|
|
466
|
+
const stripped = removeHooks(document, scriptPath)
|
|
467
|
+
if (other === paths.hooksJson && hookEventNames(stripped.document.hooks).length === 0) {
|
|
468
|
+
rmSync(other)
|
|
469
|
+
return other
|
|
470
|
+
}
|
|
471
|
+
applyPlan(other, stripped.document)
|
|
472
|
+
return other
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function codexRepresentationProblems(
|
|
476
|
+
cwd: string,
|
|
477
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
478
|
+
): string[] {
|
|
479
|
+
return [false, true].flatMap((global) => {
|
|
480
|
+
const layer = inspectCodexLayer(codexLayerPaths(global, cwd, env))
|
|
481
|
+
if (!layer.dual) return []
|
|
482
|
+
const overlap =
|
|
483
|
+
layer.overlappingEvents.length > 0
|
|
484
|
+
? ` Matching handlers from both files all run — this layer's ${layer.overlappingEvents.join(', ')} handlers will both fire.`
|
|
485
|
+
: ' Matching handlers from both files all run.'
|
|
486
|
+
const collapse = layer.canCollapse
|
|
487
|
+
? ` Run \`notifai hooks install --harness codex${global ? ' --global' : ''}\` to collapse onto ${path.basename(layer.writeTarget)}.`
|
|
488
|
+
: ' Both files have other hooks, so Notifai cannot collapse them.'
|
|
489
|
+
return [
|
|
490
|
+
`loading hooks from both ${layer.paths.hooksJson} and ${layer.paths.configToml}; prefer a single representation for this layer.${overlap}${collapse}`,
|
|
491
|
+
]
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function tryLoadSettings(file: string): SettingsDocument | null {
|
|
496
|
+
if (!existsSync(file)) return null
|
|
497
|
+
try {
|
|
498
|
+
return loadSettings(file)
|
|
499
|
+
} catch {
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function hookEventNames(hooks: SettingsDocument['hooks']): string[] {
|
|
505
|
+
return Object.entries(hooks ?? {})
|
|
506
|
+
.filter(([, value]) => Array.isArray(value))
|
|
507
|
+
.map(([event]) => event)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function documentHasForeignHandlers(document: SettingsDocument | null): boolean {
|
|
511
|
+
if (document === null) return false
|
|
512
|
+
return locateAllHandlers(document).some((handler) => !isNotifaiCommand(handler.command))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function documentHasOurHandlers(document: SettingsDocument | null): boolean {
|
|
516
|
+
if (document === null) return false
|
|
517
|
+
return locateHandlers(document).length > 0
|
|
518
|
+
}
|
|
519
|
+
|
|
366
520
|
/** Best-effort detection so `hooks install` usually needs no flags. */
|
|
367
521
|
/**
|
|
368
522
|
* Harnesses this *project* shows evidence of, in the working directory only.
|
|
@@ -389,8 +543,8 @@ function localHarnessEvidence(cwd: string): Harness[] {
|
|
|
389
543
|
}
|
|
390
544
|
|
|
391
545
|
/** Harnesses installed anywhere on this machine — a much weaker signal. */
|
|
392
|
-
function globalHarnessEvidence(): Harness[] {
|
|
393
|
-
const home = os.homedir()
|
|
546
|
+
function globalHarnessEvidence(env: NodeJS.ProcessEnv = process.env): Harness[] {
|
|
547
|
+
const home = env['HOME'] !== undefined && env['HOME'] !== '' ? env['HOME'] : os.homedir()
|
|
394
548
|
const found: Harness[] = []
|
|
395
549
|
if (existsSync(path.join(home, '.claude'))) found.push('claude-code')
|
|
396
550
|
if (existsSync(path.join(home, '.codex'))) found.push('codex')
|
|
@@ -400,30 +554,34 @@ function globalHarnessEvidence(): Harness[] {
|
|
|
400
554
|
}
|
|
401
555
|
|
|
402
556
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
557
|
+
* Every supported harness this project or machine shows evidence of, in
|
|
558
|
+
* declared order. Project markers come first, then machine installs that the
|
|
559
|
+
* project did not already name.
|
|
405
560
|
*
|
|
406
|
-
*
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
561
|
+
* `AGENTS.md` still counts for nothing — see `localHarnessEvidence`.
|
|
562
|
+
*/
|
|
563
|
+
export function detectedHarnesses(cwd: string, env: NodeJS.ProcessEnv = process.env): Harness[] {
|
|
564
|
+
const seen = new Set<Harness>([...localHarnessEvidence(cwd), ...globalHarnessEvidence(env)])
|
|
565
|
+
return HARNESSES.filter((harness) => seen.has(harness))
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Which *single* harness to wire when a caller still needs exactly one, or
|
|
570
|
+
* null when it is genuinely unclear.
|
|
414
571
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
572
|
+
* Project evidence decides, and machine evidence is consulted only when the
|
|
573
|
+
* project offers none. Several markers are not a failure of detection — they
|
|
574
|
+
* are several harnesses. `detectedHarnesses` is the default for install;
|
|
575
|
+
* this helper stays for uninstall and any caller that cannot take a list.
|
|
418
576
|
*/
|
|
419
|
-
export function detectHarness(cwd: string): Harness | null {
|
|
577
|
+
export function detectHarness(cwd: string, env: NodeJS.ProcessEnv = process.env): Harness | null {
|
|
420
578
|
const local = localHarnessEvidence(cwd)
|
|
421
579
|
if (local.length > 0) return local.length === 1 ? local[0]! : null
|
|
422
|
-
const global = globalHarnessEvidence()
|
|
580
|
+
const global = globalHarnessEvidence(env)
|
|
423
581
|
return global.length === 1 ? global[0]! : null
|
|
424
582
|
}
|
|
425
583
|
|
|
426
|
-
interface SettingsDocument {
|
|
584
|
+
export interface SettingsDocument {
|
|
427
585
|
hooks?: HookConfig
|
|
428
586
|
[key: string]: unknown
|
|
429
587
|
}
|
|
@@ -571,6 +729,11 @@ export function mergeHooks(
|
|
|
571
729
|
const removed: string[] = []
|
|
572
730
|
|
|
573
731
|
for (const [event, groups] of Object.entries(existing.hooks ?? {})) {
|
|
732
|
+
if (!Array.isArray(groups)) {
|
|
733
|
+
// Codex keeps `[hooks.state]` beside event tables. It is not a matcher list.
|
|
734
|
+
;(hooks as Record<string, unknown>)[event] = groups
|
|
735
|
+
continue
|
|
736
|
+
}
|
|
574
737
|
const { groups: foreign, removed: hadOurs } = withoutOurs(groups, scriptPath)
|
|
575
738
|
if (hadOurs) {
|
|
576
739
|
if (event in incoming) replaced.push(event)
|
|
@@ -591,6 +754,10 @@ export function removeHooks(existing: SettingsDocument, scriptPath: string): Mer
|
|
|
591
754
|
const hooks: HookConfig = {}
|
|
592
755
|
const replaced: string[] = []
|
|
593
756
|
for (const [event, groups] of Object.entries(existing.hooks ?? {})) {
|
|
757
|
+
if (!Array.isArray(groups)) {
|
|
758
|
+
;(hooks as Record<string, unknown>)[event] = groups
|
|
759
|
+
continue
|
|
760
|
+
}
|
|
594
761
|
const { groups: foreign, removed } = withoutOurs(groups, scriptPath)
|
|
595
762
|
if (removed) replaced.push(event)
|
|
596
763
|
if (foreign.length > 0) hooks[event] = foreign
|
|
@@ -609,13 +776,31 @@ export function removeHooks(existing: SettingsDocument, scriptPath: string): Mer
|
|
|
609
776
|
* support.
|
|
610
777
|
*/
|
|
611
778
|
export function applyPlan(file: string, document: SettingsDocument | CursorSettingsDocument): void {
|
|
612
|
-
|
|
779
|
+
const body = isTomlSettingsPath(file)
|
|
780
|
+
? `${stringifyToml(document)}\n`
|
|
781
|
+
: `${JSON.stringify(document, null, 2)}\n`
|
|
782
|
+
atomicWriteFileSync(file, body, {
|
|
613
783
|
requireCurrentUserOwner: true,
|
|
614
784
|
})
|
|
615
785
|
}
|
|
616
786
|
|
|
617
787
|
export function loadSettings(file: string): SettingsDocument {
|
|
618
|
-
return readSettings(file)
|
|
788
|
+
return isTomlSettingsPath(file) ? readTomlSettings(file) : readSettings(file)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function isTomlSettingsPath(file: string): boolean {
|
|
792
|
+
return file.endsWith('.toml')
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function readTomlSettings(file: string): SettingsDocument {
|
|
796
|
+
if (!existsSync(file)) return {}
|
|
797
|
+
const source = readOwnedRegularFile(file)
|
|
798
|
+
try {
|
|
799
|
+
const parsed: unknown = parseToml(source)
|
|
800
|
+
return typeof parsed === 'object' && parsed !== null ? (parsed as SettingsDocument) : {}
|
|
801
|
+
} catch {
|
|
802
|
+
throw new Error(`Could not parse ${file}; fix or move it before installing hooks.`)
|
|
803
|
+
}
|
|
619
804
|
}
|
|
620
805
|
|
|
621
806
|
export function loadCursorSettings(file: string): CursorSettingsDocument {
|
|
@@ -766,10 +951,6 @@ function isNotifaiCommand(command: string): boolean {
|
|
|
766
951
|
return / hook (user-prompt-submit|stop|session-end)\b/.test(command)
|
|
767
952
|
}
|
|
768
953
|
|
|
769
|
-
function isNotifaiHandler(handler: HookHandler): boolean {
|
|
770
|
-
return isNotifaiCommand(handler.command)
|
|
771
|
-
}
|
|
772
|
-
|
|
773
954
|
/** Every place either harness would read a Notifai handler from. */
|
|
774
955
|
export function findInstallations(
|
|
775
956
|
cwd: string,
|
|
@@ -779,7 +960,8 @@ export function findInstallations(
|
|
|
779
960
|
const found: Installation[] = []
|
|
780
961
|
for (const harness of HARNESSES) {
|
|
781
962
|
for (const global of [false, true]) {
|
|
782
|
-
const
|
|
963
|
+
const files = hookDefinitionFiles(harness, global, cwd, env)
|
|
964
|
+
for (const file of files) {
|
|
783
965
|
if (!existsSync(file)) continue
|
|
784
966
|
// OpenCode's adapter is a plugin module, not a settings document, so it
|
|
785
967
|
// is reported as one installation covering all three events rather than
|
|
@@ -837,7 +1019,7 @@ export function findInstallations(
|
|
|
837
1019
|
}
|
|
838
1020
|
let document: SettingsDocument
|
|
839
1021
|
try {
|
|
840
|
-
document =
|
|
1022
|
+
document = loadSettings(file)
|
|
841
1023
|
} catch {
|
|
842
1024
|
continue
|
|
843
1025
|
}
|
|
@@ -846,6 +1028,7 @@ export function findInstallations(
|
|
|
846
1028
|
const problems = harnessMarkerProblems(harness, handlers, adapterHome)
|
|
847
1029
|
found.push({ harness, file, global, handlers, ...(problems.length > 0 ? { problems } : {}) })
|
|
848
1030
|
}
|
|
1031
|
+
}
|
|
849
1032
|
}
|
|
850
1033
|
}
|
|
851
1034
|
return found
|
|
@@ -889,28 +1072,32 @@ function locateCursorHandlers(document: CursorSettingsDocument): InstalledHandle
|
|
|
889
1072
|
return handlers
|
|
890
1073
|
}
|
|
891
1074
|
|
|
892
|
-
function
|
|
1075
|
+
function locateAllHandlers(document: SettingsDocument): InstalledHandler[] {
|
|
893
1076
|
const handlers: InstalledHandler[] = []
|
|
894
1077
|
for (const [event, groups] of Object.entries(document.hooks ?? {})) {
|
|
1078
|
+
if (!Array.isArray(groups)) continue
|
|
895
1079
|
groups.forEach((group, groupIndex) => {
|
|
896
1080
|
group.hooks?.forEach((handler, handlerIndex) => {
|
|
897
|
-
if (
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
1081
|
+
if (typeof handler?.command !== 'string') return
|
|
1082
|
+
handlers.push({
|
|
1083
|
+
event,
|
|
1084
|
+
groupIndex,
|
|
1085
|
+
handlerIndex,
|
|
1086
|
+
command: handler.command,
|
|
1087
|
+
...(handler.timeout === undefined ? {} : { timeout: handler.timeout }),
|
|
1088
|
+
...(handler.async === undefined ? {} : { async: handler.async }),
|
|
1089
|
+
...(handler.statusMessage === undefined ? {} : { statusMessage: handler.statusMessage }),
|
|
1090
|
+
})
|
|
908
1091
|
})
|
|
909
1092
|
})
|
|
910
1093
|
}
|
|
911
1094
|
return handlers
|
|
912
1095
|
}
|
|
913
1096
|
|
|
1097
|
+
function locateHandlers(document: SettingsDocument): InstalledHandler[] {
|
|
1098
|
+
return locateAllHandlers(document).filter((handler) => isNotifaiCommand(handler.command))
|
|
1099
|
+
}
|
|
1100
|
+
|
|
914
1101
|
/** The hook event a handler's command actually invokes, e.g. `stop`. */
|
|
915
1102
|
export function handlerEvent(command: string): string | null {
|
|
916
1103
|
return / hook ([a-z-]+)/.exec(command)?.[1] ?? null
|