@mamdouh-aboammar/agentic-workflow 1.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/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +13 -0
- package/.skills.json +19 -0
- package/AGENTS.md +1344 -0
- package/CLAUDE.md +178 -0
- package/GEMINI.md +102 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/SKILL.md +132 -0
- package/bin/agentic-hooks.sh +79 -0
- package/bin/cli.js +1060 -0
- package/core/__init__.py +52 -0
- package/core/ai_evaluator.py +117 -0
- package/core/autopilot_engine.py +368 -0
- package/core/clean_code_guard.py +188 -0
- package/core/engine_py/__init__.py +29 -0
- package/core/engine_py/agent_worker.py +136 -0
- package/core/engine_py/decider.py +150 -0
- package/core/engine_py/energy.py +45 -0
- package/core/engine_py/event_bus.py +63 -0
- package/core/engine_py/executor.py +186 -0
- package/core/engine_py/models.py +193 -0
- package/core/engine_py/queue.py +314 -0
- package/core/engine_py/runner.py +116 -0
- package/core/engine_py/system_workers.py +70 -0
- package/core/engine_py/toon_adapter.py +586 -0
- package/core/engine_py/verification_controller.py +208 -0
- package/core/engine_py/worker.py +167 -0
- package/core/engine_spec/event_schema.json +65 -0
- package/core/engine_spec/example_workflow.yaml +73 -0
- package/core/engine_spec/workflow_schema.json +127 -0
- package/core/hooks/__init__.py +29 -0
- package/core/hooks/adapters/__init__.py +25 -0
- package/core/hooks/adapters/claude_adapter.py +83 -0
- package/core/hooks/adapters/cli_agent_adapter.py +82 -0
- package/core/hooks/adapters/codex_adapter.py +78 -0
- package/core/hooks/adapters/cursor_adapter.py +73 -0
- package/core/hooks/adapters/gemini_adapter.py +93 -0
- package/core/hooks/adapters/homebrew_adapter.py +69 -0
- package/core/hooks/adapters/mcp_proxy.py +133 -0
- package/core/hooks/adapters/shell_adapter.py +65 -0
- package/core/hooks/dispatcher.py +118 -0
- package/core/hooks/policy_engine.py +375 -0
- package/core/hooks/session_end.py +141 -0
- package/core/hooks/types.py +147 -0
- package/core/integrations/__init__.py +28 -0
- package/core/integrations/installer.py +225 -0
- package/core/integrations/lifecycle_director.py +175 -0
- package/core/integrations/registry.py +105 -0
- package/core/multi_agent_system.py +164 -0
- package/core/skills_indexer.py +742 -0
- package/core/system/__init__.py +25 -0
- package/core/system/announcements.py +72 -0
- package/core/system/dependencies.py +69 -0
- package/core/system/doctor.py +171 -0
- package/core/system/health.py +144 -0
- package/core/system/installer.py +137 -0
- package/core/system/notifications.py +97 -0
- package/core/system/refresher.py +110 -0
- package/core/system/updater.py +167 -0
- package/core/system/version_tracker.py +65 -0
- package/docs/architecture_plan.md +7 -0
- package/docs/guides/failure-recovery.md +714 -0
- package/docs/implementation_summary.md +10 -0
- package/docs/protocols/autopilot-execution.md +148 -0
- package/docs/protocols/code-change-protocol.md +49 -0
- package/docs/protocols/context-preservation-detail.md +114 -0
- package/docs/protocols/quality-gates.md +110 -0
- package/docs/protocols/ulw-mode.md +60 -0
- package/docs/research_findings.md +10 -0
- package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
- package/install.sh +111 -0
- package/marketplace.json +37 -0
- package/package.json +81 -0
- package/skills/agentic-workflow/SKILL.md +132 -0
- package/skills/agentic-workflow/skill-spec.json +100 -0
- package/soul.md +445 -0
- package/src/engine_ts/decider.ts +186 -0
- package/src/engine_ts/event-bus.ts +57 -0
- package/src/engine_ts/executor.ts +262 -0
- package/src/engine_ts/index.ts +12 -0
- package/src/engine_ts/queue.ts +93 -0
- package/src/engine_ts/runner.ts +108 -0
- package/src/engine_ts/skills-indexer.ts +264 -0
- package/src/engine_ts/toon-adapter.ts +91 -0
- package/src/engine_ts/types.ts +134 -0
- package/src/engine_ts/verification-controller.ts +204 -0
- package/src/engine_ts/worker.ts +280 -0
- package/src/hooks/adapters/claude-adapter.ts +54 -0
- package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
- package/src/hooks/adapters/codex-adapter.ts +69 -0
- package/src/hooks/adapters/cursor-adapter.ts +60 -0
- package/src/hooks/adapters/gemini-adapter.ts +71 -0
- package/src/hooks/adapters/homebrew-adapter.ts +36 -0
- package/src/hooks/adapters/mcp-proxy.ts +66 -0
- package/src/hooks/adapters/shell-adapter.ts +42 -0
- package/src/hooks/dispatcher.ts +113 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/policy-engine.ts +376 -0
- package/src/hooks/session-end.ts +125 -0
- package/src/hooks/types.ts +61 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +23 -0
- package/src/integrations/index.ts +7 -0
- package/src/integrations/installer.ts +208 -0
- package/src/integrations/lifecycle-director.ts +139 -0
- package/src/integrations/registry.ts +82 -0
- package/src/system/announcements.ts +143 -0
- package/src/system/dependencies.ts +176 -0
- package/src/system/doctor.ts +374 -0
- package/src/system/health.ts +270 -0
- package/src/system/index.ts +14 -0
- package/src/system/installer.ts +262 -0
- package/src/system/notifications.ts +180 -0
- package/src/system/refresher.ts +207 -0
- package/src/system/types.ts +268 -0
- package/src/system/updater.ts +219 -0
- package/src/system/version-tracker.ts +137 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/system/health.ts — Health Telemetry & Composite Scoring Engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import type { HealthReport, HealthScore, SystemVitals, WorkflowVitals, HealthGrade } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export class HealthEngine {
|
|
12
|
+
private projectDir: string;
|
|
13
|
+
|
|
14
|
+
constructor(projectDir: string = '.') {
|
|
15
|
+
this.projectDir = path.resolve(projectDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private getDiskUsage(): { freeBytes?: number; totalBytes?: number } {
|
|
19
|
+
try {
|
|
20
|
+
const out = execSync("df -k . | tail -1", { cwd: this.projectDir, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
21
|
+
const parts = out.split(/\s+/);
|
|
22
|
+
if (parts.length >= 4) {
|
|
23
|
+
const total = parseInt(parts[1], 10) * 1024;
|
|
24
|
+
const free = parseInt(parts[3], 10) * 1024;
|
|
25
|
+
return { totalBytes: total, freeBytes: free };
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignore
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public collectSystemVitals(): SystemVitals {
|
|
34
|
+
const totalMem = os.totalmem();
|
|
35
|
+
const freeMem = os.freemem();
|
|
36
|
+
const usedMem = totalMem - freeMem;
|
|
37
|
+
const usedPct = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0;
|
|
38
|
+
const rss = process.memoryUsage().rss;
|
|
39
|
+
|
|
40
|
+
let bunVersion: string | undefined;
|
|
41
|
+
try {
|
|
42
|
+
bunVersion = execSync('bun --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let pyVersion: string | undefined;
|
|
48
|
+
try {
|
|
49
|
+
pyVersion = execSync('python3 --version', { stdio: 'pipe', encoding: 'utf-8' }).trim().replace('Python ', '');
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
platform: os.platform(),
|
|
56
|
+
arch: os.arch(),
|
|
57
|
+
uptimeSeconds: Math.floor(os.uptime()),
|
|
58
|
+
memory: {
|
|
59
|
+
totalBytes: totalMem,
|
|
60
|
+
freeBytes: freeMem,
|
|
61
|
+
rssBytes: rss,
|
|
62
|
+
usedPercentage: usedPct
|
|
63
|
+
},
|
|
64
|
+
disk: this.getDiskUsage(),
|
|
65
|
+
nodeVersion: process.version,
|
|
66
|
+
bunVersion,
|
|
67
|
+
pythonVersion: pyVersion
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public collectWorkflowVitals(): WorkflowVitals {
|
|
72
|
+
const sotPath = path.join(this.projectDir, 'state.yaml');
|
|
73
|
+
let hasActiveWorkflow = false;
|
|
74
|
+
let currentStep: string | undefined;
|
|
75
|
+
let retryCount = 0;
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(sotPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const content = fs.readFileSync(sotPath, 'utf-8');
|
|
80
|
+
hasActiveWorkflow = content.includes('status: in_progress') || content.includes('status: planning');
|
|
81
|
+
const match = content.match(/current_step:\s*["']?([^"'\n]+)/);
|
|
82
|
+
if (match) currentStep = match[1];
|
|
83
|
+
const retryMatch = content.match(/retry_count:\s*(\d+)/);
|
|
84
|
+
if (retryMatch) retryCount = parseInt(retryMatch[1], 10);
|
|
85
|
+
} catch {
|
|
86
|
+
// Ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let circuitBreakerStatus: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
|
|
91
|
+
let failureStreak = 0;
|
|
92
|
+
const fableStatePath = path.join(this.projectDir, '.fable', 'state.json');
|
|
93
|
+
if (fs.existsSync(fableStatePath)) {
|
|
94
|
+
try {
|
|
95
|
+
const fsData = JSON.parse(fs.readFileSync(fableStatePath, 'utf-8'));
|
|
96
|
+
if (fsData.circuit_breaker_tripped) circuitBreakerStatus = 'OPEN';
|
|
97
|
+
if (typeof fsData.failure_streak === 'number') failureStreak = fsData.failure_streak;
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let traceSpansCount = 0;
|
|
104
|
+
const traceDir = path.join(this.projectDir, '.traces');
|
|
105
|
+
if (fs.existsSync(traceDir)) {
|
|
106
|
+
try {
|
|
107
|
+
const files = fs.readdirSync(traceDir).filter(f => f.endsWith('.jsonl'));
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const lines = fs.readFileSync(path.join(traceDir, file), 'utf-8').trim().split('\n');
|
|
110
|
+
traceSpansCount += lines.length;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Ignore
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
hasActiveWorkflow,
|
|
119
|
+
currentStep,
|
|
120
|
+
circuitBreakerStatus,
|
|
121
|
+
failureStreak,
|
|
122
|
+
retryCount,
|
|
123
|
+
traceSpansCount
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public evaluateHealthScore(vitals: SystemVitals, wf: WorkflowVitals): HealthScore {
|
|
128
|
+
const warnings: string[] = [];
|
|
129
|
+
let runtimeScore = 20;
|
|
130
|
+
let workflowScore = 25;
|
|
131
|
+
let securityScore = 20;
|
|
132
|
+
let dependenciesScore = 15;
|
|
133
|
+
let integrityScore = 20;
|
|
134
|
+
|
|
135
|
+
// Runtime deductions
|
|
136
|
+
if (!vitals.bunVersion) {
|
|
137
|
+
runtimeScore -= 5;
|
|
138
|
+
warnings.push('Bun runtime not detected (preferred engine).');
|
|
139
|
+
}
|
|
140
|
+
if (!vitals.pythonVersion) {
|
|
141
|
+
runtimeScore -= 5;
|
|
142
|
+
warnings.push('Python 3 runtime not detected (dual-parity hooks).');
|
|
143
|
+
}
|
|
144
|
+
if (vitals.memory.usedPercentage > 90) {
|
|
145
|
+
runtimeScore -= 5;
|
|
146
|
+
warnings.push(`High system memory utilization (${vitals.memory.usedPercentage}%).`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Workflow deductions
|
|
150
|
+
if (wf.circuitBreakerStatus === 'OPEN') {
|
|
151
|
+
workflowScore -= 15;
|
|
152
|
+
warnings.push('Circuit breaker is OPEN due to repeated step failures.');
|
|
153
|
+
} else if (wf.failureStreak > 0) {
|
|
154
|
+
workflowScore -= Math.min(10, wf.failureStreak * 3);
|
|
155
|
+
warnings.push(`Current failure streak: ${wf.failureStreak}.`);
|
|
156
|
+
}
|
|
157
|
+
if (wf.retryCount > 3) {
|
|
158
|
+
workflowScore -= 5;
|
|
159
|
+
warnings.push(`Elevated retry count in workflow: ${wf.retryCount}.`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Security score
|
|
163
|
+
const gitignore = path.join(this.projectDir, '.gitignore');
|
|
164
|
+
if (!fs.existsSync(gitignore) || !fs.readFileSync(gitignore, 'utf-8').includes('.env')) {
|
|
165
|
+
securityScore -= 5;
|
|
166
|
+
warnings.push('.env secret exclusion rule missing from .gitignore.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Dependencies score
|
|
170
|
+
const hasNodeModules = fs.existsSync(path.join(this.projectDir, 'node_modules'));
|
|
171
|
+
if (!hasNodeModules) {
|
|
172
|
+
dependenciesScore -= 5;
|
|
173
|
+
warnings.push('node_modules directory missing. Dependencies not installed.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Integrity score
|
|
177
|
+
const hasSkillsIndex = fs.existsSync(path.join(this.projectDir, 'skills-index.toon'));
|
|
178
|
+
if (!hasSkillsIndex) {
|
|
179
|
+
integrityScore -= 5;
|
|
180
|
+
warnings.push('High-density skills-index.toon missing.');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const total = Math.max(0, Math.min(100, runtimeScore + workflowScore + securityScore + dependenciesScore + integrityScore));
|
|
184
|
+
let grade: HealthGrade = 'F';
|
|
185
|
+
if (total >= 95) grade = 'A+';
|
|
186
|
+
else if (total >= 85) grade = 'A';
|
|
187
|
+
else if (total >= 70) grade = 'B';
|
|
188
|
+
else if (total >= 50) grade = 'C';
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
score: total,
|
|
192
|
+
grade,
|
|
193
|
+
breakdown: {
|
|
194
|
+
runtime: Math.max(0, runtimeScore),
|
|
195
|
+
workflow: Math.max(0, workflowScore),
|
|
196
|
+
security: Math.max(0, securityScore),
|
|
197
|
+
dependencies: Math.max(0, dependenciesScore),
|
|
198
|
+
integrity: Math.max(0, integrityScore)
|
|
199
|
+
},
|
|
200
|
+
warnings
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public getReport(): HealthReport {
|
|
205
|
+
const vitals = this.collectSystemVitals();
|
|
206
|
+
const wf = this.collectWorkflowVitals();
|
|
207
|
+
const score = this.evaluateHealthScore(vitals, wf);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
score,
|
|
212
|
+
vitals,
|
|
213
|
+
workflow: wf,
|
|
214
|
+
services: {
|
|
215
|
+
TS_ENGINE: vitals.bunVersion ? 'HEALTHY' : 'DEGRADED',
|
|
216
|
+
PY_ENGINE: vitals.pythonVersion ? 'HEALTHY' : 'DEGRADED',
|
|
217
|
+
UAHF_HOOKS: fs.existsSync(path.join(this.projectDir, '.claude', 'hooks', 'scripts')) ? 'HEALTHY' : 'DOWN',
|
|
218
|
+
SKILLS_MESH: fs.existsSync(path.join(this.projectDir, 'skills-index.toon')) ? 'HEALTHY' : 'DEGRADED',
|
|
219
|
+
CIRCUIT_BREAKER: wf.circuitBreakerStatus === 'CLOSED' ? 'HEALTHY' : 'DEGRADED'
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public formatToon(report: HealthReport): string {
|
|
225
|
+
return `health_telemetry{timestamp:"${report.timestamp}",score:${report.score.score},grade:"${report.score.grade}"}:
|
|
226
|
+
runtime[1]{os,arch,uptime_s,mem_used_pct,bun_v,py_v}:
|
|
227
|
+
${report.vitals.platform},${report.vitals.arch},${report.vitals.uptimeSeconds},${report.vitals.memory.usedPercentage}%,${report.vitals.bunVersion || 'none'},${report.vitals.pythonVersion || 'none'}
|
|
228
|
+
workflow[1]{active,step,circuit_breaker,streak,retries,spans}:
|
|
229
|
+
${report.workflow.hasActiveWorkflow},${report.workflow.currentStep || 'idle'},${report.workflow.circuitBreakerStatus},${report.workflow.failureStreak},${report.workflow.retryCount},${report.workflow.traceSpansCount}
|
|
230
|
+
services[5]{service,status}:
|
|
231
|
+
TS_ENGINE,${report.services.TS_ENGINE}
|
|
232
|
+
PY_ENGINE,${report.services.PY_ENGINE}
|
|
233
|
+
UAHF_HOOKS,${report.services.UAHF_HOOKS}
|
|
234
|
+
SKILLS_MESH,${report.services.SKILLS_MESH}
|
|
235
|
+
CIRCUIT_BREAKER,${report.services.CIRCUIT_BREAKER}
|
|
236
|
+
breakdown{runtime:${report.score.breakdown.runtime}/20,workflow:${report.score.breakdown.workflow}/25,security:${report.score.breakdown.security}/20,deps:${report.score.breakdown.dependencies}/15,integrity:${report.score.breakdown.integrity}/20}
|
|
237
|
+
warnings[${report.score.warnings.length}]:
|
|
238
|
+
${report.score.warnings.map(w => ` - "${w}"`).join('\n') || ' none'}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public formatDashboard(report: HealthReport): string {
|
|
242
|
+
const s = report.score;
|
|
243
|
+
const gradeColor = s.grade.startsWith('A') ? '\x1b[32m' : s.grade === 'B' ? '\x1b[34m' : '\x1b[33m';
|
|
244
|
+
const reset = '\x1b[0m';
|
|
245
|
+
const bold = '\x1b[1m';
|
|
246
|
+
|
|
247
|
+
return `
|
|
248
|
+
${bold}══════════════════════════════════════════════════════════════════${reset}
|
|
249
|
+
⚡ ${bold}AGENTICWORKFLOW LIVE HEALTH ENGINE DASHBOARD${reset} ⚡
|
|
250
|
+
${bold}══════════════════════════════════════════════════════════════════${reset}
|
|
251
|
+
Composite Health Score: ${gradeColor}${bold}${s.score}/100 [Grade: ${s.grade}]${reset}
|
|
252
|
+
System Vitals: OS: ${report.vitals.platform} (${report.vitals.arch}) | Uptime: ${Math.floor(report.vitals.uptimeSeconds / 60)}m
|
|
253
|
+
Memory Utilization: ${report.vitals.memory.usedPercentage}% [RSS: ${Math.round(report.vitals.memory.rssBytes / 1024 / 1024)}MB]
|
|
254
|
+
Dual Engine Runtimes: Bun: ${report.vitals.bunVersion || 'N/A'} | Python: ${report.vitals.pythonVersion || 'N/A'}
|
|
255
|
+
|
|
256
|
+
Workflow Status: ${report.workflow.hasActiveWorkflow ? `ACTIVE (Step: ${report.workflow.currentStep})` : 'IDLE / READY'}
|
|
257
|
+
Circuit Breaker: ${report.workflow.circuitBreakerStatus === 'CLOSED' ? '\x1b[32mCLOSED [Healthy]\x1b[0m' : '\x1b[31mOPEN [Tripped]\x1b[0m'}
|
|
258
|
+
Telemetry Spans: ${report.workflow.traceSpansCount} spans recorded
|
|
259
|
+
|
|
260
|
+
Score Subsystem Breakdown:
|
|
261
|
+
• Runtime Parity: ${s.breakdown.runtime}/20
|
|
262
|
+
• Workflow Vitals: ${s.breakdown.workflow}/25
|
|
263
|
+
• Security & Secrets: ${s.breakdown.security}/20
|
|
264
|
+
• Lib Dependencies: ${s.breakdown.dependencies}/15
|
|
265
|
+
• State Integrity: ${s.breakdown.integrity}/20
|
|
266
|
+
${s.warnings.length > 0 ? `\nActive Advisories:\n${s.warnings.map(w => ` ⚠️ ${w}`).join('\n')}` : '\n✅ All system invariants and health parameters optimal.'}
|
|
267
|
+
${bold}══════════════════════════════════════════════════════════════════${reset}
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/system/index.ts — Unified Export for AgenticWorkflow System Engines Subsystem
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './types.ts';
|
|
6
|
+
export * from './updater.ts';
|
|
7
|
+
export * from './installer.ts';
|
|
8
|
+
export * from './refresher.ts';
|
|
9
|
+
export * from './doctor.ts';
|
|
10
|
+
export * from './health.ts';
|
|
11
|
+
export * from './dependencies.ts';
|
|
12
|
+
export * from './notifications.ts';
|
|
13
|
+
export * from './announcements.ts';
|
|
14
|
+
export * from './version-tracker.ts';
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/system/installer.ts — Auto-Installer & Environment Bootstrapper Engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import type { InstallOptions, InstallReport, InstallTarget, HostPlatform } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export class AutoInstaller {
|
|
12
|
+
private projectDir: string;
|
|
13
|
+
private homeDir: string;
|
|
14
|
+
|
|
15
|
+
constructor(projectDir: string = '.') {
|
|
16
|
+
this.projectDir = path.resolve(projectDir);
|
|
17
|
+
this.homeDir = os.homedir();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private checkBinary(cmd: string): { available: boolean; version?: string } {
|
|
21
|
+
try {
|
|
22
|
+
const out = execSync(`${cmd} --version`, { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
23
|
+
return { available: true, version: out.split('\n')[0] };
|
|
24
|
+
} catch {
|
|
25
|
+
return { available: false };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public getHostTargets(): InstallTarget[] {
|
|
30
|
+
const targets: InstallTarget[] = [
|
|
31
|
+
{
|
|
32
|
+
name: 'Claude Code Skills',
|
|
33
|
+
platform: 'claude',
|
|
34
|
+
path: path.join(this.homeDir, '.claude', 'skills', 'agentic-workflow'),
|
|
35
|
+
installed: fs.existsSync(path.join(this.homeDir, '.claude', 'skills', 'agentic-workflow'))
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Gemini CLI / Antigravity Skills',
|
|
39
|
+
platform: 'gemini',
|
|
40
|
+
path: path.join(this.homeDir, '.gemini', 'config', 'skills', 'agentic-workflow'),
|
|
41
|
+
installed: fs.existsSync(path.join(this.homeDir, '.gemini', 'config', 'skills', 'agentic-workflow'))
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Cursor IDE Skills',
|
|
45
|
+
platform: 'cursor',
|
|
46
|
+
path: path.join(this.homeDir, '.cursor', 'skills', 'agentic-workflow'),
|
|
47
|
+
installed: fs.existsSync(path.join(this.homeDir, '.cursor', 'skills', 'agentic-workflow'))
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Codex / OpenCode Skills',
|
|
51
|
+
platform: 'codex',
|
|
52
|
+
path: path.join(this.homeDir, '.codex', 'skills', 'agentic-workflow'),
|
|
53
|
+
installed: fs.existsSync(path.join(this.homeDir, '.codex', 'skills', 'agentic-workflow'))
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Universal Agent Kernel',
|
|
57
|
+
platform: 'agents',
|
|
58
|
+
path: path.join(this.homeDir, '.agents', 'skills', 'agentic-workflow'),
|
|
59
|
+
installed: fs.existsSync(path.join(this.homeDir, '.agents', 'skills', 'agentic-workflow'))
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
return targets;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public checkStatus(): InstallReport {
|
|
66
|
+
const targets = this.getHostTargets();
|
|
67
|
+
const installed = targets.filter(t => t.installed);
|
|
68
|
+
const skipped = targets.filter(t => !t.installed);
|
|
69
|
+
|
|
70
|
+
const binPaths = [
|
|
71
|
+
path.join(this.homeDir, '.local', 'bin', 'agentic-workflow'),
|
|
72
|
+
'/usr/local/bin/agentic-workflow'
|
|
73
|
+
];
|
|
74
|
+
const linked = binPaths.filter(p => fs.existsSync(p));
|
|
75
|
+
|
|
76
|
+
const systemDeps = [
|
|
77
|
+
{ name: 'bun', ...this.checkBinary('bun') },
|
|
78
|
+
{ name: 'node', ...this.checkBinary('node') },
|
|
79
|
+
{ name: 'python3', ...this.checkBinary('python3') },
|
|
80
|
+
{ name: 'git', ...this.checkBinary('git') },
|
|
81
|
+
{ name: 'brew', ...this.checkBinary('brew') }
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
installedTargets: installed,
|
|
87
|
+
skippedTargets: skipped,
|
|
88
|
+
binLinked: linked,
|
|
89
|
+
shellConfigured: [],
|
|
90
|
+
systemDeps,
|
|
91
|
+
messages: [
|
|
92
|
+
`Installed in ${installed.length} agent host(s)`,
|
|
93
|
+
`CLI binaries linked in ${linked.length} path(s)`
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public install(options: InstallOptions = {}): InstallReport {
|
|
99
|
+
const messages: string[] = [];
|
|
100
|
+
const installedTargets: InstallTarget[] = [];
|
|
101
|
+
const skippedTargets: InstallTarget[] = [];
|
|
102
|
+
const binLinked: string[] = [];
|
|
103
|
+
const shellConfigured: string[] = [];
|
|
104
|
+
|
|
105
|
+
const hostTargets = this.getHostTargets();
|
|
106
|
+
const requestedPlatforms = options.targets || ['claude', 'gemini', 'cursor', 'codex', 'agents'];
|
|
107
|
+
|
|
108
|
+
// 1. Copy or Symlink into host skills directories
|
|
109
|
+
for (const target of hostTargets) {
|
|
110
|
+
if (!requestedPlatforms.includes(target.platform)) {
|
|
111
|
+
skippedTargets.push(target);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parentDir = path.dirname(target.path);
|
|
116
|
+
try {
|
|
117
|
+
if (!fs.existsSync(parentDir)) {
|
|
118
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(target.path)) {
|
|
122
|
+
fs.rmSync(target.path, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Copy files excluding node_modules / .git
|
|
126
|
+
fs.cpSync(this.projectDir, target.path, {
|
|
127
|
+
recursive: true,
|
|
128
|
+
filter: (src) => {
|
|
129
|
+
const rel = path.relative(this.projectDir, src);
|
|
130
|
+
if (rel.startsWith('node_modules') || rel.startsWith('.git') || rel.startsWith('__pycache__')) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
target.installed = true;
|
|
138
|
+
installedTargets.push(target);
|
|
139
|
+
messages.push(`✓ Installed skill to ${target.name} (${target.path})`);
|
|
140
|
+
} catch (err: any) {
|
|
141
|
+
messages.push(`! Failed installing to ${target.name}: ${err.message}`);
|
|
142
|
+
skippedTargets.push(target);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Global CLI Binary Symlink
|
|
147
|
+
const cliSource = path.join(this.projectDir, 'bin', 'cli.js');
|
|
148
|
+
if (fs.existsSync(cliSource)) {
|
|
149
|
+
try {
|
|
150
|
+
fs.chmodSync(cliSource, 0o755);
|
|
151
|
+
} catch {
|
|
152
|
+
// Ignore
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const localBinDir = path.join(this.homeDir, '.local', 'bin');
|
|
156
|
+
if (!fs.existsSync(localBinDir)) {
|
|
157
|
+
fs.mkdirSync(localBinDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const localBinTarget = path.join(localBinDir, 'agentic-workflow');
|
|
161
|
+
try {
|
|
162
|
+
if (fs.existsSync(localBinTarget)) {
|
|
163
|
+
fs.unlinkSync(localBinTarget);
|
|
164
|
+
}
|
|
165
|
+
fs.symlinkSync(cliSource, localBinTarget);
|
|
166
|
+
binLinked.push(localBinTarget);
|
|
167
|
+
messages.push(`✓ Linked executable to ${localBinTarget}`);
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
messages.push(`! Failed linking ${localBinTarget}: ${err.message}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Try /usr/local/bin if writable
|
|
173
|
+
if (options.globalBin) {
|
|
174
|
+
const usrBinTarget = '/usr/local/bin/agentic-workflow';
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(usrBinTarget)) {
|
|
177
|
+
fs.unlinkSync(usrBinTarget);
|
|
178
|
+
}
|
|
179
|
+
fs.symlinkSync(cliSource, usrBinTarget);
|
|
180
|
+
binLinked.push(usrBinTarget);
|
|
181
|
+
messages.push(`✓ Linked global executable to ${usrBinTarget}`);
|
|
182
|
+
} catch {
|
|
183
|
+
// May need sudo, skip gracefully
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 3. Update Shell RC if requested
|
|
189
|
+
if (options.updateShellRc !== false) {
|
|
190
|
+
const localBin = path.join(this.homeDir, '.local', 'bin');
|
|
191
|
+
const exportLine = `export PATH="$PATH:${localBin}"`;
|
|
192
|
+
const rcFiles = [
|
|
193
|
+
path.join(this.homeDir, '.zshrc'),
|
|
194
|
+
path.join(this.homeDir, '.bashrc'),
|
|
195
|
+
path.join(this.homeDir, '.profile')
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
for (const rc of rcFiles) {
|
|
199
|
+
if (fs.existsSync(rc)) {
|
|
200
|
+
try {
|
|
201
|
+
const content = fs.readFileSync(rc, 'utf-8');
|
|
202
|
+
if (!content.includes(localBin)) {
|
|
203
|
+
fs.appendFileSync(rc, `\n# AgenticWorkflow CLI\n${exportLine}\n`);
|
|
204
|
+
shellConfigured.push(rc);
|
|
205
|
+
messages.push(`✓ Added PATH export to ${path.basename(rc)}`);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Ignore
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 4. System Dependencies Check
|
|
215
|
+
const systemDeps = [
|
|
216
|
+
{ name: 'bun', ...this.checkBinary('bun') },
|
|
217
|
+
{ name: 'node', ...this.checkBinary('node') },
|
|
218
|
+
{ name: 'python3', ...this.checkBinary('python3') },
|
|
219
|
+
{ name: 'git', ...this.checkBinary('git') },
|
|
220
|
+
{ name: 'brew', ...this.checkBinary('brew') }
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: installedTargets.length > 0 || binLinked.length > 0,
|
|
225
|
+
installedTargets,
|
|
226
|
+
skippedTargets,
|
|
227
|
+
binLinked,
|
|
228
|
+
shellConfigured,
|
|
229
|
+
systemDeps,
|
|
230
|
+
messages
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public generateShellCompletion(shell: 'zsh' | 'bash' | 'fish'): string {
|
|
235
|
+
const commands = [
|
|
236
|
+
'engine', 'autopilot', 'run', 'skills', 'guard', 'eval', 'traces',
|
|
237
|
+
'hooks', 'integrations', 'fable', 'toon', 'init', 'validate', 'status',
|
|
238
|
+
'test', 'update', 'install', 'refresh', 'doctor', 'health', 'deps',
|
|
239
|
+
'notify', 'announcements', 'version', 'help'
|
|
240
|
+
].join(' ');
|
|
241
|
+
|
|
242
|
+
if (shell === 'zsh') {
|
|
243
|
+
return `#compdef agentic-workflow
|
|
244
|
+
_agentic_workflow() {
|
|
245
|
+
local -a commands
|
|
246
|
+
commands=(${commands})
|
|
247
|
+
_describe 'command' commands
|
|
248
|
+
}
|
|
249
|
+
compdef _agentic_workflow agentic-workflow
|
|
250
|
+
`;
|
|
251
|
+
} else if (shell === 'fish') {
|
|
252
|
+
return `complete -c agentic-workflow -f -a "${commands}"`;
|
|
253
|
+
} else {
|
|
254
|
+
return `_agentic_workflow() {
|
|
255
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
256
|
+
COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
|
|
257
|
+
}
|
|
258
|
+
complete -F _agentic_workflow agentic-workflow
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|