@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
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Initialization & Auto-Detection
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds a new Nimbus project by detecting the existing project type,
|
|
5
|
+
* infrastructure tooling, cloud providers, and development conventions.
|
|
6
|
+
* Generates a NIMBUS.md file and .nimbus/ configuration directory.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* nimbus init
|
|
10
|
+
* nimbus init --force # overwrite existing NIMBUS.md
|
|
11
|
+
* nimbus init --quiet # suppress console output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { exec } from 'node:child_process';
|
|
17
|
+
import { promisify } from 'node:util';
|
|
18
|
+
|
|
19
|
+
const _execAsync = promisify(exec);
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Detected language / runtime of the project */
|
|
26
|
+
export type ProjectType =
|
|
27
|
+
| 'typescript'
|
|
28
|
+
| 'javascript'
|
|
29
|
+
| 'go'
|
|
30
|
+
| 'python'
|
|
31
|
+
| 'rust'
|
|
32
|
+
| 'java'
|
|
33
|
+
| 'unknown';
|
|
34
|
+
|
|
35
|
+
/** Infrastructure tool category */
|
|
36
|
+
export type InfraType = 'terraform' | 'kubernetes' | 'helm' | 'docker' | 'cicd';
|
|
37
|
+
|
|
38
|
+
/** Cloud provider identifier */
|
|
39
|
+
export type CloudProvider = 'aws' | 'gcp' | 'azure';
|
|
40
|
+
|
|
41
|
+
/** Complete detection result for a project directory */
|
|
42
|
+
export interface ProjectDetection {
|
|
43
|
+
/** Inferred project name (directory basename) */
|
|
44
|
+
projectName: string;
|
|
45
|
+
/** Primary language / runtime */
|
|
46
|
+
projectType: ProjectType;
|
|
47
|
+
/** Infrastructure tools found */
|
|
48
|
+
infraTypes: InfraType[];
|
|
49
|
+
/** Cloud providers detected from config or Terraform */
|
|
50
|
+
cloudProviders: CloudProvider[];
|
|
51
|
+
/** Whether the directory is a git repository */
|
|
52
|
+
hasGit: boolean;
|
|
53
|
+
/** Node.js package manager, if applicable */
|
|
54
|
+
packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
|
|
55
|
+
/** Test framework name, if detected */
|
|
56
|
+
testFramework?: string;
|
|
57
|
+
/** Linter name, if detected */
|
|
58
|
+
linter?: string;
|
|
59
|
+
/** Formatter name, if detected */
|
|
60
|
+
formatter?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Options accepted by {@link runInit} */
|
|
64
|
+
export interface InitOptions {
|
|
65
|
+
/** Working directory (defaults to `process.cwd()`) */
|
|
66
|
+
cwd?: string;
|
|
67
|
+
/** Overwrite an existing NIMBUS.md without prompting */
|
|
68
|
+
force?: boolean;
|
|
69
|
+
/** Suppress all console output */
|
|
70
|
+
quiet?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Value returned by {@link runInit} on success */
|
|
74
|
+
export interface InitResult {
|
|
75
|
+
/** Full detection results */
|
|
76
|
+
detection: ProjectDetection;
|
|
77
|
+
/** Absolute paths of files created during init */
|
|
78
|
+
filesCreated: string[];
|
|
79
|
+
/** Absolute path to the generated NIMBUS.md */
|
|
80
|
+
nimbusmdPath: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Internal helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check whether a file or directory exists at `filePath`.
|
|
89
|
+
* Swallows all errors and returns `false` on failure.
|
|
90
|
+
*/
|
|
91
|
+
function exists(filePath: string): boolean {
|
|
92
|
+
try {
|
|
93
|
+
return fs.existsSync(filePath);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List immediate children of `dir`, returning an empty array when the
|
|
101
|
+
* directory does not exist or is unreadable.
|
|
102
|
+
*/
|
|
103
|
+
function listDir(dir: string): string[] {
|
|
104
|
+
try {
|
|
105
|
+
return fs.readdirSync(dir);
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read a file as UTF-8 text. Returns an empty string on failure.
|
|
113
|
+
*/
|
|
114
|
+
function readText(filePath: string): string {
|
|
115
|
+
try {
|
|
116
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
117
|
+
} catch {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Recursively collect file names matching a predicate.
|
|
124
|
+
* Searches at most `maxDepth` levels deep and stops after `limit` matches.
|
|
125
|
+
*/
|
|
126
|
+
function findFiles(
|
|
127
|
+
dir: string,
|
|
128
|
+
predicate: (name: string) => boolean,
|
|
129
|
+
maxDepth = 3,
|
|
130
|
+
limit = 50
|
|
131
|
+
): string[] {
|
|
132
|
+
const results: string[] = [];
|
|
133
|
+
|
|
134
|
+
function walk(current: string, depth: number): void {
|
|
135
|
+
if (depth > maxDepth || results.length >= limit) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const entry of listDir(current)) {
|
|
140
|
+
if (results.length >= limit) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Skip heavy directories that would slow detection
|
|
145
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'vendor') {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const full = path.join(current, entry);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const stat = fs.statSync(full);
|
|
153
|
+
if (stat.isDirectory()) {
|
|
154
|
+
walk(full, depth + 1);
|
|
155
|
+
} else if (predicate(entry)) {
|
|
156
|
+
results.push(full);
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Permission or broken symlink -- skip
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
walk(dir, 0);
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Detection functions
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Detect the primary project type from marker files in `dir`.
|
|
174
|
+
*
|
|
175
|
+
* Priority order: TypeScript > JavaScript > Go > Python > Rust > Java > unknown
|
|
176
|
+
*/
|
|
177
|
+
export function detectProjectType(dir: string): ProjectType {
|
|
178
|
+
try {
|
|
179
|
+
if (exists(path.join(dir, 'tsconfig.json'))) {
|
|
180
|
+
return 'typescript';
|
|
181
|
+
}
|
|
182
|
+
if (exists(path.join(dir, 'package.json'))) {
|
|
183
|
+
return 'javascript';
|
|
184
|
+
}
|
|
185
|
+
if (exists(path.join(dir, 'go.mod'))) {
|
|
186
|
+
return 'go';
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
exists(path.join(dir, 'pyproject.toml')) ||
|
|
190
|
+
exists(path.join(dir, 'setup.py')) ||
|
|
191
|
+
exists(path.join(dir, 'requirements.txt'))
|
|
192
|
+
) {
|
|
193
|
+
return 'python';
|
|
194
|
+
}
|
|
195
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
196
|
+
return 'rust';
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
exists(path.join(dir, 'pom.xml')) ||
|
|
200
|
+
exists(path.join(dir, 'build.gradle')) ||
|
|
201
|
+
exists(path.join(dir, 'build.gradle.kts'))
|
|
202
|
+
) {
|
|
203
|
+
return 'java';
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Fall through to unknown
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return 'unknown';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Detect which infrastructure tools are present in `dir`.
|
|
214
|
+
*
|
|
215
|
+
* Scans for Terraform files, Kubernetes manifests, Helm charts,
|
|
216
|
+
* Docker files, and CI/CD configuration.
|
|
217
|
+
*/
|
|
218
|
+
export function detectInfrastructure(dir: string): InfraType[] {
|
|
219
|
+
const found: Set<InfraType> = new Set();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Terraform -- look for any .tf files
|
|
223
|
+
const tfFiles = findFiles(dir, name => name.endsWith('.tf'), 3, 5);
|
|
224
|
+
if (tfFiles.length > 0) {
|
|
225
|
+
found.add('terraform');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Kubernetes -- look for YAML files containing common K8s markers
|
|
229
|
+
const yamlFiles = findFiles(
|
|
230
|
+
dir,
|
|
231
|
+
name => name.endsWith('.yaml') || name.endsWith('.yml'),
|
|
232
|
+
3,
|
|
233
|
+
30
|
|
234
|
+
);
|
|
235
|
+
for (const yamlFile of yamlFiles) {
|
|
236
|
+
const content = readText(yamlFile);
|
|
237
|
+
if (
|
|
238
|
+
content.includes('kind: Deployment') ||
|
|
239
|
+
content.includes('kind: Service') ||
|
|
240
|
+
content.includes('apiVersion:')
|
|
241
|
+
) {
|
|
242
|
+
found.add('kubernetes');
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Helm
|
|
248
|
+
if (findFiles(dir, name => name === 'Chart.yaml', 3, 1).length > 0) {
|
|
249
|
+
found.add('helm');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Docker
|
|
253
|
+
const entries = listDir(dir);
|
|
254
|
+
if (
|
|
255
|
+
entries.some(
|
|
256
|
+
e => e === 'Dockerfile' || e === 'docker-compose.yml' || e === 'docker-compose.yaml'
|
|
257
|
+
)
|
|
258
|
+
) {
|
|
259
|
+
found.add('docker');
|
|
260
|
+
}
|
|
261
|
+
// Also check for a docker/ directory with Dockerfiles
|
|
262
|
+
if (exists(path.join(dir, 'docker'))) {
|
|
263
|
+
const dockerDir = listDir(path.join(dir, 'docker'));
|
|
264
|
+
if (
|
|
265
|
+
dockerDir.some(e => e.startsWith('Dockerfile') || e.endsWith('.yml') || e.endsWith('.yaml'))
|
|
266
|
+
) {
|
|
267
|
+
found.add('docker');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// CI/CD
|
|
272
|
+
if (
|
|
273
|
+
exists(path.join(dir, '.github', 'workflows')) ||
|
|
274
|
+
exists(path.join(dir, '.gitlab-ci.yml')) ||
|
|
275
|
+
exists(path.join(dir, 'Jenkinsfile')) ||
|
|
276
|
+
exists(path.join(dir, '.circleci'))
|
|
277
|
+
) {
|
|
278
|
+
found.add('cicd');
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// Return whatever we collected so far
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return Array.from(found);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Detect cloud providers referenced in Terraform files or local credentials.
|
|
289
|
+
*
|
|
290
|
+
* Checks both `.tf` file contents and well-known credential locations
|
|
291
|
+
* or environment variables.
|
|
292
|
+
*/
|
|
293
|
+
export function detectCloudProviders(dir: string): CloudProvider[] {
|
|
294
|
+
const found: Set<CloudProvider> = new Set();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// --- Scan Terraform files for provider blocks ---
|
|
298
|
+
const tfFiles = findFiles(dir, name => name.endsWith('.tf'), 3, 20);
|
|
299
|
+
|
|
300
|
+
for (const tfFile of tfFiles) {
|
|
301
|
+
const content = readText(tfFile);
|
|
302
|
+
|
|
303
|
+
if (content.includes('provider "aws"') || content.includes("provider 'aws'")) {
|
|
304
|
+
found.add('aws');
|
|
305
|
+
}
|
|
306
|
+
if (content.includes('provider "google"') || content.includes("provider 'google'")) {
|
|
307
|
+
found.add('gcp');
|
|
308
|
+
}
|
|
309
|
+
if (content.includes('provider "azurerm"') || content.includes("provider 'azurerm'")) {
|
|
310
|
+
found.add('azure');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- Check environment variables ---
|
|
315
|
+
if (process.env['AWS_ACCESS_KEY_ID'] || process.env['AWS_PROFILE']) {
|
|
316
|
+
found.add('aws');
|
|
317
|
+
}
|
|
318
|
+
if (process.env['GOOGLE_APPLICATION_CREDENTIALS'] || process.env['GCLOUD_PROJECT']) {
|
|
319
|
+
found.add('gcp');
|
|
320
|
+
}
|
|
321
|
+
if (process.env['AZURE_SUBSCRIPTION_ID'] || process.env['ARM_SUBSCRIPTION_ID']) {
|
|
322
|
+
found.add('azure');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Check local credential files ---
|
|
326
|
+
const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
|
|
327
|
+
if (home) {
|
|
328
|
+
if (
|
|
329
|
+
exists(path.join(home, '.aws', 'credentials')) ||
|
|
330
|
+
exists(path.join(home, '.aws', 'config'))
|
|
331
|
+
) {
|
|
332
|
+
found.add('aws');
|
|
333
|
+
}
|
|
334
|
+
if (exists(path.join(home, '.config', 'gcloud'))) {
|
|
335
|
+
found.add('gcp');
|
|
336
|
+
}
|
|
337
|
+
if (exists(path.join(home, '.azure'))) {
|
|
338
|
+
found.add('azure');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
// Return whatever we collected
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return Array.from(found);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Detect which Node.js package manager is used in `dir`.
|
|
350
|
+
*
|
|
351
|
+
* Lock-file priority: bun > yarn > pnpm > npm.
|
|
352
|
+
* Returns `undefined` when no lock file is found.
|
|
353
|
+
*/
|
|
354
|
+
export function detectPackageManager(dir: string): 'npm' | 'yarn' | 'pnpm' | 'bun' | undefined {
|
|
355
|
+
try {
|
|
356
|
+
if (exists(path.join(dir, 'bun.lock')) || exists(path.join(dir, 'bun.lockb'))) {
|
|
357
|
+
return 'bun';
|
|
358
|
+
}
|
|
359
|
+
if (exists(path.join(dir, 'yarn.lock'))) {
|
|
360
|
+
return 'yarn';
|
|
361
|
+
}
|
|
362
|
+
if (exists(path.join(dir, 'pnpm-lock.yaml'))) {
|
|
363
|
+
return 'pnpm';
|
|
364
|
+
}
|
|
365
|
+
if (exists(path.join(dir, 'package-lock.json'))) {
|
|
366
|
+
return 'npm';
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
// fall through
|
|
370
|
+
}
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Detect the test framework from `package.json` dependencies or
|
|
376
|
+
* lock-file presence.
|
|
377
|
+
*
|
|
378
|
+
* Returns the framework name as a human-readable string, or `undefined`
|
|
379
|
+
* if none is detected.
|
|
380
|
+
*/
|
|
381
|
+
export function detectTestFramework(dir: string): string | undefined {
|
|
382
|
+
try {
|
|
383
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
384
|
+
if (!exists(pkgPath)) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const pkg = JSON.parse(readText(pkgPath)) as Record<string, unknown>;
|
|
389
|
+
const allDeps = {
|
|
390
|
+
...(pkg['dependencies'] as Record<string, string> | undefined),
|
|
391
|
+
...(pkg['devDependencies'] as Record<string, string> | undefined),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if ('vitest' in allDeps) {
|
|
395
|
+
return 'vitest';
|
|
396
|
+
}
|
|
397
|
+
if ('jest' in allDeps) {
|
|
398
|
+
return 'jest';
|
|
399
|
+
}
|
|
400
|
+
if ('mocha' in allDeps) {
|
|
401
|
+
return 'mocha';
|
|
402
|
+
}
|
|
403
|
+
if ('@playwright/test' in allDeps) {
|
|
404
|
+
return 'playwright';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Bun ships its own test runner -- detect via lock file
|
|
408
|
+
if (exists(path.join(dir, 'bun.lock')) || exists(path.join(dir, 'bun.lockb'))) {
|
|
409
|
+
return 'bun:test';
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
// fall through
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Non-JS projects
|
|
416
|
+
if (exists(path.join(dir, 'go.mod'))) {
|
|
417
|
+
return 'go test';
|
|
418
|
+
}
|
|
419
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
420
|
+
return 'cargo test';
|
|
421
|
+
}
|
|
422
|
+
if (exists(path.join(dir, 'pyproject.toml')) || exists(path.join(dir, 'setup.py'))) {
|
|
423
|
+
const pyproject = readText(path.join(dir, 'pyproject.toml'));
|
|
424
|
+
if (pyproject.includes('pytest')) {
|
|
425
|
+
return 'pytest';
|
|
426
|
+
}
|
|
427
|
+
if (exists(path.join(dir, 'pytest.ini')) || exists(path.join(dir, 'setup.cfg'))) {
|
|
428
|
+
return 'pytest';
|
|
429
|
+
}
|
|
430
|
+
return 'unittest';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Detect the linter used in the project.
|
|
438
|
+
*
|
|
439
|
+
* Checks for ESLint, Biome, golangci-lint, Ruff, and Clippy configuration.
|
|
440
|
+
*/
|
|
441
|
+
export function detectLinter(dir: string): string | undefined {
|
|
442
|
+
try {
|
|
443
|
+
const entries = listDir(dir);
|
|
444
|
+
|
|
445
|
+
// ESLint (various config formats)
|
|
446
|
+
if (entries.some(e => e.startsWith('.eslintrc') || e.startsWith('eslint.config'))) {
|
|
447
|
+
return 'eslint';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Biome
|
|
451
|
+
if (entries.includes('biome.json') || entries.includes('biome.jsonc')) {
|
|
452
|
+
return 'biome';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Go -- golangci-lint
|
|
456
|
+
if (entries.includes('.golangci.yml') || entries.includes('.golangci.yaml')) {
|
|
457
|
+
return 'golangci-lint';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Python -- ruff
|
|
461
|
+
if (entries.includes('ruff.toml') || entries.includes('.ruff.toml')) {
|
|
462
|
+
return 'ruff';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Check pyproject.toml for ruff or flake8
|
|
466
|
+
if (exists(path.join(dir, 'pyproject.toml'))) {
|
|
467
|
+
const pyproject = readText(path.join(dir, 'pyproject.toml'));
|
|
468
|
+
if (pyproject.includes('[tool.ruff]')) {
|
|
469
|
+
return 'ruff';
|
|
470
|
+
}
|
|
471
|
+
if (pyproject.includes('[tool.flake8]')) {
|
|
472
|
+
return 'flake8';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Rust -- clippy is part of the toolchain, detect via Cargo.toml
|
|
477
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
478
|
+
return 'clippy';
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
// fall through
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Detect the code formatter used in the project.
|
|
488
|
+
*
|
|
489
|
+
* Checks for Prettier, Biome, gofmt, rustfmt, and Black configuration.
|
|
490
|
+
*/
|
|
491
|
+
export function detectFormatter(dir: string): string | undefined {
|
|
492
|
+
try {
|
|
493
|
+
const entries = listDir(dir);
|
|
494
|
+
|
|
495
|
+
// Prettier
|
|
496
|
+
if (entries.some(e => e.startsWith('.prettierrc') || e.startsWith('prettier.config'))) {
|
|
497
|
+
return 'prettier';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Biome doubles as formatter
|
|
501
|
+
if (entries.includes('biome.json') || entries.includes('biome.jsonc')) {
|
|
502
|
+
return 'biome';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Go -- gofmt is built-in
|
|
506
|
+
if (exists(path.join(dir, 'go.mod'))) {
|
|
507
|
+
return 'gofmt';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Rust -- rustfmt
|
|
511
|
+
if (exists(path.join(dir, 'rustfmt.toml')) || exists(path.join(dir, '.rustfmt.toml'))) {
|
|
512
|
+
return 'rustfmt';
|
|
513
|
+
}
|
|
514
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
515
|
+
return 'rustfmt';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Python -- black / ruff format
|
|
519
|
+
if (exists(path.join(dir, 'pyproject.toml'))) {
|
|
520
|
+
const pyproject = readText(path.join(dir, 'pyproject.toml'));
|
|
521
|
+
if (pyproject.includes('[tool.black]')) {
|
|
522
|
+
return 'black';
|
|
523
|
+
}
|
|
524
|
+
if (pyproject.includes('[tool.ruff]')) {
|
|
525
|
+
return 'ruff';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
// fall through
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Run the full project detection pipeline on `dir`.
|
|
536
|
+
*
|
|
537
|
+
* Aggregates results from all individual detection functions into a
|
|
538
|
+
* single {@link ProjectDetection} object.
|
|
539
|
+
*/
|
|
540
|
+
export function detectProject(dir: string): ProjectDetection {
|
|
541
|
+
const resolvedDir = path.resolve(dir);
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
projectName: path.basename(resolvedDir),
|
|
545
|
+
projectType: detectProjectType(resolvedDir),
|
|
546
|
+
infraTypes: detectInfrastructure(resolvedDir),
|
|
547
|
+
cloudProviders: detectCloudProviders(resolvedDir),
|
|
548
|
+
hasGit: exists(path.join(resolvedDir, '.git')),
|
|
549
|
+
packageManager: detectPackageManager(resolvedDir),
|
|
550
|
+
testFramework: detectTestFramework(resolvedDir),
|
|
551
|
+
linter: detectLinter(resolvedDir),
|
|
552
|
+
formatter: detectFormatter(resolvedDir),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Generation
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Produce the contents of a `NIMBUS.md` file from detection results.
|
|
562
|
+
*
|
|
563
|
+
* The generated markdown serves as both human-readable documentation
|
|
564
|
+
* and machine-readable project metadata for the Nimbus agent.
|
|
565
|
+
*/
|
|
566
|
+
export function generateNimbusMd(detection: ProjectDetection, _dir: string): string {
|
|
567
|
+
const lines: string[] = [];
|
|
568
|
+
|
|
569
|
+
// --- Header ---
|
|
570
|
+
lines.push(`# ${detection.projectName}`);
|
|
571
|
+
lines.push('');
|
|
572
|
+
lines.push('> Auto-generated by `nimbus init`. Edit freely to refine agent behaviour.');
|
|
573
|
+
lines.push('');
|
|
574
|
+
|
|
575
|
+
// --- Project Overview ---
|
|
576
|
+
lines.push('## Project Overview');
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push(`- **Type:** ${detection.projectType}`);
|
|
579
|
+
if (detection.packageManager) {
|
|
580
|
+
lines.push(`- **Package Manager:** ${detection.packageManager}`);
|
|
581
|
+
}
|
|
582
|
+
if (detection.testFramework) {
|
|
583
|
+
lines.push(`- **Test Framework:** ${detection.testFramework}`);
|
|
584
|
+
}
|
|
585
|
+
if (detection.hasGit) {
|
|
586
|
+
lines.push('- **Version Control:** git');
|
|
587
|
+
}
|
|
588
|
+
lines.push('');
|
|
589
|
+
|
|
590
|
+
// --- Infrastructure ---
|
|
591
|
+
if (detection.infraTypes.length > 0 || detection.cloudProviders.length > 0) {
|
|
592
|
+
lines.push('## Infrastructure');
|
|
593
|
+
lines.push('');
|
|
594
|
+
if (detection.infraTypes.length > 0) {
|
|
595
|
+
lines.push(`- **Tools:** ${detection.infraTypes.join(', ')}`);
|
|
596
|
+
}
|
|
597
|
+
if (detection.cloudProviders.length > 0) {
|
|
598
|
+
lines.push(`- **Cloud Providers:** ${detection.cloudProviders.join(', ')}`);
|
|
599
|
+
}
|
|
600
|
+
lines.push('');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// --- Conventions ---
|
|
604
|
+
if (detection.linter || detection.formatter) {
|
|
605
|
+
lines.push('## Conventions');
|
|
606
|
+
lines.push('');
|
|
607
|
+
if (detection.linter) {
|
|
608
|
+
lines.push(`- **Linter:** ${detection.linter}`);
|
|
609
|
+
}
|
|
610
|
+
if (detection.formatter) {
|
|
611
|
+
lines.push(`- **Formatter:** ${detection.formatter}`);
|
|
612
|
+
}
|
|
613
|
+
lines.push('');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// --- Safety Rules ---
|
|
617
|
+
lines.push('## Safety Rules');
|
|
618
|
+
lines.push('');
|
|
619
|
+
lines.push('- Protected branches: `main`, `master`');
|
|
620
|
+
lines.push('- Protected Kubernetes namespaces: `production`, `kube-system`');
|
|
621
|
+
lines.push('- Always preview before `terraform apply`');
|
|
622
|
+
lines.push('- Run tests before committing');
|
|
623
|
+
lines.push('- Never store secrets in source control');
|
|
624
|
+
lines.push('');
|
|
625
|
+
|
|
626
|
+
// --- Custom Instructions ---
|
|
627
|
+
lines.push('## Custom Instructions');
|
|
628
|
+
lines.push('');
|
|
629
|
+
lines.push('<!-- Add project-specific instructions for the Nimbus agent here -->');
|
|
630
|
+
lines.push('');
|
|
631
|
+
|
|
632
|
+
return lines.join('\n');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Generate the contents of `.nimbus/config.yaml`.
|
|
637
|
+
*
|
|
638
|
+
* Produces a valid YAML string without requiring an external YAML library.
|
|
639
|
+
*/
|
|
640
|
+
function generateConfigYaml(detection: ProjectDetection): string {
|
|
641
|
+
const lines: string[] = [];
|
|
642
|
+
|
|
643
|
+
lines.push('# Nimbus project configuration');
|
|
644
|
+
lines.push('# See https://nimbus.dev/docs/config for all options');
|
|
645
|
+
lines.push('');
|
|
646
|
+
lines.push('# Default LLM model for agent interactions');
|
|
647
|
+
lines.push('default_model: anthropic/claude-sonnet-4');
|
|
648
|
+
lines.push('');
|
|
649
|
+
lines.push('# Default agent mode: build | plan | debug | review');
|
|
650
|
+
lines.push('default_mode: build');
|
|
651
|
+
lines.push('');
|
|
652
|
+
lines.push('# Project metadata');
|
|
653
|
+
lines.push('project:');
|
|
654
|
+
lines.push(` name: ${detection.projectName}`);
|
|
655
|
+
lines.push(` type: ${detection.projectType}`);
|
|
656
|
+
if (detection.packageManager) {
|
|
657
|
+
lines.push(` package_manager: ${detection.packageManager}`);
|
|
658
|
+
}
|
|
659
|
+
lines.push('');
|
|
660
|
+
|
|
661
|
+
// Permissions
|
|
662
|
+
lines.push('# Permission rules control what the agent can do without asking');
|
|
663
|
+
lines.push('permissions:');
|
|
664
|
+
lines.push(' # File operations');
|
|
665
|
+
lines.push(' file_read: allow');
|
|
666
|
+
lines.push(' file_write: ask');
|
|
667
|
+
lines.push(' file_delete: deny');
|
|
668
|
+
lines.push('');
|
|
669
|
+
lines.push(' # Shell commands');
|
|
670
|
+
lines.push(' shell_read: allow # non-destructive commands (ls, cat, git status)');
|
|
671
|
+
lines.push(' shell_write: ask # potentially destructive commands');
|
|
672
|
+
lines.push('');
|
|
673
|
+
lines.push(' # Git operations');
|
|
674
|
+
lines.push(' git_read: allow');
|
|
675
|
+
lines.push(' git_write: ask');
|
|
676
|
+
lines.push('');
|
|
677
|
+
lines.push(' # Infrastructure operations');
|
|
678
|
+
lines.push(' terraform_plan: allow');
|
|
679
|
+
lines.push(' terraform_apply: deny');
|
|
680
|
+
lines.push(' kubectl_read: allow');
|
|
681
|
+
lines.push(' kubectl_write: deny');
|
|
682
|
+
lines.push('');
|
|
683
|
+
|
|
684
|
+
// Safety
|
|
685
|
+
lines.push('# Safety settings');
|
|
686
|
+
lines.push('safety:');
|
|
687
|
+
lines.push(' protected_branches:');
|
|
688
|
+
lines.push(' - main');
|
|
689
|
+
lines.push(' - master');
|
|
690
|
+
lines.push(' protected_k8s_namespaces:');
|
|
691
|
+
lines.push(' - production');
|
|
692
|
+
lines.push(' - kube-system');
|
|
693
|
+
lines.push(' require_plan_before_apply: true');
|
|
694
|
+
lines.push(' require_tests_before_commit: true');
|
|
695
|
+
lines.push('');
|
|
696
|
+
|
|
697
|
+
return lines.join('\n');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
// Main init
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Initialize a Nimbus project in the given directory.
|
|
706
|
+
*
|
|
707
|
+
* Creates the `.nimbus/` directory structure and a `NIMBUS.md` file
|
|
708
|
+
* populated with auto-detected project metadata.
|
|
709
|
+
*
|
|
710
|
+
* @param options - Configuration for the init process
|
|
711
|
+
* @returns The detection results and list of created files
|
|
712
|
+
*
|
|
713
|
+
* @example
|
|
714
|
+
* ```ts
|
|
715
|
+
* const result = await runInit({ cwd: '/path/to/project' });
|
|
716
|
+
* console.log(result.detection.projectType); // 'typescript'
|
|
717
|
+
* console.log(result.filesCreated); // ['.nimbus/config.yaml', ...]
|
|
718
|
+
* ```
|
|
719
|
+
*/
|
|
720
|
+
export async function runInit(options?: InitOptions): Promise<InitResult> {
|
|
721
|
+
const dir = path.resolve(options?.cwd ?? process.cwd());
|
|
722
|
+
const force = options?.force ?? false;
|
|
723
|
+
const quiet = options?.quiet ?? false;
|
|
724
|
+
|
|
725
|
+
const log = (msg: string): void => {
|
|
726
|
+
if (!quiet) {
|
|
727
|
+
console.log(msg);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// ---- Step 1: Detect project characteristics ----
|
|
732
|
+
log('Detecting project...');
|
|
733
|
+
const detection = detectProject(dir);
|
|
734
|
+
|
|
735
|
+
log(` Project type: ${detection.projectType}`);
|
|
736
|
+
if (detection.packageManager) {
|
|
737
|
+
log(` Package manager: ${detection.packageManager}`);
|
|
738
|
+
}
|
|
739
|
+
if (detection.infraTypes.length > 0) {
|
|
740
|
+
log(` Infrastructure: ${detection.infraTypes.join(', ')}`);
|
|
741
|
+
}
|
|
742
|
+
if (detection.cloudProviders.length > 0) {
|
|
743
|
+
log(` Cloud providers: ${detection.cloudProviders.join(', ')}`);
|
|
744
|
+
}
|
|
745
|
+
if (detection.testFramework) {
|
|
746
|
+
log(` Test framework: ${detection.testFramework}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ---- Step 2: Check for existing NIMBUS.md ----
|
|
750
|
+
const nimbusmdPath = path.join(dir, 'NIMBUS.md');
|
|
751
|
+
if (exists(nimbusmdPath) && !force) {
|
|
752
|
+
throw new Error('NIMBUS.md already exists. Use --force to overwrite.');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ---- Step 3: Create .nimbus/ directory structure ----
|
|
756
|
+
const filesCreated: string[] = [];
|
|
757
|
+
const nimbusDirPath = path.join(dir, '.nimbus');
|
|
758
|
+
const hooksDirPath = path.join(nimbusDirPath, 'hooks');
|
|
759
|
+
const agentsDirPath = path.join(nimbusDirPath, 'agents');
|
|
760
|
+
|
|
761
|
+
if (!exists(nimbusDirPath)) {
|
|
762
|
+
fs.mkdirSync(nimbusDirPath, { recursive: true });
|
|
763
|
+
}
|
|
764
|
+
if (!exists(hooksDirPath)) {
|
|
765
|
+
fs.mkdirSync(hooksDirPath, { recursive: true });
|
|
766
|
+
}
|
|
767
|
+
if (!exists(agentsDirPath)) {
|
|
768
|
+
fs.mkdirSync(agentsDirPath, { recursive: true });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ---- Step 4: Create .nimbus/config.yaml ----
|
|
772
|
+
const configPath = path.join(nimbusDirPath, 'config.yaml');
|
|
773
|
+
if (!exists(configPath) || force) {
|
|
774
|
+
const configContent = generateConfigYaml(detection);
|
|
775
|
+
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
776
|
+
filesCreated.push(configPath);
|
|
777
|
+
log(' Created .nimbus/config.yaml');
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ---- Step 5: Create placeholder hook files ----
|
|
781
|
+
const preCommitHookPath = path.join(hooksDirPath, 'pre-commit.ts');
|
|
782
|
+
if (!exists(preCommitHookPath) || force) {
|
|
783
|
+
const preCommitContent = [
|
|
784
|
+
'/**',
|
|
785
|
+
' * Nimbus pre-commit hook',
|
|
786
|
+
' *',
|
|
787
|
+
' * Runs automatically before each commit when enabled.',
|
|
788
|
+
' * Add custom validation logic here.',
|
|
789
|
+
' */',
|
|
790
|
+
'',
|
|
791
|
+
'export default async function preCommit(): Promise<void> {',
|
|
792
|
+
' // Example: ensure tests pass before committing',
|
|
793
|
+
' // await $`bun test`;',
|
|
794
|
+
'}',
|
|
795
|
+
'',
|
|
796
|
+
].join('\n');
|
|
797
|
+
fs.writeFileSync(preCommitHookPath, preCommitContent, 'utf-8');
|
|
798
|
+
filesCreated.push(preCommitHookPath);
|
|
799
|
+
log(' Created .nimbus/hooks/pre-commit.ts');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ---- Step 6: Create placeholder agent config ----
|
|
803
|
+
const defaultAgentPath = path.join(agentsDirPath, 'default.yaml');
|
|
804
|
+
if (!exists(defaultAgentPath) || force) {
|
|
805
|
+
const agentContent = [
|
|
806
|
+
'# Default agent profile',
|
|
807
|
+
'# Customize the system prompt and tool access for this agent',
|
|
808
|
+
'',
|
|
809
|
+
'name: default',
|
|
810
|
+
'description: General-purpose Nimbus agent',
|
|
811
|
+
'',
|
|
812
|
+
'tools:',
|
|
813
|
+
' - file_read',
|
|
814
|
+
' - file_write',
|
|
815
|
+
' - shell',
|
|
816
|
+
' - git',
|
|
817
|
+
'',
|
|
818
|
+
'system_prompt: |',
|
|
819
|
+
` You are working on the ${detection.projectName} project.`,
|
|
820
|
+
` It is a ${detection.projectType} project.`,
|
|
821
|
+
' Follow the safety rules in NIMBUS.md.',
|
|
822
|
+
'',
|
|
823
|
+
].join('\n');
|
|
824
|
+
fs.writeFileSync(defaultAgentPath, agentContent, 'utf-8');
|
|
825
|
+
filesCreated.push(defaultAgentPath);
|
|
826
|
+
log(' Created .nimbus/agents/default.yaml');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ---- Step 7: Generate and write NIMBUS.md ----
|
|
830
|
+
const nimbusmdContent = generateNimbusMd(detection, dir);
|
|
831
|
+
fs.writeFileSync(nimbusmdPath, nimbusmdContent, 'utf-8');
|
|
832
|
+
filesCreated.push(nimbusmdPath);
|
|
833
|
+
log(' Created NIMBUS.md');
|
|
834
|
+
|
|
835
|
+
// ---- Step 8: Append .nimbus/ to .gitignore if not already present ----
|
|
836
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
837
|
+
if (exists(gitignorePath)) {
|
|
838
|
+
const gitignore = readText(gitignorePath);
|
|
839
|
+
if (!gitignore.includes('.nimbus/')) {
|
|
840
|
+
fs.appendFileSync(gitignorePath, '\n# Nimbus local config\n.nimbus/\n', 'utf-8');
|
|
841
|
+
log(' Updated .gitignore');
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
log('');
|
|
846
|
+
log('Nimbus project initialized successfully.');
|
|
847
|
+
log('Edit NIMBUS.md to customise agent behaviour.');
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
detection,
|
|
851
|
+
filesCreated,
|
|
852
|
+
nimbusmdPath,
|
|
853
|
+
};
|
|
854
|
+
}
|