@build-astron-co/nimbus 0.2.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/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Configuration
|
|
3
|
+
*
|
|
4
|
+
* Parses and validates `.nimbus/hooks.yaml` configuration files.
|
|
5
|
+
* Provides types and utilities for the Nimbus hooks system that allows
|
|
6
|
+
* users to run custom scripts before/after tool invocations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Events that can trigger hook execution */
|
|
17
|
+
export type HookEvent = 'PreToolUse' | 'PostToolUse' | 'PermissionRequest';
|
|
18
|
+
|
|
19
|
+
/** All valid hook event names for validation */
|
|
20
|
+
const VALID_HOOK_EVENTS: readonly HookEvent[] = [
|
|
21
|
+
'PreToolUse',
|
|
22
|
+
'PostToolUse',
|
|
23
|
+
'PermissionRequest',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A single hook definition specifying when and what to run.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```yaml
|
|
31
|
+
* - match: "edit_file|write_file"
|
|
32
|
+
* command: ".nimbus/hooks/pre-edit.sh"
|
|
33
|
+
* timeout: 30000
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export interface HookDefinition {
|
|
37
|
+
/** Regex pattern to match tool names (e.g. "edit_file|write_file") */
|
|
38
|
+
match: string;
|
|
39
|
+
/** Shell command or path to script to execute */
|
|
40
|
+
command: string;
|
|
41
|
+
/** Timeout in milliseconds before the hook is killed (default: 30000) */
|
|
42
|
+
timeout?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Top-level hooks configuration parsed from `.nimbus/hooks.yaml`.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```yaml
|
|
50
|
+
* hooks:
|
|
51
|
+
* PreToolUse:
|
|
52
|
+
* - match: "edit_file|write_file"
|
|
53
|
+
* command: ".nimbus/hooks/pre-edit.sh"
|
|
54
|
+
* PostToolUse:
|
|
55
|
+
* - match: "edit_file|write_file"
|
|
56
|
+
* command: ".nimbus/hooks/auto-format.sh"
|
|
57
|
+
* PermissionRequest:
|
|
58
|
+
* - match: "*"
|
|
59
|
+
* command: ".nimbus/hooks/audit-permission.sh"
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export interface HooksConfig {
|
|
63
|
+
hooks: Record<HookEvent, HookDefinition[]>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Default timeout in milliseconds for hook execution */
|
|
67
|
+
export const DEFAULT_HOOK_TIMEOUT = 30_000;
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Minimal YAML Parser
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Represents a single line of parsed YAML content with its indentation level.
|
|
75
|
+
*/
|
|
76
|
+
interface YamlLine {
|
|
77
|
+
indent: number;
|
|
78
|
+
content: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse raw YAML text into an array of meaningful lines with indentation info.
|
|
83
|
+
* Strips comments and blank lines.
|
|
84
|
+
*
|
|
85
|
+
* @param text - Raw YAML content
|
|
86
|
+
* @returns Array of parsed lines with indentation levels
|
|
87
|
+
*/
|
|
88
|
+
function tokenizeYaml(text: string): YamlLine[] {
|
|
89
|
+
const lines: YamlLine[] = [];
|
|
90
|
+
for (const raw of text.split('\n')) {
|
|
91
|
+
// Strip inline comments (but not inside quoted strings)
|
|
92
|
+
const withoutComment = raw.replace(/#.*$/, '');
|
|
93
|
+
const trimmed = withoutComment.trimEnd();
|
|
94
|
+
if (trimmed.length === 0) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const indent = trimmed.search(/\S/);
|
|
98
|
+
if (indent === -1) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
lines.push({ indent, content: trimmed.trim() });
|
|
102
|
+
}
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove surrounding quotes (single or double) from a string value.
|
|
108
|
+
*
|
|
109
|
+
* @param value - Potentially quoted string
|
|
110
|
+
* @returns Unquoted string
|
|
111
|
+
*/
|
|
112
|
+
function unquote(value: string): string {
|
|
113
|
+
if (
|
|
114
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
115
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
116
|
+
) {
|
|
117
|
+
return value.slice(1, -1);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Coerce a raw YAML string value to the appropriate JS primitive.
|
|
124
|
+
*
|
|
125
|
+
* @param raw - Raw string value from YAML
|
|
126
|
+
* @returns Coerced value (string, number, boolean, or null)
|
|
127
|
+
*/
|
|
128
|
+
function coerceValue(raw: string): string | number | boolean | null {
|
|
129
|
+
if (raw === 'true') {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (raw === 'false') {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (raw === 'null' || raw === '~') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const unquoted = unquote(raw);
|
|
140
|
+
if (unquoted !== raw) {
|
|
141
|
+
// Was quoted -- keep as string
|
|
142
|
+
return unquoted;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Try number coercion
|
|
146
|
+
if (raw !== '' && !isNaN(Number(raw))) {
|
|
147
|
+
return Number(raw);
|
|
148
|
+
}
|
|
149
|
+
return raw;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Minimal recursive-descent YAML parser.
|
|
154
|
+
*
|
|
155
|
+
* Handles the subset of YAML needed for hooks configuration:
|
|
156
|
+
* - Top-level maps
|
|
157
|
+
* - Arrays of objects (using `- key: value` syntax)
|
|
158
|
+
* - Scalar values (string, number, boolean)
|
|
159
|
+
* - Nested maps
|
|
160
|
+
*
|
|
161
|
+
* This is intentionally NOT a full YAML parser. It covers the structure
|
|
162
|
+
* required by `.nimbus/hooks.yaml` without requiring an external dependency.
|
|
163
|
+
*
|
|
164
|
+
* @param text - Raw YAML content
|
|
165
|
+
* @returns Parsed object
|
|
166
|
+
*/
|
|
167
|
+
function parseYaml(text: string): Record<string, unknown> {
|
|
168
|
+
const lines = tokenizeYaml(text);
|
|
169
|
+
let pos = 0;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse a mapping (object) at the given indentation level.
|
|
173
|
+
*/
|
|
174
|
+
function parseMapping(minIndent: number): Record<string, unknown> {
|
|
175
|
+
const result: Record<string, unknown> = {};
|
|
176
|
+
|
|
177
|
+
while (pos < lines.length) {
|
|
178
|
+
const line = lines[pos];
|
|
179
|
+
|
|
180
|
+
// If the line is at a lower indent, we've left this mapping
|
|
181
|
+
if (line.indent < minIndent) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Skip lines that are deeper than expected (shouldn't happen in
|
|
186
|
+
// well-formed input, but be defensive)
|
|
187
|
+
if (line.indent > minIndent && !line.content.startsWith('- ')) {
|
|
188
|
+
pos++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Array items are handled by the caller (parseArray)
|
|
193
|
+
if (line.content.startsWith('- ')) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const colonIdx = line.content.indexOf(':');
|
|
198
|
+
if (colonIdx === -1) {
|
|
199
|
+
pos++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const key = line.content.slice(0, colonIdx).trim();
|
|
204
|
+
const rest = line.content.slice(colonIdx + 1).trim();
|
|
205
|
+
|
|
206
|
+
pos++;
|
|
207
|
+
|
|
208
|
+
if (rest.length > 0) {
|
|
209
|
+
// Inline scalar value: `key: value`
|
|
210
|
+
result[key] = coerceValue(rest);
|
|
211
|
+
} else {
|
|
212
|
+
// Value is on subsequent indented lines -- either a nested map or array
|
|
213
|
+
if (pos < lines.length && lines[pos].indent > minIndent) {
|
|
214
|
+
const childIndent = lines[pos].indent;
|
|
215
|
+
if (lines[pos].content.startsWith('- ')) {
|
|
216
|
+
result[key] = parseArray(childIndent);
|
|
217
|
+
} else {
|
|
218
|
+
result[key] = parseMapping(childIndent);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
// Empty value
|
|
222
|
+
result[key] = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse an array at the given indentation level.
|
|
232
|
+
* Each array element starts with `- ` and can contain inline key-value
|
|
233
|
+
* pairs or a nested block mapping.
|
|
234
|
+
*/
|
|
235
|
+
function parseArray(minIndent: number): unknown[] {
|
|
236
|
+
const result: unknown[] = [];
|
|
237
|
+
|
|
238
|
+
while (pos < lines.length) {
|
|
239
|
+
const line = lines[pos];
|
|
240
|
+
|
|
241
|
+
if (line.indent < minIndent) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!line.content.startsWith('- ')) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Strip the leading `- `
|
|
250
|
+
const afterDash = line.content.slice(2).trim();
|
|
251
|
+
pos++;
|
|
252
|
+
|
|
253
|
+
if (afterDash.includes(':')) {
|
|
254
|
+
// Inline object start: `- key: value`
|
|
255
|
+
const obj: Record<string, unknown> = {};
|
|
256
|
+
const colonIdx = afterDash.indexOf(':');
|
|
257
|
+
const key = afterDash.slice(0, colonIdx).trim();
|
|
258
|
+
const val = afterDash.slice(colonIdx + 1).trim();
|
|
259
|
+
obj[key] = val.length > 0 ? coerceValue(val) : null;
|
|
260
|
+
|
|
261
|
+
// Collect subsequent indented key-value pairs belonging to the same item
|
|
262
|
+
while (pos < lines.length) {
|
|
263
|
+
const next = lines[pos];
|
|
264
|
+
// Must be indented deeper than the `- ` marker and NOT be another array item
|
|
265
|
+
if (next.indent <= minIndent || next.content.startsWith('- ')) {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
const nextColon = next.content.indexOf(':');
|
|
269
|
+
if (nextColon === -1) {
|
|
270
|
+
pos++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const nk = next.content.slice(0, nextColon).trim();
|
|
274
|
+
const nv = next.content.slice(nextColon + 1).trim();
|
|
275
|
+
obj[nk] = nv.length > 0 ? coerceValue(nv) : null;
|
|
276
|
+
pos++;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
result.push(obj);
|
|
280
|
+
} else if (afterDash.length > 0) {
|
|
281
|
+
// Scalar array element: `- value`
|
|
282
|
+
result.push(coerceValue(afterDash));
|
|
283
|
+
} else {
|
|
284
|
+
// Block-style object under `- `
|
|
285
|
+
if (pos < lines.length && lines[pos].indent > minIndent) {
|
|
286
|
+
const childIndent = lines[pos].indent;
|
|
287
|
+
result.push(parseMapping(childIndent));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return parseMapping(0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Validation
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validate a single hook definition and return any errors found.
|
|
304
|
+
*
|
|
305
|
+
* Checks:
|
|
306
|
+
* - `match` is a non-empty string that compiles as a valid RegExp
|
|
307
|
+
* - `command` is a non-empty string
|
|
308
|
+
* - `timeout`, if provided, is a positive number
|
|
309
|
+
*
|
|
310
|
+
* @param hook - The hook definition to validate
|
|
311
|
+
* @returns Array of human-readable error strings (empty if valid)
|
|
312
|
+
*/
|
|
313
|
+
export function validateHookDefinition(hook: HookDefinition): string[] {
|
|
314
|
+
const errors: string[] = [];
|
|
315
|
+
|
|
316
|
+
// match
|
|
317
|
+
if (typeof hook.match !== 'string' || hook.match.length === 0) {
|
|
318
|
+
errors.push('hook "match" must be a non-empty string');
|
|
319
|
+
} else {
|
|
320
|
+
try {
|
|
321
|
+
new RegExp(hook.match);
|
|
322
|
+
} catch {
|
|
323
|
+
errors.push(`hook "match" is not a valid regex: "${hook.match}"`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// command
|
|
328
|
+
if (typeof hook.command !== 'string' || hook.command.length === 0) {
|
|
329
|
+
errors.push('hook "command" must be a non-empty string');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// timeout (optional)
|
|
333
|
+
if (hook.timeout !== undefined) {
|
|
334
|
+
if (typeof hook.timeout !== 'number' || hook.timeout <= 0) {
|
|
335
|
+
errors.push('hook "timeout" must be a positive number');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return errors;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Loader
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Load and validate a hooks configuration from `<projectDir>/.nimbus/hooks.yaml`.
|
|
348
|
+
*
|
|
349
|
+
* @param projectDir - Absolute or relative path to the project root directory
|
|
350
|
+
* @returns Parsed and validated `HooksConfig`, or `null` if the file does not exist
|
|
351
|
+
* @throws Error if the file exists but contains invalid configuration
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* const config = loadHooksConfig('/path/to/project');
|
|
356
|
+
* if (config) {
|
|
357
|
+
* console.log(config.hooks.PreToolUse);
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
export function loadHooksConfig(projectDir: string): HooksConfig | null {
|
|
362
|
+
const configPath = path.join(projectDir, '.nimbus', 'hooks.yaml');
|
|
363
|
+
|
|
364
|
+
if (!fs.existsSync(configPath)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
369
|
+
const parsed = parseYaml(raw) as Record<string, unknown>;
|
|
370
|
+
|
|
371
|
+
// Validate top-level structure
|
|
372
|
+
if (!parsed.hooks || typeof parsed.hooks !== 'object') {
|
|
373
|
+
throw new Error(`Invalid hooks config at ${configPath}: missing top-level "hooks" key`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const hooksRaw = parsed.hooks as Record<string, unknown>;
|
|
377
|
+
|
|
378
|
+
// Build the validated config, ensuring all three event types are present
|
|
379
|
+
const config: HooksConfig = {
|
|
380
|
+
hooks: {
|
|
381
|
+
PreToolUse: [],
|
|
382
|
+
PostToolUse: [],
|
|
383
|
+
PermissionRequest: [],
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
for (const [eventName, definitions] of Object.entries(hooksRaw)) {
|
|
388
|
+
// Validate event name
|
|
389
|
+
if (!VALID_HOOK_EVENTS.includes(eventName as HookEvent)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Invalid hooks config at ${configPath}: unknown hook event "${eventName}". ` +
|
|
392
|
+
`Valid events: ${VALID_HOOK_EVENTS.join(', ')}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const event = eventName as HookEvent;
|
|
397
|
+
|
|
398
|
+
if (!Array.isArray(definitions)) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Invalid hooks config at ${configPath}: "${eventName}" must be an array of hook definitions`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (let i = 0; i < definitions.length; i++) {
|
|
405
|
+
const def = definitions[i] as Record<string, unknown>;
|
|
406
|
+
|
|
407
|
+
if (typeof def !== 'object' || def === null) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Invalid hooks config at ${configPath}: ${eventName}[${i}] must be an object`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const hookDef: HookDefinition = {
|
|
414
|
+
match: String(def.match ?? ''),
|
|
415
|
+
command: String(def.command ?? ''),
|
|
416
|
+
timeout:
|
|
417
|
+
def.timeout !== undefined && def.timeout !== null ? Number(def.timeout) : undefined,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const validationErrors = validateHookDefinition(hookDef);
|
|
421
|
+
if (validationErrors.length > 0) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`Invalid hooks config at ${configPath}: ${eventName}[${i}]: ${validationErrors.join('; ')}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
config.hooks[event].push(hookDef);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return config;
|
|
432
|
+
}
|