@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,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages Nimbus configuration stored at ~/.nimbus/config.yaml
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { CONFIG_KEYS, type NimbusConfig, type ConfigKey } from './types';
|
|
11
|
+
import { NimbusConfigSchema } from './schema';
|
|
12
|
+
|
|
13
|
+
const CONFIG_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Forbidden keys that could lead to prototype pollution
|
|
17
|
+
*/
|
|
18
|
+
const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a key is safe from prototype pollution
|
|
22
|
+
*/
|
|
23
|
+
function isSafeKey(key: string): boolean {
|
|
24
|
+
return !FORBIDDEN_KEYS.includes(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate all parts of a dot-notation key path
|
|
29
|
+
*/
|
|
30
|
+
function validateKeyPath(key: string): void {
|
|
31
|
+
const parts = key.split('.');
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
if (!isSafeKey(part)) {
|
|
34
|
+
throw new Error(`Invalid config key: "${part}" is not allowed`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve environment variables in config values.
|
|
41
|
+
* Supports ${VAR} and ${VAR:-default} syntax.
|
|
42
|
+
* Recursively walks objects and arrays.
|
|
43
|
+
*/
|
|
44
|
+
function resolveEnvVars(value: any): any {
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
|
|
47
|
+
const defaultSep = expr.indexOf(':-');
|
|
48
|
+
if (defaultSep !== -1) {
|
|
49
|
+
const varName = expr.slice(0, defaultSep);
|
|
50
|
+
const defaultValue = expr.slice(defaultSep + 2);
|
|
51
|
+
return process.env[varName] ?? defaultValue;
|
|
52
|
+
}
|
|
53
|
+
return process.env[expr] ?? '';
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return value.map(resolveEnvVars);
|
|
58
|
+
}
|
|
59
|
+
if (value !== null && typeof value === 'object') {
|
|
60
|
+
const result: Record<string, any> = {};
|
|
61
|
+
for (const [k, v] of Object.entries(value)) {
|
|
62
|
+
result[k] = resolveEnvVars(v);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create default configuration
|
|
71
|
+
*/
|
|
72
|
+
function createDefaultConfig(): NimbusConfig {
|
|
73
|
+
return {
|
|
74
|
+
version: CONFIG_VERSION,
|
|
75
|
+
workspace: {
|
|
76
|
+
defaultProvider: 'aws',
|
|
77
|
+
outputDirectory: './infrastructure',
|
|
78
|
+
},
|
|
79
|
+
llm: {
|
|
80
|
+
temperature: 0.7,
|
|
81
|
+
maxTokens: 4096,
|
|
82
|
+
},
|
|
83
|
+
history: {
|
|
84
|
+
maxEntries: 100,
|
|
85
|
+
enabled: true,
|
|
86
|
+
},
|
|
87
|
+
safety: {
|
|
88
|
+
requireConfirmation: true,
|
|
89
|
+
dryRunByDefault: false,
|
|
90
|
+
},
|
|
91
|
+
ui: {
|
|
92
|
+
theme: 'auto',
|
|
93
|
+
colors: true,
|
|
94
|
+
spinner: 'dots',
|
|
95
|
+
},
|
|
96
|
+
persona: {
|
|
97
|
+
mode: 'standard',
|
|
98
|
+
verbosity: 'normal',
|
|
99
|
+
custom: '',
|
|
100
|
+
},
|
|
101
|
+
cloud: {
|
|
102
|
+
default_provider: 'aws',
|
|
103
|
+
aws: { default_region: 'us-east-1', default_profile: 'default' },
|
|
104
|
+
gcp: { default_region: 'us-central1' },
|
|
105
|
+
azure: { default_region: 'eastus' },
|
|
106
|
+
},
|
|
107
|
+
terraform: { default_backend: 's3' },
|
|
108
|
+
kubernetes: { default_namespace: 'default' },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse YAML-like config (simple key: value format)
|
|
114
|
+
* Note: This is a simple parser, not full YAML
|
|
115
|
+
*/
|
|
116
|
+
function parseSimpleYaml(content: string): Record<string, any> {
|
|
117
|
+
const result: Record<string, any> = {};
|
|
118
|
+
const lines = content.split('\n');
|
|
119
|
+
const stack: Array<{ indent: number; obj: Record<string, any>; key: string }> = [];
|
|
120
|
+
let currentObj = result;
|
|
121
|
+
let currentIndent = 0;
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
// Skip comments and empty lines
|
|
125
|
+
if (line.trim().startsWith('#') || !line.trim()) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const indent = line.search(/\S/);
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
|
|
132
|
+
// Handle nested objects
|
|
133
|
+
if (trimmed.endsWith(':')) {
|
|
134
|
+
const key = trimmed.slice(0, -1);
|
|
135
|
+
|
|
136
|
+
if (indent > currentIndent) {
|
|
137
|
+
// Going deeper
|
|
138
|
+
stack.push({ indent: currentIndent, obj: currentObj, key: '' });
|
|
139
|
+
} else if (indent < currentIndent) {
|
|
140
|
+
// Going back up
|
|
141
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
142
|
+
const item = stack.pop()!;
|
|
143
|
+
currentObj = item.obj;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
currentObj[key] = {};
|
|
148
|
+
stack.push({ indent, obj: currentObj, key });
|
|
149
|
+
currentObj = currentObj[key];
|
|
150
|
+
currentIndent = indent;
|
|
151
|
+
} else if (trimmed.includes(':')) {
|
|
152
|
+
// Key-value pair
|
|
153
|
+
const colonIndex = trimmed.indexOf(':');
|
|
154
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
155
|
+
let value: any = trimmed.slice(colonIndex + 1).trim();
|
|
156
|
+
|
|
157
|
+
// Handle indentation changes
|
|
158
|
+
if (indent < currentIndent && stack.length > 0) {
|
|
159
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
160
|
+
const item = stack.pop()!;
|
|
161
|
+
currentObj = item.obj;
|
|
162
|
+
}
|
|
163
|
+
currentIndent = indent;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Parse value type
|
|
167
|
+
if (value === 'true') {
|
|
168
|
+
value = true;
|
|
169
|
+
} else if (value === 'false') {
|
|
170
|
+
value = false;
|
|
171
|
+
} else if (value === 'null' || value === '~') {
|
|
172
|
+
value = null;
|
|
173
|
+
} else if (!isNaN(Number(value)) && value !== '') {
|
|
174
|
+
value = Number(value);
|
|
175
|
+
} else if (
|
|
176
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
177
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
178
|
+
) {
|
|
179
|
+
value = value.slice(1, -1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
currentObj[key] = value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Serialize config to YAML-like format
|
|
191
|
+
*/
|
|
192
|
+
function serializeToYaml(config: Record<string, any>, indent = 0): string {
|
|
193
|
+
const lines: string[] = [];
|
|
194
|
+
const prefix = ' '.repeat(indent);
|
|
195
|
+
|
|
196
|
+
for (const [key, value] of Object.entries(config)) {
|
|
197
|
+
if (value === null || value === undefined) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
202
|
+
lines.push(`${prefix}${key}:`);
|
|
203
|
+
lines.push(serializeToYaml(value, indent + 1));
|
|
204
|
+
} else {
|
|
205
|
+
let serializedValue: string;
|
|
206
|
+
|
|
207
|
+
if (typeof value === 'string') {
|
|
208
|
+
// Quote strings that need it - escape backslashes and quotes
|
|
209
|
+
if (
|
|
210
|
+
value.includes(':') ||
|
|
211
|
+
value.includes('#') ||
|
|
212
|
+
value.includes("'") ||
|
|
213
|
+
value.includes('"') ||
|
|
214
|
+
value.includes('\\')
|
|
215
|
+
) {
|
|
216
|
+
serializedValue = `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
217
|
+
} else {
|
|
218
|
+
serializedValue = value;
|
|
219
|
+
}
|
|
220
|
+
} else if (typeof value === 'boolean') {
|
|
221
|
+
serializedValue = value ? 'true' : 'false';
|
|
222
|
+
} else {
|
|
223
|
+
serializedValue = String(value);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
lines.push(`${prefix}${key}: ${serializedValue}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* ConfigManager class for configuration persistence
|
|
235
|
+
*/
|
|
236
|
+
export class ConfigManager {
|
|
237
|
+
private configPath: string;
|
|
238
|
+
private config: NimbusConfig | null = null;
|
|
239
|
+
|
|
240
|
+
constructor(configPath?: string) {
|
|
241
|
+
this.configPath = configPath || path.join(os.homedir(), '.nimbus', 'config.yaml');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Deep merge two objects, with source values taking precedence
|
|
246
|
+
*/
|
|
247
|
+
private deepMerge(target: any, source: any): any {
|
|
248
|
+
const result = { ...target };
|
|
249
|
+
for (const key of Object.keys(source)) {
|
|
250
|
+
if (
|
|
251
|
+
source[key] &&
|
|
252
|
+
typeof source[key] === 'object' &&
|
|
253
|
+
!Array.isArray(source[key]) &&
|
|
254
|
+
target[key] &&
|
|
255
|
+
typeof target[key] === 'object' &&
|
|
256
|
+
!Array.isArray(target[key])
|
|
257
|
+
) {
|
|
258
|
+
result[key] = this.deepMerge(target[key], source[key]);
|
|
259
|
+
} else if (source[key] !== undefined) {
|
|
260
|
+
result[key] = source[key];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get the path to the config file
|
|
268
|
+
*/
|
|
269
|
+
getConfigPath(): string {
|
|
270
|
+
return this.configPath;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Ensure the config directory exists
|
|
275
|
+
*/
|
|
276
|
+
private ensureDirectory(): void {
|
|
277
|
+
const dir = path.dirname(this.configPath);
|
|
278
|
+
if (!fs.existsSync(dir)) {
|
|
279
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Load configuration from disk
|
|
285
|
+
*/
|
|
286
|
+
load(): NimbusConfig {
|
|
287
|
+
if (this.config) {
|
|
288
|
+
return this.config;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.ensureDirectory();
|
|
292
|
+
|
|
293
|
+
if (!fs.existsSync(this.configPath)) {
|
|
294
|
+
this.config = createDefaultConfig();
|
|
295
|
+
return this.config;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
300
|
+
const parsed = resolveEnvVars(parseSimpleYaml(content));
|
|
301
|
+
|
|
302
|
+
// Validate with Zod schema
|
|
303
|
+
const parseResult = NimbusConfigSchema.safeParse(parsed);
|
|
304
|
+
const defaults = createDefaultConfig();
|
|
305
|
+
|
|
306
|
+
if (!parseResult.success) {
|
|
307
|
+
// Use defaults for invalid fields — don't crash
|
|
308
|
+
const merged = this.deepMerge(defaults, parsed);
|
|
309
|
+
this.config = merged as NimbusConfig;
|
|
310
|
+
} else {
|
|
311
|
+
this.config = this.deepMerge(defaults, parseResult.data) as NimbusConfig;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.config.version = CONFIG_VERSION;
|
|
315
|
+
return this.config;
|
|
316
|
+
} catch {
|
|
317
|
+
// If file is corrupted, start fresh
|
|
318
|
+
this.config = createDefaultConfig();
|
|
319
|
+
return this.config;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Save configuration to disk
|
|
325
|
+
*/
|
|
326
|
+
save(config?: NimbusConfig): void {
|
|
327
|
+
this.ensureDirectory();
|
|
328
|
+
|
|
329
|
+
const configToSave = config || this.config;
|
|
330
|
+
if (!configToSave) {
|
|
331
|
+
throw new Error('No config to save');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.config = configToSave;
|
|
335
|
+
|
|
336
|
+
const header = `# Nimbus CLI Configuration
|
|
337
|
+
# Version: ${CONFIG_VERSION}
|
|
338
|
+
# Documentation: https://github.com/the-ai-project-co/nimbus
|
|
339
|
+
#
|
|
340
|
+
# Edit this file to customize Nimbus behavior.
|
|
341
|
+
# Run 'nimbus config list' to see all available options.
|
|
342
|
+
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
const content = header + serializeToYaml(configToSave);
|
|
346
|
+
fs.writeFileSync(this.configPath, content, { mode: 0o600 });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if config file exists
|
|
351
|
+
*/
|
|
352
|
+
exists(): boolean {
|
|
353
|
+
return fs.existsSync(this.configPath);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get a configuration value by dot-notation key
|
|
358
|
+
*/
|
|
359
|
+
get(key: string): any {
|
|
360
|
+
const config = this.load();
|
|
361
|
+
const parts = key.split('.');
|
|
362
|
+
let value: any = config;
|
|
363
|
+
|
|
364
|
+
for (const part of parts) {
|
|
365
|
+
if (value === undefined || value === null) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
value = value[part];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Set a configuration value by dot-notation key
|
|
376
|
+
*/
|
|
377
|
+
set(key: string, value: any): void {
|
|
378
|
+
// Guard against prototype pollution
|
|
379
|
+
validateKeyPath(key);
|
|
380
|
+
|
|
381
|
+
const config = this.load();
|
|
382
|
+
const configCopy = JSON.parse(JSON.stringify(config));
|
|
383
|
+
const parts = key.split('.');
|
|
384
|
+
let obj: any = configCopy;
|
|
385
|
+
|
|
386
|
+
// Navigate to parent object
|
|
387
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
388
|
+
const part = parts[i];
|
|
389
|
+
if (obj[part] === undefined) {
|
|
390
|
+
// Use Object.create(null) to avoid prototype chain
|
|
391
|
+
obj[part] = Object.create(null);
|
|
392
|
+
}
|
|
393
|
+
obj = obj[part];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Set the value
|
|
397
|
+
const lastPart = parts[parts.length - 1];
|
|
398
|
+
obj[lastPart] = value;
|
|
399
|
+
|
|
400
|
+
// Validate the entire config after the change
|
|
401
|
+
const result = NimbusConfigSchema.safeParse(configCopy);
|
|
402
|
+
if (!result.success) {
|
|
403
|
+
const issue = result.error.issues[0];
|
|
404
|
+
throw new Error(`Invalid value for '${key}': ${issue?.message || 'validation failed'}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Apply to the real config and save
|
|
408
|
+
let realObj: any = config;
|
|
409
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
410
|
+
const part = parts[i];
|
|
411
|
+
if (realObj[part] === undefined) {
|
|
412
|
+
realObj[part] = Object.create(null);
|
|
413
|
+
}
|
|
414
|
+
realObj = realObj[part];
|
|
415
|
+
}
|
|
416
|
+
realObj[lastPart] = value;
|
|
417
|
+
|
|
418
|
+
this.save(config);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Delete a configuration value by dot-notation key
|
|
423
|
+
*/
|
|
424
|
+
delete(key: string): void {
|
|
425
|
+
// Guard against prototype pollution
|
|
426
|
+
validateKeyPath(key);
|
|
427
|
+
|
|
428
|
+
const config = this.load();
|
|
429
|
+
const parts = key.split('.');
|
|
430
|
+
let obj: any = config;
|
|
431
|
+
|
|
432
|
+
// Navigate to parent object
|
|
433
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
434
|
+
const part = parts[i];
|
|
435
|
+
if (!Object.prototype.hasOwnProperty.call(obj, part) || obj[part] === undefined) {
|
|
436
|
+
return; // Key doesn't exist
|
|
437
|
+
}
|
|
438
|
+
obj = obj[part];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Delete the value
|
|
442
|
+
const lastPart = parts[parts.length - 1];
|
|
443
|
+
delete obj[lastPart];
|
|
444
|
+
|
|
445
|
+
this.save(config);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get all configuration as flat key-value pairs
|
|
450
|
+
*/
|
|
451
|
+
getAllFlat(): Record<string, any> {
|
|
452
|
+
const config = this.load();
|
|
453
|
+
const result: Record<string, any> = {};
|
|
454
|
+
|
|
455
|
+
function flatten(obj: any, prefix = '') {
|
|
456
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
457
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
458
|
+
|
|
459
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
460
|
+
flatten(value, fullKey);
|
|
461
|
+
} else {
|
|
462
|
+
result[fullKey] = value;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
flatten(config);
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Reset configuration to defaults
|
|
473
|
+
*/
|
|
474
|
+
reset(): void {
|
|
475
|
+
this.config = createDefaultConfig();
|
|
476
|
+
this.save();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Reload configuration from disk
|
|
481
|
+
*/
|
|
482
|
+
reload(): NimbusConfig {
|
|
483
|
+
this.config = null;
|
|
484
|
+
return this.load();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get config key info
|
|
489
|
+
*/
|
|
490
|
+
getKeyInfo(key: string): (typeof CONFIG_KEYS)[number] | undefined {
|
|
491
|
+
return CONFIG_KEYS.find(k => k.key === key);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Validate a key exists in the schema
|
|
496
|
+
*/
|
|
497
|
+
isValidKey(key: string): boolean {
|
|
498
|
+
return CONFIG_KEYS.some(k => k.key === key);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Parse a value according to the key's type
|
|
503
|
+
*/
|
|
504
|
+
parseValue(key: string, value: string): any {
|
|
505
|
+
const keyInfo = this.getKeyInfo(key);
|
|
506
|
+
if (!keyInfo) {
|
|
507
|
+
// Unknown key, return as string
|
|
508
|
+
return value;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
switch (keyInfo.type) {
|
|
512
|
+
case 'boolean':
|
|
513
|
+
return value.toLowerCase() === 'true';
|
|
514
|
+
case 'number':
|
|
515
|
+
return Number(value);
|
|
516
|
+
case 'string':
|
|
517
|
+
default:
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Export singleton instance
|
|
525
|
+
*/
|
|
526
|
+
export const configManager = new ConfigManager();
|
|
527
|
+
|
|
528
|
+
// Re-export types
|
|
529
|
+
export { CONFIG_KEYS };
|
|
530
|
+
export type { ConfigKey };
|