@raidiant/notifai 8.0.0 → 9.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 +41 -0
- package/README.md +20 -13
- package/dist/atomic-file.d.ts +14 -0
- package/dist/atomic-file.js +27 -2
- package/dist/atomic-file.js.map +1 -1
- package/dist/cli-bin.d.ts +4 -0
- package/dist/cli-bin.js +62 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli-release.d.ts +10 -0
- package/dist/cli-release.js +75 -0
- package/dist/cli-release.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +13 -3
- package/dist/client.js.map +1 -1
- package/dist/commands-auth.d.ts +6 -0
- package/dist/commands-auth.js +28 -3
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +22 -3
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +12 -0
- package/dist/commands-core.js +27 -3
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +71 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +7 -3
- package/dist/commands-guidance.js +63 -28
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-hooks.d.ts +9 -4
- package/dist/commands-hooks.js +430 -32
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +10 -18
- package/dist/commands-init.js +114 -32
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +61 -5
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.js +60 -11
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +8 -0
- package/dist/commands-setup-proof.js +31 -2
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands-skill.d.ts +11 -1
- package/dist/commands-skill.js +61 -11
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +25 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -3
- package/dist/config.js +37 -5
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +10 -1
- package/dist/credentials.js +25 -12
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +20 -0
- package/dist/guidance-content.js +73 -8
- package/dist/guidance-content.js.map +1 -1
- package/dist/guidance.d.ts +21 -0
- package/dist/guidance.js +73 -18
- package/dist/guidance.js.map +1 -1
- package/dist/hook-adapter.js +37 -3
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +68 -7
- package/dist/hooks.js +231 -85
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -18
- package/dist/install-hooks.js +110 -23
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +1 -1
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +8 -1
- package/dist/logging.js +43 -2
- package/dist/logging.js.map +1 -1
- package/dist/main.js +2 -13
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +19 -0
- package/dist/native-skills.js +30 -8
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.js +14 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +13 -8
- package/dist/program.js.map +1 -1
- package/dist/send.d.ts +8 -1
- package/dist/send.js +17 -1
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +15 -0
- package/dist/session-activation.js +35 -0
- package/dist/session-activation.js.map +1 -0
- package/dist/url-policy.d.ts +85 -0
- package/dist/url-policy.js +369 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +3 -3
- package/src/atomic-file.ts +36 -2
- package/src/cli-bin.test.ts +38 -0
- package/src/cli-bin.ts +67 -0
- package/src/cli-release.test.ts +46 -0
- package/src/cli-release.ts +78 -0
- package/src/client.test.ts +95 -0
- package/src/client.ts +16 -3
- package/src/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-doctor.ts +99 -13
- package/src/commands-guidance.ts +75 -29
- package/src/commands-hooks.ts +503 -34
- package/src/commands-init.ts +139 -38
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +67 -5
- package/src/commands-send.ts +73 -10
- package/src/commands-setup-proof.ts +36 -2
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1083 -101
- package/src/commands.ts +1 -0
- package/src/config-schema.ts +28 -1
- package/src/config.test.ts +1 -1
- package/src/config.ts +49 -5
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +74 -8
- package/src/guidance.test.ts +42 -11
- package/src/guidance.ts +100 -18
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +746 -165
- package/src/hooks.ts +299 -124
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +1 -1
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +2 -14
- package/src/native-skills.test.ts +41 -1
- package/src/native-skills.ts +40 -6
- package/src/opencode-plugin.ts +14 -1
- package/src/program.test.ts +37 -2
- package/src/program.ts +40 -12
- package/src/send.test.ts +28 -0
- package/src/send.ts +28 -2
- package/src/session-activation.ts +45 -0
- package/src/skill.test.ts +120 -4
- package/src/trust-policy.test.ts +701 -0
- package/src/url-policy.ts +399 -0
package/src/commands-doctor.ts
CHANGED
|
@@ -40,9 +40,11 @@ import {
|
|
|
40
40
|
} from './readiness.js'
|
|
41
41
|
import { packageVersion } from './release.js'
|
|
42
42
|
import type { Tone } from './ui/theme.js'
|
|
43
|
+
import type { MachineCredential } from './credentials.js'
|
|
43
44
|
import {
|
|
44
45
|
EXIT,
|
|
45
46
|
UPDATE_CLI_COMMAND,
|
|
47
|
+
diagnoseIgnoredOriginOverride,
|
|
46
48
|
loadLoggedConfig,
|
|
47
49
|
makeClient,
|
|
48
50
|
resolvedBaseUrl,
|
|
@@ -56,8 +58,25 @@ import {
|
|
|
56
58
|
type ActiveHarnessSession,
|
|
57
59
|
} from './commands-harness-context.js'
|
|
58
60
|
import { stopShapeProblems } from './commands-hook-shape.js'
|
|
59
|
-
import {
|
|
60
|
-
|
|
61
|
+
import {
|
|
62
|
+
HOOK_EVENTS,
|
|
63
|
+
activeQuestionRouteProblems,
|
|
64
|
+
hookActivationAdvice,
|
|
65
|
+
requiredHookEvents,
|
|
66
|
+
} from './commands-hooks.js'
|
|
67
|
+
import { cliBinReadiness } from './cli-bin.js'
|
|
68
|
+
import {
|
|
69
|
+
latestPublishedCliVersion,
|
|
70
|
+
newerPublishedCli,
|
|
71
|
+
shouldConsultCliRegistry,
|
|
72
|
+
thisCliVersion,
|
|
73
|
+
} from './cli-release.js'
|
|
74
|
+
import {
|
|
75
|
+
SETUP_PROOF_STALE_MS,
|
|
76
|
+
observedCompanionReceipt,
|
|
77
|
+
readSetupProof,
|
|
78
|
+
setupProofIsStale,
|
|
79
|
+
} from './commands-setup-proof.js'
|
|
61
80
|
import { skillReadiness } from './commands-skill.js'
|
|
62
81
|
|
|
63
82
|
// ---------------------------------------------------------------------------
|
|
@@ -236,10 +255,14 @@ function remoteStatesFrom(previous: Readiness): {
|
|
|
236
255
|
return { credential, server, contract, auth, devices, proof }
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
function remoteInvalidatedByConfig(
|
|
240
|
-
|
|
258
|
+
function remoteInvalidatedByConfig(
|
|
259
|
+
previous: Readiness,
|
|
260
|
+
config: CliConfig,
|
|
261
|
+
credential: MachineCredential | null,
|
|
262
|
+
): boolean {
|
|
263
|
+
const origin = resolvedBaseUrl(config, credential)
|
|
241
264
|
const server = previous.states.find((state) => state.id === 'server')
|
|
242
|
-
return server === undefined || !server.detail.includes(
|
|
265
|
+
return server === undefined || !server.detail.includes(origin)
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
/**
|
|
@@ -257,9 +280,12 @@ export async function assessReadiness(
|
|
|
257
280
|
skillScope?: SkillScope
|
|
258
281
|
previous?: Readiness
|
|
259
282
|
refresh?: readonly ReadinessRefresh[]
|
|
283
|
+
json?: boolean
|
|
260
284
|
} = {},
|
|
261
285
|
): Promise<Readiness> {
|
|
262
286
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
287
|
+
const credential = deps.store.load()
|
|
288
|
+
if (credential) diagnoseIgnoredOriginOverride(deps.io, config, credential)
|
|
263
289
|
const previous = options.previous
|
|
264
290
|
const refresh = options.refresh
|
|
265
291
|
if (previous !== undefined && refresh !== undefined && refresh.length === 0) return previous
|
|
@@ -268,12 +294,13 @@ export async function assessReadiness(
|
|
|
268
294
|
previous !== undefined &&
|
|
269
295
|
refresh !== undefined &&
|
|
270
296
|
!refresh.includes('remote') &&
|
|
271
|
-
!remoteInvalidatedByConfig(previous, config)
|
|
297
|
+
!remoteInvalidatedByConfig(previous, config, credential)
|
|
272
298
|
if (reuseRemote) {
|
|
273
299
|
const reused = remoteStatesFrom(previous)
|
|
274
300
|
if (reused !== null) {
|
|
275
301
|
return {
|
|
276
302
|
states: [
|
|
303
|
+
cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform),
|
|
277
304
|
projectReadiness(deps, config),
|
|
278
305
|
reused.credential,
|
|
279
306
|
reused.server,
|
|
@@ -292,9 +319,9 @@ export async function assessReadiness(
|
|
|
292
319
|
let accountClient: ApiClient | null = null
|
|
293
320
|
let accountDevices: RoutableDevice[] | null = null
|
|
294
321
|
|
|
322
|
+
states.push(cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform))
|
|
295
323
|
states.push(projectReadiness(deps, config))
|
|
296
324
|
|
|
297
|
-
const credential = deps.store.load()
|
|
298
325
|
states.push(
|
|
299
326
|
credential
|
|
300
327
|
? {
|
|
@@ -457,10 +484,39 @@ export async function assessReadiness(
|
|
|
457
484
|
}
|
|
458
485
|
|
|
459
486
|
states.push(await setupProofState(deps, config, accountClient, accountDevices))
|
|
487
|
+
await applyRegistryRecommendation(deps, options.json === true, states)
|
|
460
488
|
|
|
461
489
|
return { states }
|
|
462
490
|
}
|
|
463
491
|
|
|
492
|
+
async function applyRegistryRecommendation(
|
|
493
|
+
deps: CommandDeps,
|
|
494
|
+
json: boolean,
|
|
495
|
+
states: ReadinessState[],
|
|
496
|
+
): Promise<void> {
|
|
497
|
+
if (
|
|
498
|
+
!shouldConsultCliRegistry({
|
|
499
|
+
...(deps.io.interactive === undefined ? {} : { interactive: deps.io.interactive }),
|
|
500
|
+
json,
|
|
501
|
+
env: deps.env,
|
|
502
|
+
})
|
|
503
|
+
) {
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
const latest = await latestPublishedCliVersion(deps.fetchImpl)
|
|
507
|
+
const newer = newerPublishedCli(thisCliVersion(), latest)
|
|
508
|
+
if (newer === null) return
|
|
509
|
+
const contract = states.find((state) => state.id === 'contract')
|
|
510
|
+
if (contract === undefined || contract.status === 'gap') return
|
|
511
|
+
contract.status = 'optional-gap'
|
|
512
|
+
contract.detail = 'A newer Notifai is available.'
|
|
513
|
+
contract.remedy = {
|
|
514
|
+
by: 'user-here',
|
|
515
|
+
summary: 'update Notifai',
|
|
516
|
+
command: UPDATE_CLI_COMMAND,
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
464
520
|
async function setupProofState(
|
|
465
521
|
deps: CommandDeps,
|
|
466
522
|
config: CliConfig,
|
|
@@ -518,6 +574,20 @@ async function setupProofState(
|
|
|
518
574
|
detail: `Companion Receipt (the app's delivery confirmation) observed from ${observed.delivery.device_name} at ${observed.observedAt} (${proof.request_id})`,
|
|
519
575
|
}
|
|
520
576
|
}
|
|
577
|
+
const now = (deps.now ?? Date.now)()
|
|
578
|
+
if (setupProofIsStale(proof, now)) {
|
|
579
|
+
return {
|
|
580
|
+
id: 'proof',
|
|
581
|
+
title: 'Delivery proof',
|
|
582
|
+
status: 'gap',
|
|
583
|
+
detail: `${proof.request_id} is older than ${SETUP_PROOF_STALE_MS / 36e5}h with no Companion Receipt; init will send a replacement`,
|
|
584
|
+
remedy: {
|
|
585
|
+
by: 'cli',
|
|
586
|
+
summary: 'replace the stale verification notification',
|
|
587
|
+
command: 'notifai init',
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
}
|
|
521
591
|
return {
|
|
522
592
|
id: 'proof',
|
|
523
593
|
title: 'Delivery proof',
|
|
@@ -549,7 +619,8 @@ export async function doctorCommand(
|
|
|
549
619
|
flags: { json?: boolean },
|
|
550
620
|
options: { readiness?: Readiness } = {},
|
|
551
621
|
): Promise<number> {
|
|
552
|
-
const readiness =
|
|
622
|
+
const readiness =
|
|
623
|
+
options.readiness ?? (await assessReadiness(deps, flags.json === true ? { json: true } : {}))
|
|
553
624
|
const blocker = firstBlocker(readiness)
|
|
554
625
|
const ok = blocker === null
|
|
555
626
|
|
|
@@ -879,13 +950,25 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
879
950
|
})
|
|
880
951
|
.map((h) => `${h.event} -> ${handlerEvent(h.command)} in ${i.file}`),
|
|
881
952
|
)
|
|
953
|
+
const missing = installations.flatMap((installation) => {
|
|
954
|
+
const required = requiredHookEvents(installation.harness)
|
|
955
|
+
const installed = new Set(
|
|
956
|
+
installation.handlers
|
|
957
|
+
.map((handler) => handlerEvent(handler.command))
|
|
958
|
+
.filter((event): event is string => event !== null),
|
|
959
|
+
)
|
|
960
|
+
const absent = required.filter((event) => !installed.has(event))
|
|
961
|
+
return absent.length === 0
|
|
962
|
+
? []
|
|
963
|
+
: [`${installation.file} is missing ${absent.join(', ')}`]
|
|
964
|
+
})
|
|
882
965
|
checks.push({
|
|
883
966
|
name: 'hooks (stale)',
|
|
884
|
-
ok: stale.length === 0,
|
|
967
|
+
ok: stale.length === 0 && missing.length === 0,
|
|
885
968
|
detail:
|
|
886
|
-
stale.length === 0
|
|
887
|
-
? 'every installed handler names an event this build serves'
|
|
888
|
-
: `${stale.join('; ')} — rerun \`notifai hooks install\` to
|
|
969
|
+
stale.length === 0 && missing.length === 0
|
|
970
|
+
? 'every installed handler names an event this build serves, and every required lifecycle handler is present'
|
|
971
|
+
: `${[...stale, ...missing].join('; ')} — rerun \`notifai hooks install\` to refresh the complete lifecycle set`,
|
|
889
972
|
})
|
|
890
973
|
|
|
891
974
|
const adapterProblems = installations.flatMap((installation) =>
|
|
@@ -940,7 +1023,10 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
940
1023
|
// Project and global definitions for one harness both fire. Stable adapter
|
|
941
1024
|
// identity deliberately makes their command bytes equal, so comparing
|
|
942
1025
|
// command targets would now hide this duplicate rather than diagnose it.
|
|
943
|
-
//
|
|
1026
|
+
// Cursor is the exception to harness independence because it may also load
|
|
1027
|
+
// Claude's user hook file. The shared adapter detects Cursor's hook-only
|
|
1028
|
+
// environment and makes that compatibility copy a no-op; the native Cursor
|
|
1029
|
+
// definition remains the single routing owner.
|
|
944
1030
|
const duplicated = [...new Set(installations.map((i) => i.harness))]
|
|
945
1031
|
.map((harness) => ({
|
|
946
1032
|
harness,
|
package/src/commands-guidance.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { describeSource } from './config-schema.js'
|
|
4
4
|
import { EXIT, type CommandDeps } from './commands-core.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
GUIDANCE_TRUST_PREAMBLE,
|
|
7
|
+
SHIPPED_GUIDANCE,
|
|
8
|
+
shippedGuidanceTopic,
|
|
9
|
+
} from './guidance-content.js'
|
|
6
10
|
import {
|
|
7
11
|
GUIDANCE_TOPIC_MAX_BYTES,
|
|
8
12
|
GUIDANCE_TOPIC_PATTERN,
|
|
@@ -10,28 +14,60 @@ import {
|
|
|
10
14
|
globalGuidanceDir,
|
|
11
15
|
personalProjectGuidanceDir,
|
|
12
16
|
resolveGuidance,
|
|
17
|
+
type GuidanceAuthority,
|
|
13
18
|
} from './guidance.js'
|
|
14
19
|
|
|
15
20
|
// ---------------------------------------------------------------------------
|
|
16
21
|
// guidance show / set / unset
|
|
17
22
|
// ---------------------------------------------------------------------------
|
|
18
23
|
|
|
24
|
+
/** How each authority is named in the marker above a topic. */
|
|
25
|
+
const AUTHORITY_LABEL: Record<GuidanceAuthority, string> = {
|
|
26
|
+
user: 'you',
|
|
27
|
+
repository: 'this repository',
|
|
28
|
+
shipped: 'shipped default',
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
32
|
+
* The token that makes a marker line ours.
|
|
33
|
+
*
|
|
34
|
+
* Without it, a repository topic could open with a handwritten
|
|
35
|
+
* `<!-- when-to-notify · this machine · /Users/... -->` and everything after
|
|
36
|
+
* it would read as the User's own standing word. The marker therefore carries
|
|
37
|
+
* a fixed token, and any occurrence of that token inside file content is
|
|
38
|
+
* defanged below — so the only lines that can claim to be provenance are the
|
|
39
|
+
* ones this command wrote.
|
|
40
|
+
*/
|
|
41
|
+
const MARKER_TOKEN = 'notifai:guidance'
|
|
42
|
+
|
|
43
|
+
function markerSafe(content: string): string {
|
|
44
|
+
return content.replaceAll(MARKER_TOKEN, `${MARKER_TOKEN.replace(':', '-')} [not a provenance marker]`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The whole effective guidance, ready to be followed as read.
|
|
49
|
+
*
|
|
50
|
+
* The trust preamble comes first and belongs to the CLI: it says who supplied
|
|
51
|
+
* each topic and what no topic may ask for. Each topic then follows its own
|
|
52
|
+
* marker, so a reader can tell the User's standing word from a repository's
|
|
53
|
+
* house rules from the shipped default without the output stopping being
|
|
54
|
+
* Markdown.
|
|
23
55
|
*/
|
|
24
56
|
export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }): number {
|
|
25
57
|
const topics = resolveGuidance({ cwd: deps.cwd, env: deps.env })
|
|
26
58
|
if (flags.json) {
|
|
27
59
|
deps.io.out(
|
|
28
60
|
JSON.stringify(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
{
|
|
62
|
+
trust: GUIDANCE_TRUST_PREAMBLE,
|
|
63
|
+
topics: topics.map((topic) => ({
|
|
64
|
+
name: topic.name,
|
|
65
|
+
source: topic.source,
|
|
66
|
+
authority: topic.authority,
|
|
67
|
+
summary: topic.summary,
|
|
68
|
+
content: topic.content,
|
|
69
|
+
})),
|
|
70
|
+
},
|
|
35
71
|
null,
|
|
36
72
|
2,
|
|
37
73
|
),
|
|
@@ -39,12 +75,13 @@ export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }
|
|
|
39
75
|
return EXIT.ok
|
|
40
76
|
}
|
|
41
77
|
const blocks = topics.map((topic) => {
|
|
42
|
-
const {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
78
|
+
const { path: filePath } = describeSource(topic.source)
|
|
79
|
+
const marker =
|
|
80
|
+
`<!-- ${MARKER_TOKEN} topic=${topic.name} from=${AUTHORITY_LABEL[topic.authority]}` +
|
|
81
|
+
`${filePath === null ? '' : ` file=${encodeURIComponent(filePath)}`} -->`
|
|
82
|
+
return `${marker}\n${markerSafe(topic.content).trimEnd()}`
|
|
46
83
|
})
|
|
47
|
-
deps.io.out(blocks.join('\n\n'))
|
|
84
|
+
deps.io.out([GUIDANCE_TRUST_PREAMBLE, ...blocks].join('\n\n'))
|
|
48
85
|
return EXIT.ok
|
|
49
86
|
}
|
|
50
87
|
|
|
@@ -72,7 +109,7 @@ async function guidanceMutationTarget(
|
|
|
72
109
|
deps: CommandDeps,
|
|
73
110
|
topic: string,
|
|
74
111
|
flags: GuidanceMutationFlags,
|
|
75
|
-
): Promise<string | null> {
|
|
112
|
+
): Promise<{ path: string; layer: GuidanceLayer } | null> {
|
|
76
113
|
if (!GUIDANCE_TOPIC_PATTERN.test(topic)) {
|
|
77
114
|
deps.io.err(
|
|
78
115
|
`"${topic}" is not a topic name. Use lowercase letters, digits, "-" and "_", e.g. ${SHIPPED_GUIDANCE.map((t) => t.name).join(', ')}.`,
|
|
@@ -88,9 +125,9 @@ async function guidanceMutationTarget(
|
|
|
88
125
|
deps.io.select
|
|
89
126
|
) {
|
|
90
127
|
const selected = await deps.io.select('Where should this guidance live?', [
|
|
91
|
-
{ value: 'global', label: 'This machine', hint: 'applies across projects' },
|
|
92
|
-
{ value: 'project', label: 'This project (shared)', hint: '.notifai/guidance/' },
|
|
93
|
-
{ value: 'local', label: 'This project (personal)', hint: '
|
|
128
|
+
{ value: 'global', label: 'This machine', hint: 'your standing word, applies across projects' },
|
|
129
|
+
{ value: 'project', label: 'This project (shared)', hint: '.notifai/guidance/ — committed, read as project policy' },
|
|
130
|
+
{ value: 'local', label: 'This project (personal)', hint: 'your standing word here, stored outside the repo' },
|
|
94
131
|
])
|
|
95
132
|
if (selected === null) {
|
|
96
133
|
deps.io.err('No guidance layer selected.')
|
|
@@ -98,7 +135,7 @@ async function guidanceMutationTarget(
|
|
|
98
135
|
}
|
|
99
136
|
layer = selected as GuidanceLayer
|
|
100
137
|
}
|
|
101
|
-
return path.join(guidanceLayerDir(deps, layer), `${topic}.md`)
|
|
138
|
+
return { path: path.join(guidanceLayerDir(deps, layer), `${topic}.md`), layer }
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
/**
|
|
@@ -130,19 +167,28 @@ export async function guidanceSetCommand(
|
|
|
130
167
|
shipped === undefined
|
|
131
168
|
? `Add guidance topic "${topic}"`
|
|
132
169
|
: `Replace the shipped "${topic}" guidance for this scope`
|
|
133
|
-
const confirmed = await deps.io.confirm(`${replaces} at ${target}?`)
|
|
170
|
+
const confirmed = await deps.io.confirm(`${replaces} at ${target.path}?`)
|
|
134
171
|
if (!confirmed) {
|
|
135
172
|
deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
|
|
136
173
|
return EXIT.usage
|
|
137
174
|
}
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
mkdirSync(path.dirname(target), { recursive: true })
|
|
141
|
-
writeFileSync(target, text.endsWith('\n') ? text : `${text}\n`)
|
|
142
|
-
deps.io.out(`Wrote ${topic} to ${target}`)
|
|
177
|
+
mkdirSync(path.dirname(target.path), { recursive: true })
|
|
178
|
+
writeFileSync(target.path, text.endsWith('\n') ? text : `${text}\n`)
|
|
179
|
+
deps.io.out(`Wrote ${topic} to ${target.path}`)
|
|
143
180
|
if (shipped !== undefined) {
|
|
144
181
|
deps.io.out(`This file now replaces the shipped "${topic}" guidance wherever it applies.`)
|
|
145
182
|
}
|
|
183
|
+
// A shared file is committed and travels to everyone who clones this
|
|
184
|
+
// repository, where it is read as the project's policy rather than as
|
|
185
|
+
// anyone's personal standing word. Worth saying at the moment of writing:
|
|
186
|
+
// the same sentence means something different in the two locations.
|
|
187
|
+
if (target.layer === 'project') {
|
|
188
|
+
deps.io.out(
|
|
189
|
+
'This is shared project policy: it is committed, everyone working here reads it, and it is never read as the User\'s own standing word. Use --local for a personal preference.',
|
|
190
|
+
)
|
|
191
|
+
}
|
|
146
192
|
return EXIT.ok
|
|
147
193
|
}
|
|
148
194
|
|
|
@@ -153,18 +199,18 @@ export async function guidanceUnsetCommand(
|
|
|
153
199
|
): Promise<number> {
|
|
154
200
|
const target = await guidanceMutationTarget(deps, topic, flags)
|
|
155
201
|
if (target === null) return EXIT.usage
|
|
156
|
-
if (!existsSync(target)) {
|
|
157
|
-
deps.io.out(`${topic} is not set at ${target}`)
|
|
202
|
+
if (!existsSync(target.path)) {
|
|
203
|
+
deps.io.out(`${topic} is not set at ${target.path}`)
|
|
158
204
|
return EXIT.ok
|
|
159
205
|
}
|
|
160
206
|
if (!flags.yes) {
|
|
161
|
-
const confirmed = await deps.io.confirm(`Remove ${target}?`)
|
|
207
|
+
const confirmed = await deps.io.confirm(`Remove ${target.path}?`)
|
|
162
208
|
if (!confirmed) {
|
|
163
209
|
deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
|
|
164
210
|
return EXIT.usage
|
|
165
211
|
}
|
|
166
212
|
}
|
|
167
|
-
rmSync(target, { force: true })
|
|
168
|
-
deps.io.out(`Removed ${target}; the inherited guidance now applies.`)
|
|
213
|
+
rmSync(target.path, { force: true })
|
|
214
|
+
deps.io.out(`Removed ${target.path}; the inherited guidance now applies.`)
|
|
169
215
|
return EXIT.ok
|
|
170
216
|
}
|