pi-usereq 0.12.0 → 0.32.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/.github/workflows/release-npm.yml +13 -6
- package/.gitignore +1 -2
- package/CHANGELOG.md +145 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +629 -396
- package/pi-usereq/docs/REQUIREMENTS.md +70 -46
- package/pi-usereq/docs/WORKFLOW.md +83 -42
- package/src/core/config.ts +553 -180
- package/src/core/debug-runtime.ts +24 -0
- package/src/core/extension-status.ts +2 -2
- package/src/core/path-context.ts +19 -4
- package/src/index.ts +248 -44
- package/tests/attended-results-scenarios.ts +10 -4
- package/tests/cli-command-option-parity.test.ts +28 -10
- package/tests/debug-extension-harness.test.ts +7 -9
- package/tests/extension-registration.test.ts +314 -123
- package/tests/helpers.ts +29 -6
- package/tests/release-workflow.test.ts +3 -2
- package/scripts/lib/extension-debug-harness.ts +0 -443
- package/scripts/lib/recording-extension-api.ts +0 -855
- package/scripts/lib/sdk-smoke.ts +0 -503
package/src/core/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file
|
|
3
|
-
* @brief Loads, normalizes, and persists pi-usereq
|
|
4
|
-
* @details Defines the configuration schema,
|
|
3
|
+
* @brief Loads, normalizes, merges, and persists pi-usereq configuration.
|
|
4
|
+
* @details Defines the effective configuration schema, split local/global persistence contracts, JSON serialization helpers, and prompt placeholder expansion paths. Runtime is dominated by filesystem reads and writes plus linear normalization over configured entries. Side effects include config-file persistence under `.pi-usereq.json` and `~/.config/pi-usereq/config.json`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
buildRuntimePathFacts,
|
|
13
13
|
formatRuntimePathForDisplay,
|
|
14
14
|
getConfigPath,
|
|
15
|
+
getGlobalConfigPath as resolveGlobalConfigPath,
|
|
15
16
|
normalizeRelativeDirContract,
|
|
16
17
|
} from "./path-context.js";
|
|
17
18
|
import {
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
DEFAULT_DEBUG_LOG_FILE,
|
|
36
37
|
DEFAULT_DEBUG_LOG_ON_STATUS,
|
|
37
38
|
DEFAULT_DEBUG_STATUS_CHANGES,
|
|
39
|
+
DEFAULT_DEBUG_TOOL_COMMANDS_ENABLED,
|
|
38
40
|
DEFAULT_DEBUG_WORKFLOW_EVENTS,
|
|
39
41
|
normalizeDebugEnabled,
|
|
40
42
|
normalizeDebugEnabledPrompts,
|
|
@@ -42,7 +44,9 @@ import {
|
|
|
42
44
|
normalizeDebugLogFile,
|
|
43
45
|
normalizeDebugLogOnStatus,
|
|
44
46
|
normalizeDebugStatusChanges,
|
|
47
|
+
normalizeDebugToolCommandsEnabled,
|
|
45
48
|
normalizeDebugWorkflowEvents,
|
|
49
|
+
type DebugToolCommandsEnabled,
|
|
46
50
|
} from "./debug-runtime.js";
|
|
47
51
|
import { normalizeEnabledPiUsereqTools } from "./pi-usereq-tools.js";
|
|
48
52
|
import { makeRelativeIfContainsProject } from "./utils.js";
|
|
@@ -73,8 +77,8 @@ export interface StaticCheckLanguageConfig {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
76
|
-
* @brief Defines the
|
|
77
|
-
* @details Captures documentation paths, source/test directory selection, per-language static-check enablement
|
|
80
|
+
* @brief Defines the effective pi-usereq configuration schema.
|
|
81
|
+
* @details Captures the merged runtime view produced from project-local and cross-project persisted scopes, including documentation paths, source/test directory selection, per-language static-check enablement plus checker configuration, prompt-command worktree settings, enabled startup tools, and notification settings while excluding runtime-derived path metadata. The interface is compile-time only and introduces no runtime side effects.
|
|
78
82
|
*/
|
|
79
83
|
export interface UseReqConfig {
|
|
80
84
|
"docs-dir": string;
|
|
@@ -89,6 +93,7 @@ export interface UseReqConfig {
|
|
|
89
93
|
DEBUG_LOG_FILE: string;
|
|
90
94
|
DEBUG_STATUS_CHANGES: "enable" | "disable";
|
|
91
95
|
DEBUG_WORKFLOW_EVENTS: "enable" | "disable";
|
|
96
|
+
DEBUG_TOOL_COMMANDS_ENABLED: DebugToolCommandsEnabled;
|
|
92
97
|
DEBUG_LOG_ON_STATUS: "any" | "idle" | "checking" | "running" | "merging" | "error";
|
|
93
98
|
DEBUG_ENABLED_TOOLS: string[];
|
|
94
99
|
DEBUG_ENABLED_PROMPTS: string[];
|
|
@@ -116,6 +121,75 @@ export interface UseReqConfig {
|
|
|
116
121
|
PI_NOTIFY_SOUND_HIGH_CMD: string;
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @brief Defines one persisted local static-check language configuration.
|
|
126
|
+
* @details Stores only the project-local enable flag so checker command definitions can live exclusively in global configuration. The interface is compile-time only and introduces no runtime cost.
|
|
127
|
+
*/
|
|
128
|
+
interface LocalStaticCheckLanguageConfig {
|
|
129
|
+
enabled: StaticCheckEnabled;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @brief Defines one persisted global static-check language configuration.
|
|
134
|
+
* @details Stores only the cross-project checker-entry array so project-local files do not duplicate command definitions. The interface is compile-time only and introduces no runtime cost.
|
|
135
|
+
*/
|
|
136
|
+
interface GlobalStaticCheckLanguageConfig {
|
|
137
|
+
checkers: StaticCheckEntry[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @brief Defines the persisted local pi-usereq configuration schema.
|
|
142
|
+
* @details Captures project-scoped directory, debug, and static-check enablement fields written to `<base-path>/.pi-usereq.json` while excluding global notification, tool, git, and checker-command settings. The interface is compile-time only and introduces no runtime side effects.
|
|
143
|
+
*/
|
|
144
|
+
interface UseReqLocalConfig {
|
|
145
|
+
"docs-dir": string;
|
|
146
|
+
"tests-dir": string;
|
|
147
|
+
"src-dir": string[];
|
|
148
|
+
"static-check": Record<string, LocalStaticCheckLanguageConfig>;
|
|
149
|
+
DEBUG_ENABLED: "enable" | "disable";
|
|
150
|
+
DEBUG_LOG_FILE: string;
|
|
151
|
+
DEBUG_STATUS_CHANGES: "enable" | "disable";
|
|
152
|
+
DEBUG_WORKFLOW_EVENTS: "enable" | "disable";
|
|
153
|
+
DEBUG_TOOL_COMMANDS_ENABLED: DebugToolCommandsEnabled;
|
|
154
|
+
DEBUG_LOG_ON_STATUS: "any" | "idle" | "checking" | "running" | "merging" | "error";
|
|
155
|
+
DEBUG_ENABLED_TOOLS: string[];
|
|
156
|
+
DEBUG_ENABLED_PROMPTS: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @brief Defines the persisted global pi-usereq configuration schema.
|
|
161
|
+
* @details Captures cross-project static-check checker commands, enabled tool names, git automation fields, and notification settings written to `~/.config/pi-usereq/config.json` while excluding project-local directory and debug fields. The interface is compile-time only and introduces no runtime side effects.
|
|
162
|
+
*/
|
|
163
|
+
interface UseReqGlobalConfig {
|
|
164
|
+
"static-check": Record<string, GlobalStaticCheckLanguageConfig>;
|
|
165
|
+
"enabled-tools": string[];
|
|
166
|
+
AUTO_GIT_COMMIT: "enable" | "disable";
|
|
167
|
+
GIT_WORKTREE_ENABLED: "enable" | "disable";
|
|
168
|
+
GIT_WORKTREE_PREFIX: string;
|
|
169
|
+
"notify-enabled": boolean;
|
|
170
|
+
"notify-on-completed": boolean;
|
|
171
|
+
"notify-on-interrupted": boolean;
|
|
172
|
+
"notify-on-failed": boolean;
|
|
173
|
+
"notify-sound": "none" | "low" | "mid" | "high";
|
|
174
|
+
"notify-sound-on-completed": boolean;
|
|
175
|
+
"notify-sound-on-interrupted": boolean;
|
|
176
|
+
"notify-sound-on-failed": boolean;
|
|
177
|
+
"notify-sound-toggle-shortcut": string;
|
|
178
|
+
"notify-pushover-enabled": boolean;
|
|
179
|
+
"notify-pushover-on-completed": boolean;
|
|
180
|
+
"notify-pushover-on-interrupted": boolean;
|
|
181
|
+
"notify-pushover-on-failed": boolean;
|
|
182
|
+
"notify-pushover-user-key": string;
|
|
183
|
+
"notify-pushover-api-token": string;
|
|
184
|
+
"notify-pushover-priority": 0 | 1;
|
|
185
|
+
"notify-pushover-title": string;
|
|
186
|
+
"notify-pushover-text": string;
|
|
187
|
+
PI_NOTIFY_CMD: string;
|
|
188
|
+
PI_NOTIFY_SOUND_LOW_CMD: string;
|
|
189
|
+
PI_NOTIFY_SOUND_MID_CMD: string;
|
|
190
|
+
PI_NOTIFY_SOUND_HIGH_CMD: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
119
193
|
/**
|
|
120
194
|
* @brief Defines the default documentation directory relative to the project root.
|
|
121
195
|
* @details Used when no persisted `docs-dir` value exists or normalization yields an empty string. Lookup complexity is O(1).
|
|
@@ -261,18 +335,102 @@ export function createStaticCheckLanguageConfig(
|
|
|
261
335
|
}
|
|
262
336
|
|
|
263
337
|
/**
|
|
264
|
-
* @brief
|
|
265
|
-
* @details
|
|
266
|
-
* @
|
|
267
|
-
* @
|
|
338
|
+
* @brief Builds one persisted local static-check language configuration object.
|
|
339
|
+
* @details Stores only the normalized enable flag so local project files do not duplicate checker command definitions. Runtime is O(1). No external state is mutated.
|
|
340
|
+
* @param[in] enabled {StaticCheckEnabled} Canonical per-language enable flag.
|
|
341
|
+
* @return {LocalStaticCheckLanguageConfig} Persistable local static-check language config.
|
|
268
342
|
*/
|
|
269
|
-
|
|
343
|
+
function createLocalStaticCheckLanguageConfig(
|
|
344
|
+
enabled: StaticCheckEnabled,
|
|
345
|
+
): LocalStaticCheckLanguageConfig {
|
|
346
|
+
return { enabled };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @brief Builds one persisted global static-check language configuration object.
|
|
351
|
+
* @details Clones the supplied checker entries so global configuration retains only stable module, command, and parameter fields. Runtime is O(c + p). No external state is mutated.
|
|
352
|
+
* @param[in] checkers {StaticCheckEntry[]} Ordered checker entries.
|
|
353
|
+
* @return {GlobalStaticCheckLanguageConfig} Persistable global static-check language config.
|
|
354
|
+
*/
|
|
355
|
+
function createGlobalStaticCheckLanguageConfig(
|
|
356
|
+
checkers: StaticCheckEntry[],
|
|
357
|
+
): GlobalStaticCheckLanguageConfig {
|
|
358
|
+
return {
|
|
359
|
+
checkers: checkers.map(cloneStaticCheckEntry),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @brief Returns the documented default global static-check checker map.
|
|
365
|
+
* @details Emits one checker-array object for every supported language, preserving documented Command entries for default-enabled languages and `[]` for every other language. Runtime is O(l + c). No external state is mutated.
|
|
366
|
+
* @return {Record<string, GlobalStaticCheckLanguageConfig>} Fresh default global checker map.
|
|
367
|
+
*/
|
|
368
|
+
function getDefaultGlobalStaticCheckConfig(): Record<string, GlobalStaticCheckLanguageConfig> {
|
|
270
369
|
return Object.fromEntries(DEFAULT_STATIC_CHECK_LANGUAGES.map((language) => [
|
|
271
370
|
language,
|
|
272
|
-
|
|
371
|
+
createGlobalStaticCheckLanguageConfig(DEFAULT_STATIC_CHECK_CHECKERS[language] ?? []),
|
|
372
|
+
]));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @brief Returns the documented default local static-check enable map.
|
|
377
|
+
* @details Derives each per-language enable flag from the supplied global checker definitions using the rule `enable` when `checkers` is non-empty and `disable` otherwise. Runtime is O(l). No external state is mutated.
|
|
378
|
+
* @param[in] globalStaticCheckConfig {Record<string, GlobalStaticCheckLanguageConfig>} Global checker map used to derive default enablement.
|
|
379
|
+
* @return {Record<string, LocalStaticCheckLanguageConfig>} Fresh default local enable map.
|
|
380
|
+
*/
|
|
381
|
+
function getDefaultLocalStaticCheckConfig(
|
|
382
|
+
globalStaticCheckConfig: Record<string, GlobalStaticCheckLanguageConfig>,
|
|
383
|
+
): Record<string, LocalStaticCheckLanguageConfig> {
|
|
384
|
+
return Object.fromEntries(Object.entries(globalStaticCheckConfig).map(([language, languageConfig]) => [
|
|
385
|
+
language,
|
|
386
|
+
createLocalStaticCheckLanguageConfig(
|
|
387
|
+
languageConfig.checkers.length > 0 ? "enable" : "disable",
|
|
388
|
+
),
|
|
273
389
|
]));
|
|
274
390
|
}
|
|
275
391
|
|
|
392
|
+
/**
|
|
393
|
+
* @brief Merges local enable flags with global checker definitions into the effective static-check map.
|
|
394
|
+
* @details Unifies all language keys present in either persisted scope, applies the default-enable rule when the local scope omits a language, and clones checker entries into the effective runtime config. Runtime is O(l + c + p). No external state is mutated.
|
|
395
|
+
* @param[in] localStaticCheckConfig {Record<string, LocalStaticCheckLanguageConfig>} Persisted local enable map.
|
|
396
|
+
* @param[in] globalStaticCheckConfig {Record<string, GlobalStaticCheckLanguageConfig>} Persisted global checker map.
|
|
397
|
+
* @return {Record<string, StaticCheckLanguageConfig>} Effective per-language static-check config.
|
|
398
|
+
*/
|
|
399
|
+
function mergeStaticCheckConfig(
|
|
400
|
+
localStaticCheckConfig: Record<string, LocalStaticCheckLanguageConfig>,
|
|
401
|
+
globalStaticCheckConfig: Record<string, GlobalStaticCheckLanguageConfig>,
|
|
402
|
+
): Record<string, StaticCheckLanguageConfig> {
|
|
403
|
+
const languages = new Set<string>([
|
|
404
|
+
...Object.keys(globalStaticCheckConfig),
|
|
405
|
+
...Object.keys(localStaticCheckConfig),
|
|
406
|
+
]);
|
|
407
|
+
return Object.fromEntries([...languages].map((language) => {
|
|
408
|
+
const globalLanguageConfig = globalStaticCheckConfig[language]
|
|
409
|
+
?? createGlobalStaticCheckLanguageConfig([]);
|
|
410
|
+
const checkers = globalLanguageConfig.checkers.map(cloneStaticCheckEntry);
|
|
411
|
+
const defaultEnabled = checkers.length > 0 ? "enable" : "disable";
|
|
412
|
+
const enabled = normalizeStaticCheckEnabled(
|
|
413
|
+
localStaticCheckConfig[language]?.enabled,
|
|
414
|
+
defaultEnabled,
|
|
415
|
+
);
|
|
416
|
+
return [language, createStaticCheckLanguageConfig(checkers, enabled)];
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @brief Returns the documented default static-check configuration.
|
|
422
|
+
* @details Emits one effective per-language config object for every supported language by merging documented global checker defaults with derived local enable defaults. Runtime is O(l + c). No external state is mutated.
|
|
423
|
+
* @return {Record<string, StaticCheckLanguageConfig>} Fresh default effective static-check config.
|
|
424
|
+
* @satisfies REQ-249, REQ-250, REQ-251, REQ-252, REQ-316
|
|
425
|
+
*/
|
|
426
|
+
export function getDefaultStaticCheckConfig(): Record<string, StaticCheckLanguageConfig> {
|
|
427
|
+
const globalStaticCheckConfig = getDefaultGlobalStaticCheckConfig();
|
|
428
|
+
return mergeStaticCheckConfig(
|
|
429
|
+
getDefaultLocalStaticCheckConfig(globalStaticCheckConfig),
|
|
430
|
+
globalStaticCheckConfig,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
276
434
|
/**
|
|
277
435
|
* @brief Normalizes one persisted per-language static-check enable flag.
|
|
278
436
|
* @details Accepts only the documented `enable|disable` values and falls back to the supplied default when the candidate is absent or invalid. Runtime is O(1). No external state is mutated.
|
|
@@ -309,8 +467,8 @@ export function getActiveStaticCheckEntries(
|
|
|
309
467
|
|
|
310
468
|
/**
|
|
311
469
|
* @brief Defines the default automatic git-commit prompt mode.
|
|
312
|
-
* @details New
|
|
313
|
-
* @satisfies CTN-
|
|
470
|
+
* @details New global configs enable bundled commit-instruction injection unless the persisted cross-project config explicitly disables it. Access complexity is O(1).
|
|
471
|
+
* @satisfies CTN-018, REQ-212
|
|
314
472
|
*/
|
|
315
473
|
export const DEFAULT_AUTO_GIT_COMMIT = "enable" as const;
|
|
316
474
|
/**
|
|
@@ -325,8 +483,8 @@ export function normalizeAutoGitCommit(value: unknown): "enable" | "disable" {
|
|
|
325
483
|
}
|
|
326
484
|
/**
|
|
327
485
|
* @brief Defines the default worktree orchestration mode.
|
|
328
|
-
* @details New
|
|
329
|
-
* @satisfies CTN-
|
|
486
|
+
* @details New global configs enable prompt-command worktree orchestration unless the persisted cross-project config explicitly disables it. Access complexity is O(1).
|
|
487
|
+
* @satisfies CTN-018, REQ-204
|
|
330
488
|
*/
|
|
331
489
|
export const DEFAULT_GIT_WORKTREE_ENABLED = "enable" as const;
|
|
332
490
|
/**
|
|
@@ -355,8 +513,8 @@ export function resolveEffectiveGitWorktreeEnabled(
|
|
|
355
513
|
}
|
|
356
514
|
/**
|
|
357
515
|
* @brief Defines the default static prefix used by generated prompt-command worktree names.
|
|
358
|
-
* @details The prefix is concatenated verbatim ahead of the repository basename inside slash-command-owned worktree-name generation. Access complexity is O(1).
|
|
359
|
-
* @satisfies CTN-
|
|
516
|
+
* @details The prefix is concatenated verbatim ahead of the repository basename inside slash-command-owned worktree-name generation and is persisted in global configuration. Access complexity is O(1).
|
|
517
|
+
* @satisfies CTN-018, REQ-205
|
|
360
518
|
*/
|
|
361
519
|
export const DEFAULT_GIT_WORKTREE_PREFIX = "PI-useReq-";
|
|
362
520
|
/**
|
|
@@ -374,39 +532,62 @@ export function normalizeGitWorktreePrefix(value: unknown): string {
|
|
|
374
532
|
return trimmedValue === "" ? DEFAULT_GIT_WORKTREE_PREFIX : trimmedValue;
|
|
375
533
|
}
|
|
376
534
|
/**
|
|
377
|
-
* @brief Computes the per-project config file path.
|
|
378
|
-
* @details Joins the project base with `.pi-usereq.json`, producing the canonical persistence location used by CLI and extension code. Time complexity is O(1). No I/O side effects occur.
|
|
535
|
+
* @brief Computes the per-project local config file path.
|
|
536
|
+
* @details Joins the project base with `.pi-usereq.json`, producing the canonical local persistence location used by CLI and extension code. Time complexity is O(1). No I/O side effects occur.
|
|
379
537
|
* @param[in] projectBase {string} Absolute project root path.
|
|
380
|
-
* @return {string} Absolute config file path.
|
|
538
|
+
* @return {string} Absolute local config file path.
|
|
381
539
|
*/
|
|
382
540
|
export function getProjectConfigPath(projectBase: string): string {
|
|
383
541
|
return getConfigPath(projectBase);
|
|
384
542
|
}
|
|
385
543
|
|
|
386
544
|
/**
|
|
387
|
-
* @brief
|
|
388
|
-
* @details
|
|
389
|
-
* @
|
|
390
|
-
* @return {UseReqConfig} Fresh default configuration object.
|
|
391
|
-
* @satisfies CTN-001, CTN-012, CTN-013, REQ-066, REQ-146, REQ-163, REQ-174, REQ-178, REQ-184, REQ-185, REQ-196, REQ-204, REQ-205, REQ-212, REQ-236, REQ-237, REQ-238, REQ-239, REQ-249, REQ-250, REQ-251, REQ-252, REQ-277
|
|
545
|
+
* @brief Computes the cross-project global config file path.
|
|
546
|
+
* @details Resolves `~/.config/pi-usereq/config.json` through the shared runtime path helper so CLI and extension code use one canonical global persistence location. Time complexity is O(1). No I/O side effects occur.
|
|
547
|
+
* @return {string} Absolute global config file path.
|
|
392
548
|
*/
|
|
393
|
-
export function
|
|
549
|
+
export function getGlobalConfigPath(): string {
|
|
550
|
+
return resolveGlobalConfigPath();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* @brief Builds the default persisted local configuration.
|
|
555
|
+
* @details Populates canonical docs/test/source directories, derives local static-check enable defaults from the supplied global checker definitions, and seeds documented debug defaults including tool-wrapper command registration without any cross-project fields. Runtime is O(l). No filesystem side effects occur.
|
|
556
|
+
* @param[in] globalStaticCheckConfig {Record<string, GlobalStaticCheckLanguageConfig>} Global checker definitions used to derive local enable defaults.
|
|
557
|
+
* @return {UseReqLocalConfig} Fresh default local configuration object.
|
|
558
|
+
* @satisfies CTN-019
|
|
559
|
+
*/
|
|
560
|
+
function getDefaultLocalConfig(
|
|
561
|
+
globalStaticCheckConfig: Record<string, GlobalStaticCheckLanguageConfig>,
|
|
562
|
+
): UseReqLocalConfig {
|
|
394
563
|
return {
|
|
395
564
|
"docs-dir": DEFAULT_DOCS_DIR,
|
|
396
565
|
"tests-dir": DEFAULT_TESTS_DIR,
|
|
397
566
|
"src-dir": [...DEFAULT_SRC_DIRS],
|
|
398
|
-
"static-check":
|
|
399
|
-
"enabled-tools": normalizeEnabledPiUsereqTools(undefined),
|
|
400
|
-
AUTO_GIT_COMMIT: DEFAULT_AUTO_GIT_COMMIT,
|
|
401
|
-
GIT_WORKTREE_ENABLED: DEFAULT_GIT_WORKTREE_ENABLED,
|
|
402
|
-
GIT_WORKTREE_PREFIX: DEFAULT_GIT_WORKTREE_PREFIX,
|
|
567
|
+
"static-check": getDefaultLocalStaticCheckConfig(globalStaticCheckConfig),
|
|
403
568
|
DEBUG_ENABLED: DEFAULT_DEBUG_ENABLED,
|
|
404
569
|
DEBUG_LOG_FILE: DEFAULT_DEBUG_LOG_FILE,
|
|
405
570
|
DEBUG_STATUS_CHANGES: DEFAULT_DEBUG_STATUS_CHANGES,
|
|
406
571
|
DEBUG_WORKFLOW_EVENTS: DEFAULT_DEBUG_WORKFLOW_EVENTS,
|
|
572
|
+
DEBUG_TOOL_COMMANDS_ENABLED: DEFAULT_DEBUG_TOOL_COMMANDS_ENABLED,
|
|
407
573
|
DEBUG_LOG_ON_STATUS: DEFAULT_DEBUG_LOG_ON_STATUS,
|
|
408
574
|
DEBUG_ENABLED_TOOLS: [],
|
|
409
575
|
DEBUG_ENABLED_PROMPTS: [],
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @brief Builds the default persisted global configuration.
|
|
581
|
+
* @details Populates documented cross-project static-check checker commands, enabled tools, git automation fields, and notification defaults without any project-local directory or debug fields. Runtime is O(l + c). No filesystem side effects occur.
|
|
582
|
+
* @return {UseReqGlobalConfig} Fresh default global configuration object.
|
|
583
|
+
*/
|
|
584
|
+
function getDefaultGlobalConfig(): UseReqGlobalConfig {
|
|
585
|
+
return {
|
|
586
|
+
"static-check": getDefaultGlobalStaticCheckConfig(),
|
|
587
|
+
"enabled-tools": normalizeEnabledPiUsereqTools(undefined),
|
|
588
|
+
AUTO_GIT_COMMIT: DEFAULT_AUTO_GIT_COMMIT,
|
|
589
|
+
GIT_WORKTREE_ENABLED: DEFAULT_GIT_WORKTREE_ENABLED,
|
|
590
|
+
GIT_WORKTREE_PREFIX: DEFAULT_GIT_WORKTREE_PREFIX,
|
|
410
591
|
"notify-enabled": false,
|
|
411
592
|
"notify-on-completed": true,
|
|
412
593
|
"notify-on-interrupted": false,
|
|
@@ -432,6 +613,97 @@ export function getDefaultConfig(_projectBase: string): UseReqConfig {
|
|
|
432
613
|
};
|
|
433
614
|
}
|
|
434
615
|
|
|
616
|
+
/**
|
|
617
|
+
* @brief Merges persisted local and global configuration scopes into the effective runtime config.
|
|
618
|
+
* @details Normalizes local directories, combines local static-check enable flags with global checker arrays, resolves effective worktree disablement when automatic git commit is off, normalizes debug and notification fields including tool-wrapper command registration, and disables Pushover until both credentials are populated. Runtime is O(l + c + p). No external state is mutated.
|
|
619
|
+
* @param[in] localConfig {UseReqLocalConfig} Persisted local configuration.
|
|
620
|
+
* @param[in] globalConfig {UseReqGlobalConfig} Persisted global configuration.
|
|
621
|
+
* @return {UseReqConfig} Effective merged configuration.
|
|
622
|
+
*/
|
|
623
|
+
function mergeConfigScopes(
|
|
624
|
+
localConfig: UseReqLocalConfig,
|
|
625
|
+
globalConfig: UseReqGlobalConfig,
|
|
626
|
+
): UseReqConfig {
|
|
627
|
+
const docsDir = normalizeRelativeDirContract(localConfig["docs-dir"]) || DEFAULT_DOCS_DIR;
|
|
628
|
+
const testsDir = normalizeRelativeDirContract(localConfig["tests-dir"]) || DEFAULT_TESTS_DIR;
|
|
629
|
+
const srcDir = localConfig["src-dir"]
|
|
630
|
+
.map((entry) => normalizeRelativeDirContract(entry))
|
|
631
|
+
.filter((entry) => entry !== "");
|
|
632
|
+
const autoGitCommit = normalizeAutoGitCommit(globalConfig.AUTO_GIT_COMMIT);
|
|
633
|
+
const gitWorktreeEnabled = resolveEffectiveGitWorktreeEnabled(
|
|
634
|
+
autoGitCommit,
|
|
635
|
+
normalizeGitWorktreeEnabled(globalConfig.GIT_WORKTREE_ENABLED),
|
|
636
|
+
);
|
|
637
|
+
const pushoverUserKey = normalizePiNotifyPushoverCredential(globalConfig["notify-pushover-user-key"]);
|
|
638
|
+
const pushoverApiToken = normalizePiNotifyPushoverCredential(globalConfig["notify-pushover-api-token"]);
|
|
639
|
+
const pushoverEnabled = globalConfig["notify-pushover-enabled"] === true
|
|
640
|
+
&& hasPiNotifyPushoverCredentials({
|
|
641
|
+
"notify-pushover-user-key": pushoverUserKey,
|
|
642
|
+
"notify-pushover-api-token": pushoverApiToken,
|
|
643
|
+
});
|
|
644
|
+
return {
|
|
645
|
+
"docs-dir": docsDir,
|
|
646
|
+
"tests-dir": testsDir,
|
|
647
|
+
"src-dir": srcDir.length > 0 ? srcDir : [...DEFAULT_SRC_DIRS],
|
|
648
|
+
"static-check": mergeStaticCheckConfig(localConfig["static-check"], globalConfig["static-check"]),
|
|
649
|
+
"enabled-tools": normalizeEnabledPiUsereqTools(globalConfig["enabled-tools"]),
|
|
650
|
+
AUTO_GIT_COMMIT: autoGitCommit,
|
|
651
|
+
GIT_WORKTREE_ENABLED: gitWorktreeEnabled,
|
|
652
|
+
GIT_WORKTREE_PREFIX: normalizeGitWorktreePrefix(globalConfig.GIT_WORKTREE_PREFIX),
|
|
653
|
+
DEBUG_ENABLED: normalizeDebugEnabled(localConfig.DEBUG_ENABLED),
|
|
654
|
+
DEBUG_LOG_FILE: normalizeDebugLogFile(localConfig.DEBUG_LOG_FILE),
|
|
655
|
+
DEBUG_STATUS_CHANGES: normalizeDebugStatusChanges(localConfig.DEBUG_STATUS_CHANGES),
|
|
656
|
+
DEBUG_WORKFLOW_EVENTS: normalizeDebugWorkflowEvents(localConfig.DEBUG_WORKFLOW_EVENTS),
|
|
657
|
+
DEBUG_TOOL_COMMANDS_ENABLED: normalizeDebugToolCommandsEnabled(localConfig.DEBUG_TOOL_COMMANDS_ENABLED),
|
|
658
|
+
DEBUG_LOG_ON_STATUS: normalizeDebugLogOnStatus(localConfig.DEBUG_LOG_ON_STATUS),
|
|
659
|
+
DEBUG_ENABLED_TOOLS: normalizeDebugEnabledTools(localConfig.DEBUG_ENABLED_TOOLS),
|
|
660
|
+
DEBUG_ENABLED_PROMPTS: normalizeDebugEnabledPrompts(localConfig.DEBUG_ENABLED_PROMPTS),
|
|
661
|
+
"notify-enabled": globalConfig["notify-enabled"] === true,
|
|
662
|
+
"notify-on-completed": globalConfig["notify-on-completed"] !== false,
|
|
663
|
+
"notify-on-interrupted": globalConfig["notify-on-interrupted"] === true,
|
|
664
|
+
"notify-on-failed": globalConfig["notify-on-failed"] === true,
|
|
665
|
+
"notify-sound": normalizePiNotifySoundLevel(globalConfig["notify-sound"]),
|
|
666
|
+
"notify-sound-on-completed": globalConfig["notify-sound-on-completed"] !== false,
|
|
667
|
+
"notify-sound-on-interrupted": globalConfig["notify-sound-on-interrupted"] === true,
|
|
668
|
+
"notify-sound-on-failed": globalConfig["notify-sound-on-failed"] === true,
|
|
669
|
+
"notify-sound-toggle-shortcut": normalizePiNotifyShortcut(globalConfig["notify-sound-toggle-shortcut"]),
|
|
670
|
+
"notify-pushover-enabled": pushoverEnabled,
|
|
671
|
+
"notify-pushover-on-completed": globalConfig["notify-pushover-on-completed"] !== false,
|
|
672
|
+
"notify-pushover-on-interrupted": globalConfig["notify-pushover-on-interrupted"] === true,
|
|
673
|
+
"notify-pushover-on-failed": globalConfig["notify-pushover-on-failed"] === true,
|
|
674
|
+
"notify-pushover-user-key": pushoverUserKey,
|
|
675
|
+
"notify-pushover-api-token": pushoverApiToken,
|
|
676
|
+
"notify-pushover-priority": normalizePiNotifyPushoverPriority(globalConfig["notify-pushover-priority"]),
|
|
677
|
+
"notify-pushover-title": normalizePiNotifyTemplateValue(
|
|
678
|
+
globalConfig["notify-pushover-title"],
|
|
679
|
+
DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
|
|
680
|
+
),
|
|
681
|
+
"notify-pushover-text": normalizePiNotifyTemplateValue(
|
|
682
|
+
globalConfig["notify-pushover-text"],
|
|
683
|
+
DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
|
|
684
|
+
),
|
|
685
|
+
PI_NOTIFY_CMD: normalizePiNotifyCommand(globalConfig.PI_NOTIFY_CMD, DEFAULT_PI_NOTIFY_CMD),
|
|
686
|
+
PI_NOTIFY_SOUND_LOW_CMD: normalizePiNotifyCommand(globalConfig.PI_NOTIFY_SOUND_LOW_CMD, DEFAULT_PI_NOTIFY_SOUND_LOW_CMD),
|
|
687
|
+
PI_NOTIFY_SOUND_MID_CMD: normalizePiNotifyCommand(globalConfig.PI_NOTIFY_SOUND_MID_CMD, DEFAULT_PI_NOTIFY_SOUND_MID_CMD),
|
|
688
|
+
PI_NOTIFY_SOUND_HIGH_CMD: normalizePiNotifyCommand(globalConfig.PI_NOTIFY_SOUND_HIGH_CMD, DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* @brief Builds the default effective configuration.
|
|
694
|
+
* @details Composes documented local and global defaults, then merges them into the effective runtime config consumed by CLI and extension code. Time complexity is O(l + c). No filesystem side effects occur.
|
|
695
|
+
* @param[in] _projectBase {string} Absolute project root path retained for stable call sites.
|
|
696
|
+
* @return {UseReqConfig} Fresh default effective configuration object.
|
|
697
|
+
* @satisfies CTN-001, CTN-012, CTN-013, CTN-018, CTN-019, REQ-066, REQ-137, REQ-146, REQ-163, REQ-174, REQ-178, REQ-184, REQ-185, REQ-196, REQ-204, REQ-205, REQ-212, REQ-236, REQ-237, REQ-238, REQ-239, REQ-249, REQ-250, REQ-251, REQ-252, REQ-277, REQ-315, REQ-316
|
|
698
|
+
*/
|
|
699
|
+
export function getDefaultConfig(_projectBase: string): UseReqConfig {
|
|
700
|
+
const globalConfig = getDefaultGlobalConfig();
|
|
701
|
+
return mergeConfigScopes(
|
|
702
|
+
getDefaultLocalConfig(globalConfig["static-check"]),
|
|
703
|
+
globalConfig,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
435
707
|
/**
|
|
436
708
|
* @brief Normalizes one raw checker-entry array from persisted config.
|
|
437
709
|
* @details Accepts only object entries with a non-empty module string, trims optional command text, filters blank params, and drops malformed records without applying any legacy schema migrations. Runtime is O(c + p). No external state is mutated.
|
|
@@ -461,58 +733,120 @@ function normalizeStaticCheckEntries(value: unknown): StaticCheckEntry[] {
|
|
|
461
733
|
}
|
|
462
734
|
|
|
463
735
|
/**
|
|
464
|
-
* @brief
|
|
465
|
-
* @details
|
|
466
|
-
* @param[in]
|
|
467
|
-
* @return {Record<string,
|
|
468
|
-
* @
|
|
736
|
+
* @brief Reads and validates one persisted config payload.
|
|
737
|
+
* @details Returns `undefined` when the target file does not exist. Otherwise parses UTF-8 JSON, rejects array or primitive payloads, and surfaces deterministic `ReqError` diagnostics keyed by the exact path. Runtime is O(n) in file size. Side effects are limited to filesystem reads.
|
|
738
|
+
* @param[in] configPath {string} Absolute config file path.
|
|
739
|
+
* @return {Record<string, unknown> | undefined} Parsed object payload or `undefined` when the file is absent.
|
|
740
|
+
* @throws {ReqError} Throws with exit code `11` when the config file contains invalid JSON or a non-object payload.
|
|
741
|
+
*/
|
|
742
|
+
function readConfigPayload(configPath: string): Record<string, unknown> | undefined {
|
|
743
|
+
if (!fs.existsSync(configPath)) {
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let payload: unknown;
|
|
748
|
+
try {
|
|
749
|
+
payload = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
750
|
+
} catch {
|
|
751
|
+
throw new ReqError(`Error: invalid ${configPath}`, 11);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
755
|
+
throw new ReqError(`Error: invalid ${configPath}`, 11);
|
|
756
|
+
}
|
|
757
|
+
return payload as Record<string, unknown>;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* @brief Normalizes the persisted local static-check enable map.
|
|
762
|
+
* @details Starts from the supplied default enable map, accepts only object-valued language entries, reads only `enabled`, and ignores misplaced checker arrays or legacy non-object payloads without migration. Runtime is O(l). No external state is mutated.
|
|
763
|
+
* @param[in] value {unknown} Candidate persisted local static-check payload.
|
|
764
|
+
* @param[in] defaultConfig {Record<string, LocalStaticCheckLanguageConfig>} Default enable map derived from global checker definitions.
|
|
765
|
+
* @return {Record<string, LocalStaticCheckLanguageConfig>} Normalized local static-check enable map.
|
|
766
|
+
* @satisfies REQ-249, REQ-316
|
|
469
767
|
*/
|
|
470
|
-
function
|
|
768
|
+
function normalizeLocalStaticCheckConfig(
|
|
471
769
|
value: unknown,
|
|
472
|
-
|
|
770
|
+
defaultConfig: Record<string, LocalStaticCheckLanguageConfig>,
|
|
771
|
+
): Record<string, LocalStaticCheckLanguageConfig> {
|
|
772
|
+
const config: Record<string, LocalStaticCheckLanguageConfig> = Object.fromEntries(
|
|
773
|
+
Object.entries(defaultConfig).map(([language, languageConfig]) => [
|
|
774
|
+
language,
|
|
775
|
+
createLocalStaticCheckLanguageConfig(languageConfig.enabled),
|
|
776
|
+
]),
|
|
777
|
+
);
|
|
473
778
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
474
|
-
return
|
|
779
|
+
return config;
|
|
475
780
|
}
|
|
476
|
-
const config: Record<string, StaticCheckLanguageConfig> = {};
|
|
477
781
|
for (const [language, languageValue] of Object.entries(value as Record<string, unknown>)) {
|
|
478
782
|
if (!languageValue || typeof languageValue !== "object" || Array.isArray(languageValue)) {
|
|
479
783
|
continue;
|
|
480
784
|
}
|
|
481
785
|
const languageRecord = languageValue as Record<string, unknown>;
|
|
482
|
-
const
|
|
483
|
-
config[language] =
|
|
484
|
-
|
|
485
|
-
normalizeStaticCheckEnabled(languageRecord.enabled, checkers.length > 0 ? "enable" : "disable"),
|
|
786
|
+
const defaultEnabled = config[language]?.enabled ?? "disable";
|
|
787
|
+
config[language] = createLocalStaticCheckLanguageConfig(
|
|
788
|
+
normalizeStaticCheckEnabled(languageRecord.enabled, defaultEnabled),
|
|
486
789
|
);
|
|
487
790
|
}
|
|
488
791
|
return config;
|
|
489
792
|
}
|
|
490
793
|
|
|
491
794
|
/**
|
|
492
|
-
* @brief
|
|
493
|
-
* @details
|
|
494
|
-
* @param[in]
|
|
495
|
-
* @return {
|
|
496
|
-
* @
|
|
497
|
-
* @satisfies CTN-012, CTN-013, REQ-066, REQ-146, REQ-163, REQ-174, REQ-178, REQ-184, REQ-185, REQ-196, REQ-204, REQ-205, REQ-212, REQ-215, REQ-234, REQ-235, REQ-236, REQ-237, REQ-238, REQ-239, REQ-249, REQ-277
|
|
795
|
+
* @brief Normalizes the persisted global static-check checker map.
|
|
796
|
+
* @details Starts from documented global checker defaults, accepts only object-valued language entries, reads only `checkers`, and ignores misplaced local enable flags or legacy non-object payloads without migration. Runtime is O(l + c + p). No external state is mutated.
|
|
797
|
+
* @param[in] value {unknown} Candidate persisted global static-check payload.
|
|
798
|
+
* @return {Record<string, GlobalStaticCheckLanguageConfig>} Normalized global static-check checker map.
|
|
799
|
+
* @satisfies REQ-249, REQ-250, REQ-251, REQ-252
|
|
498
800
|
*/
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
801
|
+
function normalizeGlobalStaticCheckConfig(
|
|
802
|
+
value: unknown,
|
|
803
|
+
): Record<string, GlobalStaticCheckLanguageConfig> {
|
|
804
|
+
const config = getDefaultGlobalStaticCheckConfig();
|
|
805
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
806
|
+
return config;
|
|
503
807
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
808
|
+
for (const [language, languageValue] of Object.entries(value as Record<string, unknown>)) {
|
|
809
|
+
if (!languageValue || typeof languageValue !== "object" || Array.isArray(languageValue)) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const languageRecord = languageValue as Record<string, unknown>;
|
|
813
|
+
config[language] = createGlobalStaticCheckLanguageConfig(
|
|
814
|
+
normalizeStaticCheckEntries(languageRecord.checkers),
|
|
815
|
+
);
|
|
510
816
|
}
|
|
817
|
+
return config;
|
|
818
|
+
}
|
|
511
819
|
|
|
512
|
-
|
|
513
|
-
|
|
820
|
+
/**
|
|
821
|
+
* @brief Loads and sanitizes the persisted local configuration.
|
|
822
|
+
* @details Returns defaults when `<base-path>/.pi-usereq.json` is absent. Otherwise parses the local JSON payload, normalizes project-scoped directory, debug, tool-wrapper command registration, and static-check enable fields, and ignores misplaced global keys without migration. Runtime is O(n) in file size. Side effects are limited to filesystem reads.
|
|
823
|
+
* @param[in] projectBase {string} Absolute project root path.
|
|
824
|
+
* @param[in] defaultStaticCheckConfig {Record<string, LocalStaticCheckLanguageConfig>} Local static-check enable defaults derived from the current global checker map.
|
|
825
|
+
* @return {UseReqLocalConfig} Sanitized local configuration.
|
|
826
|
+
* @satisfies CTN-019
|
|
827
|
+
*/
|
|
828
|
+
function loadLocalConfig(
|
|
829
|
+
projectBase: string,
|
|
830
|
+
defaultStaticCheckConfig: Record<string, LocalStaticCheckLanguageConfig>,
|
|
831
|
+
): UseReqLocalConfig {
|
|
832
|
+
const configPath = getProjectConfigPath(projectBase);
|
|
833
|
+
const data = readConfigPayload(configPath);
|
|
834
|
+
if (!data) {
|
|
835
|
+
return {
|
|
836
|
+
"docs-dir": DEFAULT_DOCS_DIR,
|
|
837
|
+
"tests-dir": DEFAULT_TESTS_DIR,
|
|
838
|
+
"src-dir": [...DEFAULT_SRC_DIRS],
|
|
839
|
+
"static-check": normalizeLocalStaticCheckConfig(undefined, defaultStaticCheckConfig),
|
|
840
|
+
DEBUG_ENABLED: DEFAULT_DEBUG_ENABLED,
|
|
841
|
+
DEBUG_LOG_FILE: DEFAULT_DEBUG_LOG_FILE,
|
|
842
|
+
DEBUG_STATUS_CHANGES: DEFAULT_DEBUG_STATUS_CHANGES,
|
|
843
|
+
DEBUG_WORKFLOW_EVENTS: DEFAULT_DEBUG_WORKFLOW_EVENTS,
|
|
844
|
+
DEBUG_TOOL_COMMANDS_ENABLED: DEFAULT_DEBUG_TOOL_COMMANDS_ENABLED,
|
|
845
|
+
DEBUG_LOG_ON_STATUS: DEFAULT_DEBUG_LOG_ON_STATUS,
|
|
846
|
+
DEBUG_ENABLED_TOOLS: [],
|
|
847
|
+
DEBUG_ENABLED_PROMPTS: [],
|
|
848
|
+
};
|
|
514
849
|
}
|
|
515
|
-
const data = payload as Record<string, unknown>;
|
|
516
850
|
const docsDirCandidate = typeof data["docs-dir"] === "string"
|
|
517
851
|
? normalizeRelativeDirContract(data["docs-dir"])
|
|
518
852
|
: "";
|
|
@@ -525,141 +859,147 @@ export function loadConfig(projectBase: string): UseReqConfig {
|
|
|
525
859
|
.map((item) => normalizeRelativeDirContract(item))
|
|
526
860
|
.filter((item) => item !== "")
|
|
527
861
|
: [];
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const debugLogOnStatus = normalizeDebugLogOnStatus(data.DEBUG_LOG_ON_STATUS);
|
|
544
|
-
const debugEnabledTools = normalizeDebugEnabledTools(data.DEBUG_ENABLED_TOOLS);
|
|
545
|
-
const debugEnabledPrompts = normalizeDebugEnabledPrompts(data.DEBUG_ENABLED_PROMPTS);
|
|
546
|
-
const notifyEnabled = data["notify-enabled"] === true;
|
|
547
|
-
const notifyOnCompleted = data["notify-on-completed"] !== false;
|
|
548
|
-
const notifyOnInterrupted = data["notify-on-interrupted"] === true;
|
|
549
|
-
const notifyOnFailed = data["notify-on-failed"] === true;
|
|
550
|
-
const notifySound = normalizePiNotifySoundLevel(data["notify-sound"]);
|
|
551
|
-
const notifySoundOnCompleted = data["notify-sound-on-completed"] !== false;
|
|
552
|
-
const notifySoundOnInterrupted = data["notify-sound-on-interrupted"] === true;
|
|
553
|
-
const notifySoundOnFailed = data["notify-sound-on-failed"] === true;
|
|
554
|
-
const notifySoundToggleShortcut = normalizePiNotifyShortcut(data["notify-sound-toggle-shortcut"]);
|
|
555
|
-
const pushoverOnCompleted = data["notify-pushover-on-completed"] !== false;
|
|
556
|
-
const pushoverOnInterrupted = data["notify-pushover-on-interrupted"] === true;
|
|
557
|
-
const pushoverOnFailed = data["notify-pushover-on-failed"] === true;
|
|
558
|
-
const pushoverUserKey = normalizePiNotifyPushoverCredential(data["notify-pushover-user-key"]);
|
|
559
|
-
const pushoverApiToken = normalizePiNotifyPushoverCredential(data["notify-pushover-api-token"]);
|
|
560
|
-
const pushoverEnabled = data["notify-pushover-enabled"] === true
|
|
561
|
-
&& hasPiNotifyPushoverCredentials({
|
|
562
|
-
"notify-pushover-user-key": pushoverUserKey,
|
|
563
|
-
"notify-pushover-api-token": pushoverApiToken,
|
|
564
|
-
});
|
|
565
|
-
const pushoverPriority = normalizePiNotifyPushoverPriority(data["notify-pushover-priority"]);
|
|
566
|
-
const pushoverTitle = normalizePiNotifyTemplateValue(
|
|
567
|
-
data["notify-pushover-title"],
|
|
568
|
-
DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
|
|
569
|
-
);
|
|
570
|
-
const pushoverText = normalizePiNotifyTemplateValue(
|
|
571
|
-
data["notify-pushover-text"],
|
|
572
|
-
DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
|
|
573
|
-
);
|
|
574
|
-
const notifyCommand = normalizePiNotifyCommand(data.PI_NOTIFY_CMD, DEFAULT_PI_NOTIFY_CMD);
|
|
575
|
-
const lowSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_LOW_CMD, DEFAULT_PI_NOTIFY_SOUND_LOW_CMD);
|
|
576
|
-
const midSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_MID_CMD, DEFAULT_PI_NOTIFY_SOUND_MID_CMD);
|
|
577
|
-
const highSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_HIGH_CMD, DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD);
|
|
862
|
+
return {
|
|
863
|
+
"docs-dir": docsDirCandidate || DEFAULT_DOCS_DIR,
|
|
864
|
+
"tests-dir": testsDirCandidate || DEFAULT_TESTS_DIR,
|
|
865
|
+
"src-dir": srcDirCandidate.length > 0 ? srcDirCandidate : [...DEFAULT_SRC_DIRS],
|
|
866
|
+
"static-check": normalizeLocalStaticCheckConfig(data["static-check"], defaultStaticCheckConfig),
|
|
867
|
+
DEBUG_ENABLED: normalizeDebugEnabled(data.DEBUG_ENABLED),
|
|
868
|
+
DEBUG_LOG_FILE: normalizeDebugLogFile(data.DEBUG_LOG_FILE),
|
|
869
|
+
DEBUG_STATUS_CHANGES: normalizeDebugStatusChanges(data.DEBUG_STATUS_CHANGES),
|
|
870
|
+
DEBUG_WORKFLOW_EVENTS: normalizeDebugWorkflowEvents(data.DEBUG_WORKFLOW_EVENTS),
|
|
871
|
+
DEBUG_TOOL_COMMANDS_ENABLED: normalizeDebugToolCommandsEnabled(data.DEBUG_TOOL_COMMANDS_ENABLED),
|
|
872
|
+
DEBUG_LOG_ON_STATUS: normalizeDebugLogOnStatus(data.DEBUG_LOG_ON_STATUS),
|
|
873
|
+
DEBUG_ENABLED_TOOLS: normalizeDebugEnabledTools(data.DEBUG_ENABLED_TOOLS),
|
|
874
|
+
DEBUG_ENABLED_PROMPTS: normalizeDebugEnabledPrompts(data.DEBUG_ENABLED_PROMPTS),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
578
877
|
|
|
878
|
+
/**
|
|
879
|
+
* @brief Loads and sanitizes the persisted global configuration.
|
|
880
|
+
* @details Returns defaults when `~/.config/pi-usereq/config.json` is absent. Otherwise parses the global JSON payload, normalizes cross-project checker, tool, git, and notification fields, and ignores misplaced local keys without migration. Runtime is O(n) in file size. Side effects are limited to filesystem reads.
|
|
881
|
+
* @return {UseReqGlobalConfig} Sanitized global configuration.
|
|
882
|
+
*/
|
|
883
|
+
function loadGlobalConfig(): UseReqGlobalConfig {
|
|
884
|
+
const configPath = getGlobalConfigPath();
|
|
885
|
+
const data = readConfigPayload(configPath);
|
|
886
|
+
if (!data) {
|
|
887
|
+
return getDefaultGlobalConfig();
|
|
888
|
+
}
|
|
579
889
|
return {
|
|
580
|
-
"
|
|
581
|
-
"
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
"notify-
|
|
596
|
-
"notify-on-
|
|
597
|
-
"notify-on-
|
|
598
|
-
"notify-
|
|
599
|
-
"notify-
|
|
600
|
-
"notify-
|
|
601
|
-
"notify-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
"notify-pushover-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
PI_NOTIFY_CMD: notifyCommand,
|
|
614
|
-
PI_NOTIFY_SOUND_LOW_CMD: lowSoundCommand,
|
|
615
|
-
PI_NOTIFY_SOUND_MID_CMD: midSoundCommand,
|
|
616
|
-
PI_NOTIFY_SOUND_HIGH_CMD: highSoundCommand,
|
|
890
|
+
"static-check": normalizeGlobalStaticCheckConfig(data["static-check"]),
|
|
891
|
+
"enabled-tools": normalizeEnabledPiUsereqTools(data["enabled-tools"]),
|
|
892
|
+
AUTO_GIT_COMMIT: normalizeAutoGitCommit(data.AUTO_GIT_COMMIT),
|
|
893
|
+
GIT_WORKTREE_ENABLED: normalizeGitWorktreeEnabled(data.GIT_WORKTREE_ENABLED),
|
|
894
|
+
GIT_WORKTREE_PREFIX: normalizeGitWorktreePrefix(data.GIT_WORKTREE_PREFIX),
|
|
895
|
+
"notify-enabled": data["notify-enabled"] === true,
|
|
896
|
+
"notify-on-completed": data["notify-on-completed"] !== false,
|
|
897
|
+
"notify-on-interrupted": data["notify-on-interrupted"] === true,
|
|
898
|
+
"notify-on-failed": data["notify-on-failed"] === true,
|
|
899
|
+
"notify-sound": normalizePiNotifySoundLevel(data["notify-sound"]),
|
|
900
|
+
"notify-sound-on-completed": data["notify-sound-on-completed"] !== false,
|
|
901
|
+
"notify-sound-on-interrupted": data["notify-sound-on-interrupted"] === true,
|
|
902
|
+
"notify-sound-on-failed": data["notify-sound-on-failed"] === true,
|
|
903
|
+
"notify-sound-toggle-shortcut": normalizePiNotifyShortcut(data["notify-sound-toggle-shortcut"]),
|
|
904
|
+
"notify-pushover-enabled": data["notify-pushover-enabled"] === true,
|
|
905
|
+
"notify-pushover-on-completed": data["notify-pushover-on-completed"] !== false,
|
|
906
|
+
"notify-pushover-on-interrupted": data["notify-pushover-on-interrupted"] === true,
|
|
907
|
+
"notify-pushover-on-failed": data["notify-pushover-on-failed"] === true,
|
|
908
|
+
"notify-pushover-user-key": normalizePiNotifyPushoverCredential(data["notify-pushover-user-key"]),
|
|
909
|
+
"notify-pushover-api-token": normalizePiNotifyPushoverCredential(data["notify-pushover-api-token"]),
|
|
910
|
+
"notify-pushover-priority": normalizePiNotifyPushoverPriority(data["notify-pushover-priority"]),
|
|
911
|
+
"notify-pushover-title": normalizePiNotifyTemplateValue(
|
|
912
|
+
data["notify-pushover-title"],
|
|
913
|
+
DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
|
|
914
|
+
),
|
|
915
|
+
"notify-pushover-text": normalizePiNotifyTemplateValue(
|
|
916
|
+
data["notify-pushover-text"],
|
|
917
|
+
DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
|
|
918
|
+
),
|
|
919
|
+
PI_NOTIFY_CMD: normalizePiNotifyCommand(data.PI_NOTIFY_CMD, DEFAULT_PI_NOTIFY_CMD),
|
|
920
|
+
PI_NOTIFY_SOUND_LOW_CMD: normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_LOW_CMD, DEFAULT_PI_NOTIFY_SOUND_LOW_CMD),
|
|
921
|
+
PI_NOTIFY_SOUND_MID_CMD: normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_MID_CMD, DEFAULT_PI_NOTIFY_SOUND_MID_CMD),
|
|
922
|
+
PI_NOTIFY_SOUND_HIGH_CMD: normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_HIGH_CMD, DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD),
|
|
617
923
|
};
|
|
618
924
|
}
|
|
619
925
|
|
|
620
926
|
/**
|
|
621
|
-
* @brief
|
|
622
|
-
* @details
|
|
927
|
+
* @brief Loads and sanitizes the effective merged configuration.
|
|
928
|
+
* @details Loads global configuration first so local static-check enable defaults can be derived from the active global checker map, then merges both scopes into the effective runtime config without applying legacy single-file migrations. Runtime is O(n) in combined local and global config size. Side effects are limited to filesystem reads.
|
|
929
|
+
* @param[in] projectBase {string} Absolute project root path.
|
|
930
|
+
* @return {UseReqConfig} Sanitized effective configuration.
|
|
931
|
+
* @throws {ReqError} Throws with exit code `11` when either persisted config file contains invalid JSON or a non-object payload.
|
|
932
|
+
* @satisfies CTN-012, CTN-013, CTN-018, CTN-019, REQ-066, REQ-137, REQ-146, REQ-163, REQ-174, REQ-178, REQ-184, REQ-185, REQ-196, REQ-204, REQ-205, REQ-212, REQ-215, REQ-234, REQ-235, REQ-236, REQ-237, REQ-238, REQ-239, REQ-249, REQ-277, REQ-315, REQ-316
|
|
933
|
+
*/
|
|
934
|
+
export function loadConfig(projectBase: string): UseReqConfig {
|
|
935
|
+
const globalConfig = loadGlobalConfig();
|
|
936
|
+
const localConfig = loadLocalConfig(
|
|
937
|
+
projectBase,
|
|
938
|
+
getDefaultLocalStaticCheckConfig(globalConfig["static-check"]),
|
|
939
|
+
);
|
|
940
|
+
return mergeConfigScopes(localConfig, globalConfig);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* @brief Builds the persisted local configuration payload.
|
|
945
|
+
* @details Copies only project-scoped keys into a fresh object so runtime-derived metadata plus global checker, tool, git, and notification fields never reach `.pi-usereq.json`, while preserving the debug tool-wrapper command flag beside other local debug settings. Runtime is O(n) in config size. No external state is mutated.
|
|
623
946
|
* @param[in] config {UseReqConfig} Effective configuration object.
|
|
624
|
-
* @return {
|
|
625
|
-
* @satisfies CTN-012, CTN-013,
|
|
947
|
+
* @return {UseReqLocalConfig} Persistable local configuration payload.
|
|
948
|
+
* @satisfies CTN-012, CTN-013, CTN-019, REQ-104, REQ-146, REQ-249, REQ-316, REQ-277
|
|
626
949
|
*/
|
|
627
|
-
function
|
|
950
|
+
function buildPersistedLocalConfig(config: UseReqConfig): UseReqLocalConfig {
|
|
951
|
+
const normalizedSrcDir = config["src-dir"]
|
|
952
|
+
.map((entry) => normalizeRelativeDirContract(entry))
|
|
953
|
+
.filter((entry) => entry !== "");
|
|
628
954
|
return {
|
|
629
955
|
"docs-dir": normalizeRelativeDirContract(config["docs-dir"]) || DEFAULT_DOCS_DIR,
|
|
630
956
|
"tests-dir": normalizeRelativeDirContract(config["tests-dir"]) || DEFAULT_TESTS_DIR,
|
|
631
|
-
"src-dir":
|
|
632
|
-
const normalizedSrcDir = config["src-dir"]
|
|
633
|
-
.map((entry) => normalizeRelativeDirContract(entry))
|
|
634
|
-
.filter((entry) => entry !== "");
|
|
635
|
-
return normalizedSrcDir.length > 0 ? normalizedSrcDir : [...DEFAULT_SRC_DIRS];
|
|
636
|
-
})(),
|
|
957
|
+
"src-dir": normalizedSrcDir.length > 0 ? normalizedSrcDir : [...DEFAULT_SRC_DIRS],
|
|
637
958
|
"static-check": Object.fromEntries(
|
|
638
959
|
Object.entries(config["static-check"]).map(([language, languageConfig]) => [
|
|
639
960
|
language,
|
|
640
|
-
|
|
641
|
-
|
|
961
|
+
createLocalStaticCheckLanguageConfig(
|
|
962
|
+
normalizeStaticCheckEnabled(
|
|
642
963
|
languageConfig.enabled,
|
|
643
964
|
languageConfig.checkers.length > 0 ? "enable" : "disable",
|
|
644
965
|
),
|
|
645
|
-
|
|
646
|
-
},
|
|
966
|
+
),
|
|
647
967
|
]),
|
|
648
968
|
),
|
|
649
|
-
"enabled-tools": [...config["enabled-tools"]],
|
|
650
|
-
AUTO_GIT_COMMIT: normalizeAutoGitCommit(config.AUTO_GIT_COMMIT),
|
|
651
|
-
GIT_WORKTREE_ENABLED: resolveEffectiveGitWorktreeEnabled(
|
|
652
|
-
normalizeAutoGitCommit(config.AUTO_GIT_COMMIT),
|
|
653
|
-
config.GIT_WORKTREE_ENABLED,
|
|
654
|
-
),
|
|
655
|
-
GIT_WORKTREE_PREFIX: normalizeGitWorktreePrefix(config.GIT_WORKTREE_PREFIX),
|
|
656
969
|
DEBUG_ENABLED: normalizeDebugEnabled(config.DEBUG_ENABLED),
|
|
657
970
|
DEBUG_LOG_FILE: normalizeDebugLogFile(config.DEBUG_LOG_FILE),
|
|
658
971
|
DEBUG_STATUS_CHANGES: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES),
|
|
659
972
|
DEBUG_WORKFLOW_EVENTS: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS),
|
|
973
|
+
DEBUG_TOOL_COMMANDS_ENABLED: normalizeDebugToolCommandsEnabled(config.DEBUG_TOOL_COMMANDS_ENABLED),
|
|
660
974
|
DEBUG_LOG_ON_STATUS: normalizeDebugLogOnStatus(config.DEBUG_LOG_ON_STATUS),
|
|
661
975
|
DEBUG_ENABLED_TOOLS: normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS),
|
|
662
976
|
DEBUG_ENABLED_PROMPTS: normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* @brief Builds the persisted global configuration payload.
|
|
982
|
+
* @details Copies only cross-project keys into a fresh object so local directory and debug fields never reach `~/.config/pi-usereq/config.json`, while forcing persisted worktree disablement when automatic git commit is disabled and forcing persisted Pushover disablement until both credentials are populated. Runtime is O(n) in config size. No external state is mutated.
|
|
983
|
+
* @param[in] config {UseReqConfig} Effective configuration object.
|
|
984
|
+
* @return {UseReqGlobalConfig} Persistable global configuration payload.
|
|
985
|
+
* @satisfies REQ-137, REQ-163, REQ-174, REQ-178, REQ-184, REQ-196, REQ-204, REQ-205, REQ-212, REQ-234, REQ-249, REQ-315
|
|
986
|
+
*/
|
|
987
|
+
function buildPersistedGlobalConfig(config: UseReqConfig): UseReqGlobalConfig {
|
|
988
|
+
const autoGitCommit = normalizeAutoGitCommit(config.AUTO_GIT_COMMIT);
|
|
989
|
+
return {
|
|
990
|
+
"static-check": Object.fromEntries(
|
|
991
|
+
Object.entries(config["static-check"]).map(([language, languageConfig]) => [
|
|
992
|
+
language,
|
|
993
|
+
createGlobalStaticCheckLanguageConfig(languageConfig.checkers),
|
|
994
|
+
]),
|
|
995
|
+
),
|
|
996
|
+
"enabled-tools": normalizeEnabledPiUsereqTools(config["enabled-tools"]),
|
|
997
|
+
AUTO_GIT_COMMIT: autoGitCommit,
|
|
998
|
+
GIT_WORKTREE_ENABLED: resolveEffectiveGitWorktreeEnabled(
|
|
999
|
+
autoGitCommit,
|
|
1000
|
+
config.GIT_WORKTREE_ENABLED,
|
|
1001
|
+
),
|
|
1002
|
+
GIT_WORKTREE_PREFIX: normalizeGitWorktreePrefix(config.GIT_WORKTREE_PREFIX),
|
|
663
1003
|
"notify-enabled": config["notify-enabled"],
|
|
664
1004
|
"notify-on-completed": config["notify-on-completed"],
|
|
665
1005
|
"notify-on-interrupted": config["notify-on-interrupted"],
|
|
@@ -669,8 +1009,7 @@ function buildPersistedConfig(config: UseReqConfig): UseReqConfig {
|
|
|
669
1009
|
"notify-sound-on-interrupted": config["notify-sound-on-interrupted"],
|
|
670
1010
|
"notify-sound-on-failed": config["notify-sound-on-failed"],
|
|
671
1011
|
"notify-sound-toggle-shortcut": config["notify-sound-toggle-shortcut"],
|
|
672
|
-
"notify-pushover-enabled": config["notify-pushover-enabled"]
|
|
673
|
-
&& hasPiNotifyPushoverCredentials(config),
|
|
1012
|
+
"notify-pushover-enabled": config["notify-pushover-enabled"] && hasPiNotifyPushoverCredentials(config),
|
|
674
1013
|
"notify-pushover-on-completed": config["notify-pushover-on-completed"],
|
|
675
1014
|
"notify-pushover-on-interrupted": config["notify-pushover-on-interrupted"],
|
|
676
1015
|
"notify-pushover-on-failed": config["notify-pushover-on-failed"],
|
|
@@ -687,17 +1026,51 @@ function buildPersistedConfig(config: UseReqConfig): UseReqConfig {
|
|
|
687
1026
|
}
|
|
688
1027
|
|
|
689
1028
|
/**
|
|
690
|
-
* @brief
|
|
691
|
-
* @details Creates the
|
|
1029
|
+
* @brief Writes one normalized config payload to disk.
|
|
1030
|
+
* @details Creates the parent directory when required, formats JSON with two-space indentation, and terminates the file with a newline. Runtime is O(n) in serialized payload size. Side effects include directory creation and file overwrite.
|
|
1031
|
+
* @param[in] configPath {string} Absolute destination config path.
|
|
1032
|
+
* @param[in] payload {object} Persistable config payload.
|
|
1033
|
+
* @return {void} No return value.
|
|
1034
|
+
*/
|
|
1035
|
+
function writeConfigFile(configPath: string, payload: object): void {
|
|
1036
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1037
|
+
fs.writeFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* @brief Persists the local configuration scope to disk.
|
|
1042
|
+
* @details Serializes only project-scoped fields into `<base-path>/.pi-usereq.json`, excluding runtime-derived metadata and every global-scope configuration key. Runtime is O(n) in serialized local config size. Side effects include directory creation and file overwrite.
|
|
1043
|
+
* @param[in] projectBase {string} Absolute project root path.
|
|
1044
|
+
* @param[in] config {UseReqConfig} Effective configuration object to persist.
|
|
1045
|
+
* @return {void} No return value.
|
|
1046
|
+
* @satisfies CTN-012, REQ-104, REQ-146
|
|
1047
|
+
*/
|
|
1048
|
+
export function saveLocalConfig(projectBase: string, config: UseReqConfig): void {
|
|
1049
|
+
writeConfigFile(getProjectConfigPath(projectBase), buildPersistedLocalConfig(config));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* @brief Persists the global configuration scope to disk.
|
|
1054
|
+
* @details Serializes only cross-project fields into `~/.config/pi-usereq/config.json`, excluding every project-local directory and debug setting. Runtime is O(n) in serialized global config size. Side effects include directory creation and file overwrite.
|
|
1055
|
+
* @param[in] config {UseReqConfig} Effective configuration object to persist.
|
|
1056
|
+
* @return {void} No return value.
|
|
1057
|
+
* @satisfies CTN-012, CTN-018, REQ-137, REQ-146, REQ-315
|
|
1058
|
+
*/
|
|
1059
|
+
export function saveGlobalConfig(config: UseReqConfig): void {
|
|
1060
|
+
writeConfigFile(getGlobalConfigPath(), buildPersistedGlobalConfig(config));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* @brief Persists the effective configuration to local and global config files.
|
|
1065
|
+
* @details Splits the effective runtime config into project-scoped and cross-project payloads, then writes both files with normalized JSON formatting. Runtime is O(n) in combined serialized config size. Side effects include directory creation and file overwrite in both persistence locations.
|
|
692
1066
|
* @param[in] projectBase {string} Absolute project root path.
|
|
693
|
-
* @param[in] config {UseReqConfig}
|
|
1067
|
+
* @param[in] config {UseReqConfig} Effective configuration object to persist.
|
|
694
1068
|
* @return {void} No return value.
|
|
695
|
-
* @satisfies CTN-012, REQ-146
|
|
1069
|
+
* @satisfies CTN-012, CTN-018, REQ-137, REQ-146, REQ-315
|
|
696
1070
|
*/
|
|
697
1071
|
export function saveConfig(projectBase: string, config: UseReqConfig): void {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
fs.writeFileSync(configPath, `${JSON.stringify(buildPersistedConfig(config), null, 2)}\n`, "utf8");
|
|
1072
|
+
saveLocalConfig(projectBase, config);
|
|
1073
|
+
saveGlobalConfig(config);
|
|
701
1074
|
}
|
|
702
1075
|
|
|
703
1076
|
/**
|