@agile-vibe-coding/avc 0.1.0 → 0.2.3
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/README.md +2 -0
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +129 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/epic-story-decomposer.md +280 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +79 -0
- package/cli/agents/mission-scope-validator.md +112 -0
- package/cli/agents/project-context-extractor.md +107 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/solver-epic-api.json +15 -0
- package/cli/agents/solver-epic-api.md +39 -0
- package/cli/agents/solver-epic-backend.json +15 -0
- package/cli/agents/solver-epic-backend.md +39 -0
- package/cli/agents/solver-epic-cloud.json +15 -0
- package/cli/agents/solver-epic-cloud.md +39 -0
- package/cli/agents/solver-epic-data.json +15 -0
- package/cli/agents/solver-epic-data.md +39 -0
- package/cli/agents/solver-epic-database.json +15 -0
- package/cli/agents/solver-epic-database.md +39 -0
- package/cli/agents/solver-epic-developer.json +15 -0
- package/cli/agents/solver-epic-developer.md +39 -0
- package/cli/agents/solver-epic-devops.json +15 -0
- package/cli/agents/solver-epic-devops.md +39 -0
- package/cli/agents/solver-epic-frontend.json +15 -0
- package/cli/agents/solver-epic-frontend.md +39 -0
- package/cli/agents/solver-epic-mobile.json +15 -0
- package/cli/agents/solver-epic-mobile.md +39 -0
- package/cli/agents/solver-epic-qa.json +15 -0
- package/cli/agents/solver-epic-qa.md +39 -0
- package/cli/agents/solver-epic-security.json +15 -0
- package/cli/agents/solver-epic-security.md +39 -0
- package/cli/agents/solver-epic-solution-architect.json +15 -0
- package/cli/agents/solver-epic-solution-architect.md +39 -0
- package/cli/agents/solver-epic-test-architect.json +15 -0
- package/cli/agents/solver-epic-test-architect.md +39 -0
- package/cli/agents/solver-epic-ui.json +15 -0
- package/cli/agents/solver-epic-ui.md +39 -0
- package/cli/agents/solver-epic-ux.json +15 -0
- package/cli/agents/solver-epic-ux.md +39 -0
- package/cli/agents/solver-story-api.json +15 -0
- package/cli/agents/solver-story-api.md +39 -0
- package/cli/agents/solver-story-backend.json +15 -0
- package/cli/agents/solver-story-backend.md +39 -0
- package/cli/agents/solver-story-cloud.json +15 -0
- package/cli/agents/solver-story-cloud.md +39 -0
- package/cli/agents/solver-story-data.json +15 -0
- package/cli/agents/solver-story-data.md +39 -0
- package/cli/agents/solver-story-database.json +15 -0
- package/cli/agents/solver-story-database.md +39 -0
- package/cli/agents/solver-story-developer.json +15 -0
- package/cli/agents/solver-story-developer.md +39 -0
- package/cli/agents/solver-story-devops.json +15 -0
- package/cli/agents/solver-story-devops.md +39 -0
- package/cli/agents/solver-story-frontend.json +15 -0
- package/cli/agents/solver-story-frontend.md +39 -0
- package/cli/agents/solver-story-mobile.json +15 -0
- package/cli/agents/solver-story-mobile.md +39 -0
- package/cli/agents/solver-story-qa.json +15 -0
- package/cli/agents/solver-story-qa.md +39 -0
- package/cli/agents/solver-story-security.json +15 -0
- package/cli/agents/solver-story-security.md +39 -0
- package/cli/agents/solver-story-solution-architect.json +15 -0
- package/cli/agents/solver-story-solution-architect.md +39 -0
- package/cli/agents/solver-story-test-architect.json +15 -0
- package/cli/agents/solver-story-test-architect.md +39 -0
- package/cli/agents/solver-story-ui.json +15 -0
- package/cli/agents/solver-story-ui.md +39 -0
- package/cli/agents/solver-story-ux.json +15 -0
- package/cli/agents/solver-story-ux.md +39 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +152 -0
- package/cli/agents/validator-documentation.md +453 -0
- package/cli/agents/validator-epic-api.json +93 -0
- package/cli/agents/validator-epic-api.md +137 -0
- package/cli/agents/validator-epic-backend.json +93 -0
- package/cli/agents/validator-epic-backend.md +130 -0
- package/cli/agents/validator-epic-cloud.json +93 -0
- package/cli/agents/validator-epic-cloud.md +137 -0
- package/cli/agents/validator-epic-data.json +93 -0
- package/cli/agents/validator-epic-data.md +130 -0
- package/cli/agents/validator-epic-database.json +93 -0
- package/cli/agents/validator-epic-database.md +137 -0
- package/cli/agents/validator-epic-developer.json +74 -0
- package/cli/agents/validator-epic-developer.md +153 -0
- package/cli/agents/validator-epic-devops.json +74 -0
- package/cli/agents/validator-epic-devops.md +153 -0
- package/cli/agents/validator-epic-frontend.json +74 -0
- package/cli/agents/validator-epic-frontend.md +153 -0
- package/cli/agents/validator-epic-mobile.json +93 -0
- package/cli/agents/validator-epic-mobile.md +130 -0
- package/cli/agents/validator-epic-qa.json +93 -0
- package/cli/agents/validator-epic-qa.md +130 -0
- package/cli/agents/validator-epic-security.json +74 -0
- package/cli/agents/validator-epic-security.md +154 -0
- package/cli/agents/validator-epic-solution-architect.json +74 -0
- package/cli/agents/validator-epic-solution-architect.md +156 -0
- package/cli/agents/validator-epic-test-architect.json +93 -0
- package/cli/agents/validator-epic-test-architect.md +130 -0
- package/cli/agents/validator-epic-ui.json +93 -0
- package/cli/agents/validator-epic-ui.md +130 -0
- package/cli/agents/validator-epic-ux.json +93 -0
- package/cli/agents/validator-epic-ux.md +130 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/agents/validator-story-api.json +104 -0
- package/cli/agents/validator-story-api.md +152 -0
- package/cli/agents/validator-story-backend.json +104 -0
- package/cli/agents/validator-story-backend.md +152 -0
- package/cli/agents/validator-story-cloud.json +104 -0
- package/cli/agents/validator-story-cloud.md +152 -0
- package/cli/agents/validator-story-data.json +104 -0
- package/cli/agents/validator-story-data.md +152 -0
- package/cli/agents/validator-story-database.json +104 -0
- package/cli/agents/validator-story-database.md +152 -0
- package/cli/agents/validator-story-developer.json +104 -0
- package/cli/agents/validator-story-developer.md +152 -0
- package/cli/agents/validator-story-devops.json +104 -0
- package/cli/agents/validator-story-devops.md +152 -0
- package/cli/agents/validator-story-frontend.json +104 -0
- package/cli/agents/validator-story-frontend.md +152 -0
- package/cli/agents/validator-story-mobile.json +104 -0
- package/cli/agents/validator-story-mobile.md +152 -0
- package/cli/agents/validator-story-qa.json +104 -0
- package/cli/agents/validator-story-qa.md +152 -0
- package/cli/agents/validator-story-security.json +104 -0
- package/cli/agents/validator-story-security.md +152 -0
- package/cli/agents/validator-story-solution-architect.json +104 -0
- package/cli/agents/validator-story-solution-architect.md +152 -0
- package/cli/agents/validator-story-test-architect.json +104 -0
- package/cli/agents/validator-story-test-architect.md +152 -0
- package/cli/agents/validator-story-ui.json +104 -0
- package/cli/agents/validator-story-ui.md +152 -0
- package/cli/agents/validator-story-ux.json +104 -0
- package/cli/agents/validator-story-ux.md +152 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/build-docs.js +298 -0
- package/cli/ceremony-history.js +369 -0
- package/cli/command-logger.js +245 -0
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +1174 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/index.js +3 -25
- package/cli/init-model-config.js +697 -0
- package/cli/init.js +1765 -100
- package/cli/kanban-server-manager.js +228 -0
- package/cli/llm-claude.js +109 -0
- package/cli/llm-gemini.js +115 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +233 -0
- package/cli/llm-provider.js +300 -0
- package/cli/llm-token-limits.js +102 -0
- package/cli/llm-verifier.js +454 -0
- package/cli/logger.js +32 -5
- package/cli/message-constants.js +58 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +297 -0
- package/cli/model-pricing.js +169 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +269 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +332 -0
- package/cli/repl-ink.js +5840 -504
- package/cli/repl-old.js +4 -4
- package/cli/seed-processor.js +792 -0
- package/cli/sprint-planning-processor.js +1813 -0
- package/cli/template-processor.js +2306 -108
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +34 -0
- package/cli/token-tracker.js +520 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +605 -0
- package/cli/verification-tracker.js +563 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
- package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +622 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +353 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +118 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +401 -0
- package/kanban/client/src/lib/status-grouping.js +144 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +115 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +516 -0
- package/kanban/server/routes/ceremony.js +305 -0
- package/kanban/server/routes/costs.js +157 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +303 -0
- package/kanban/server/routes/websocket.js +276 -0
- package/kanban/server/routes/work-items.js +347 -0
- package/kanban/server/services/CeremonyService.js +1190 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/sponsor-call-worker.js +84 -0
- package/kanban/server/workers/sprint-planning-worker.js +130 -0
- package/package.json +34 -7
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { execSync, spawn, exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import net from 'net';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build and serve VitePress documentation
|
|
12
|
+
* Builds the docs and starts a local preview server
|
|
13
|
+
*/
|
|
14
|
+
export class DocumentationBuilder {
|
|
15
|
+
constructor(projectRoot = process.cwd()) {
|
|
16
|
+
this.projectRoot = projectRoot;
|
|
17
|
+
this.docsDir = path.join(projectRoot, '.avc', 'documentation');
|
|
18
|
+
this.distDir = path.join(this.docsDir, '.vitepress', 'dist');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if documentation directory exists
|
|
23
|
+
*/
|
|
24
|
+
hasDocumentation() {
|
|
25
|
+
return fs.existsSync(this.docsDir);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if synced project docs directory exists inside documentation
|
|
30
|
+
*/
|
|
31
|
+
hasProjectDocs() {
|
|
32
|
+
return fs.existsSync(path.join(this.docsDir, 'project'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get documentation server port from avc.json config
|
|
37
|
+
* Returns default port 4173 if not configured
|
|
38
|
+
*/
|
|
39
|
+
getPort() {
|
|
40
|
+
const configPath = path.join(this.projectRoot, '.avc', 'avc.json');
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(configPath)) {
|
|
43
|
+
return 4173; // Default port
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
48
|
+
return config.settings?.documentation?.port || 4173;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn(`Could not read port from avc.json: ${error.message}`);
|
|
51
|
+
return 4173;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a port is in use
|
|
57
|
+
* @param {number} port - Port number to check
|
|
58
|
+
* @returns {Promise<boolean>} - True if port is in use
|
|
59
|
+
*/
|
|
60
|
+
async isPortInUse(port) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const server = net.createServer();
|
|
63
|
+
|
|
64
|
+
server.once('error', (err) => {
|
|
65
|
+
if (err.code === 'EADDRINUSE') {
|
|
66
|
+
resolve(true); // Port is in use
|
|
67
|
+
} else {
|
|
68
|
+
resolve(false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
server.once('listening', () => {
|
|
73
|
+
server.close();
|
|
74
|
+
resolve(false); // Port is available
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.listen(port, '127.0.0.1');
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if the server on this port is serving our AVC documentation
|
|
83
|
+
* Makes HTTP request and checks for specific AVC metatag to positively identify
|
|
84
|
+
* @param {number} port - Port number to check
|
|
85
|
+
* @returns {Promise<boolean>} - True if it's confirmed to be AVC documentation server
|
|
86
|
+
*/
|
|
87
|
+
async isDocumentationServer(port) {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const req = http.get(`http://localhost:${port}/`, {
|
|
90
|
+
timeout: 2000
|
|
91
|
+
}, (res) => {
|
|
92
|
+
let data = '';
|
|
93
|
+
|
|
94
|
+
res.on('data', (chunk) => {
|
|
95
|
+
data += chunk;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
res.on('end', () => {
|
|
99
|
+
// ONLY return true if we find the specific AVC documentation metatag
|
|
100
|
+
// This ensures we only kill processes we're 100% certain about
|
|
101
|
+
const hasAvcMetatag = data.includes('<meta name="avc-documentation" content="true">') ||
|
|
102
|
+
data.includes('name="avc-documentation"') ||
|
|
103
|
+
data.includes('name="generator" content="Agile Vibe Coding"');
|
|
104
|
+
resolve(hasAvcMetatag);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
req.on('error', () => {
|
|
109
|
+
resolve(false); // Can't connect or verify
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
req.on('timeout', () => {
|
|
113
|
+
req.destroy();
|
|
114
|
+
resolve(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Find which process is using a port
|
|
121
|
+
* Works cross-platform (Linux, macOS, Windows)
|
|
122
|
+
* @param {number} port - Port number to check
|
|
123
|
+
* @returns {Promise<{pid: number, command: string} | null>} - Process info or null if not found
|
|
124
|
+
*/
|
|
125
|
+
async findProcessUsingPort(port) {
|
|
126
|
+
try {
|
|
127
|
+
let command;
|
|
128
|
+
let parseOutput;
|
|
129
|
+
|
|
130
|
+
if (process.platform === 'win32') {
|
|
131
|
+
// Windows: netstat -ano | findstr :PORT
|
|
132
|
+
command = `netstat -ano | findstr :${port}`;
|
|
133
|
+
parseOutput = (output) => {
|
|
134
|
+
const lines = output.split('\n');
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
if (line.includes(`0.0.0.0:${port}`) || line.includes(`127.0.0.1:${port}`) || line.includes(`[::]:${port}`)) {
|
|
137
|
+
const parts = line.trim().split(/\s+/);
|
|
138
|
+
const pid = parseInt(parts[parts.length - 1]);
|
|
139
|
+
if (pid && !isNaN(pid)) {
|
|
140
|
+
return { pid, command: 'Unknown' }; // Windows netstat doesn't show command
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
// Linux/macOS: lsof -i :PORT
|
|
148
|
+
command = `lsof -i :${port} -t -sTCP:LISTEN`;
|
|
149
|
+
parseOutput = (output) => {
|
|
150
|
+
const pid = parseInt(output.trim());
|
|
151
|
+
if (pid && !isNaN(pid)) {
|
|
152
|
+
// Try to get process name
|
|
153
|
+
try {
|
|
154
|
+
const psOutput = execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8' });
|
|
155
|
+
return { pid, command: psOutput.trim() };
|
|
156
|
+
} catch {
|
|
157
|
+
return { pid, command: 'Unknown' };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const output = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
|
|
165
|
+
return parseOutput(output);
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Command failed (port not in use or lsof not available)
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Kill a process by PID
|
|
175
|
+
* Works cross-platform (Linux, macOS, Windows)
|
|
176
|
+
* @param {number} pid - Process ID to kill
|
|
177
|
+
* @returns {Promise<boolean>} - True if successfully killed, false otherwise
|
|
178
|
+
*/
|
|
179
|
+
async killProcess(pid) {
|
|
180
|
+
try {
|
|
181
|
+
if (process.platform === 'win32') {
|
|
182
|
+
// Windows: taskkill /F /PID <pid>
|
|
183
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' });
|
|
184
|
+
} else {
|
|
185
|
+
// Linux/macOS: kill -9 <pid>
|
|
186
|
+
execSync(`kill -9 ${pid}`, { stdio: 'pipe' });
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Failed to kill (permission denied, process not found, etc.)
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build and serve the documentation
|
|
197
|
+
* Returns the server URL
|
|
198
|
+
*/
|
|
199
|
+
async buildAndServe() {
|
|
200
|
+
if (!this.hasDocumentation()) {
|
|
201
|
+
throw new Error('Documentation not found. Run /init first to create documentation structure.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const port = this.getPort();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Start the dev server — no initial build needed, vitepress dev builds on-demand
|
|
208
|
+
// and hot-reloads the browser whenever source .md files change
|
|
209
|
+
const serverProcess = spawn('npx', ['vitepress', 'dev', '--port', String(port)], {
|
|
210
|
+
cwd: this.docsDir,
|
|
211
|
+
stdio: 'pipe'
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Wait for server to be ready
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
let serverReady = false;
|
|
217
|
+
|
|
218
|
+
serverProcess.stdout.on('data', (data) => {
|
|
219
|
+
const output = data.toString();
|
|
220
|
+
console.log(output);
|
|
221
|
+
|
|
222
|
+
// Check if server is ready
|
|
223
|
+
if (output.includes(`http://localhost:${port}`) || output.includes(`localhost:${port}`)) {
|
|
224
|
+
if (!serverReady) {
|
|
225
|
+
serverReady = true;
|
|
226
|
+
resolve({
|
|
227
|
+
url: `http://localhost:${port}`,
|
|
228
|
+
process: serverProcess
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
serverProcess.stderr.on('data', (data) => {
|
|
235
|
+
console.error(data.toString());
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
serverProcess.on('error', (error) => {
|
|
239
|
+
reject(error);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
serverProcess.on('exit', (code) => {
|
|
243
|
+
if (code !== 0 && !serverReady) {
|
|
244
|
+
reject(new Error(`Server exited with code ${code}`));
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Timeout after 10 seconds
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
if (!serverReady) {
|
|
251
|
+
serverProcess.kill();
|
|
252
|
+
reject(new Error('Server start timeout'));
|
|
253
|
+
}
|
|
254
|
+
}, 10000);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new Error(`Failed to build documentation: ${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Ensure ignoreDeadLinks: true is present in the VitePress config.
|
|
264
|
+
* Patches existing project configs that were created before this option was added.
|
|
265
|
+
*/
|
|
266
|
+
_ensureIgnoreDeadLinks() {
|
|
267
|
+
const configPath = path.join(this.docsDir, '.vitepress', 'config.mts');
|
|
268
|
+
if (!fs.existsSync(configPath)) return;
|
|
269
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
270
|
+
if (content.includes('ignoreDeadLinks')) return;
|
|
271
|
+
const patched = content.replace(
|
|
272
|
+
/defineConfig\(\{/,
|
|
273
|
+
'defineConfig({\n ignoreDeadLinks: true,'
|
|
274
|
+
);
|
|
275
|
+
fs.writeFileSync(configPath, patched, 'utf8');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build documentation without starting server
|
|
280
|
+
*/
|
|
281
|
+
async build() {
|
|
282
|
+
if (!this.hasDocumentation()) {
|
|
283
|
+
throw new Error('Documentation not found. Run /init first to create documentation structure.');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this._ensureIgnoreDeadLinks();
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
// Build asynchronously to avoid blocking the event loop
|
|
290
|
+
await execAsync('npx vitepress build', {
|
|
291
|
+
cwd: this.docsDir
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw new Error(`Failed to build documentation: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceremony History Tracker - Records all ceremony executions with status and metadata
|
|
3
|
+
*
|
|
4
|
+
* Tracks when ceremonies are run, their outcomes, and preserves historical data.
|
|
5
|
+
* Helps detect abrupt terminations and provides audit trail.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
export class CeremonyHistory {
|
|
12
|
+
constructor(avcPath = path.join(process.cwd(), '.avc')) {
|
|
13
|
+
this.avcPath = avcPath;
|
|
14
|
+
this.historyPath = path.join(avcPath, 'ceremonies-history.json');
|
|
15
|
+
this.data = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize ceremony history file if it doesn't exist
|
|
20
|
+
*/
|
|
21
|
+
init() {
|
|
22
|
+
// Ensure .avc directory exists
|
|
23
|
+
if (!fs.existsSync(this.avcPath)) {
|
|
24
|
+
fs.mkdirSync(this.avcPath, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(this.historyPath)) {
|
|
28
|
+
const initialData = {
|
|
29
|
+
version: "1.0",
|
|
30
|
+
lastUpdated: new Date().toISOString(),
|
|
31
|
+
ceremonies: {}
|
|
32
|
+
};
|
|
33
|
+
this._writeData(initialData);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load ceremony history from disk
|
|
39
|
+
*/
|
|
40
|
+
load() {
|
|
41
|
+
if (fs.existsSync(this.historyPath)) {
|
|
42
|
+
this.data = JSON.parse(fs.readFileSync(this.historyPath, 'utf8'));
|
|
43
|
+
} else {
|
|
44
|
+
this.init();
|
|
45
|
+
this.data = JSON.parse(fs.readFileSync(this.historyPath, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
return this.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write data to disk atomically
|
|
52
|
+
*/
|
|
53
|
+
_writeData(data) {
|
|
54
|
+
try {
|
|
55
|
+
const tempPath = this.historyPath + '.tmp';
|
|
56
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
|
|
57
|
+
fs.renameSync(tempPath, this.historyPath);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Fallback to direct write if atomic write fails
|
|
60
|
+
try {
|
|
61
|
+
fs.writeFileSync(this.historyPath, JSON.stringify(data, null, 2), 'utf8');
|
|
62
|
+
} catch (fallbackError) {
|
|
63
|
+
console.error(`Failed to write ceremony history: ${fallbackError.message}`);
|
|
64
|
+
console.error(`Path: ${this.historyPath}`);
|
|
65
|
+
throw fallbackError;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Start a new ceremony execution
|
|
72
|
+
* @param {string} ceremonyName - e.g., 'sponsor-call'
|
|
73
|
+
* @param {string} stage - Initial stage (default: 'questionnaire')
|
|
74
|
+
* @returns {string} Execution ID
|
|
75
|
+
*/
|
|
76
|
+
startExecution(ceremonyName, stage = 'questionnaire') {
|
|
77
|
+
this.load();
|
|
78
|
+
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const timestamp = now.toISOString()
|
|
81
|
+
.replace(/T/, '-')
|
|
82
|
+
.replace(/:/g, '-')
|
|
83
|
+
.replace(/\..+/, '');
|
|
84
|
+
|
|
85
|
+
const executionId = `${ceremonyName}-${timestamp}`;
|
|
86
|
+
|
|
87
|
+
// Initialize ceremony entry if doesn't exist
|
|
88
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
89
|
+
this.data.ceremonies[ceremonyName] = {
|
|
90
|
+
executions: [],
|
|
91
|
+
totalExecutions: 0,
|
|
92
|
+
lastRun: null,
|
|
93
|
+
lastSuccess: null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create execution record
|
|
98
|
+
const execution = {
|
|
99
|
+
id: executionId,
|
|
100
|
+
startTime: now.toISOString(),
|
|
101
|
+
endTime: null,
|
|
102
|
+
status: 'in-progress',
|
|
103
|
+
stage: stage,
|
|
104
|
+
answers: null,
|
|
105
|
+
filesGenerated: [],
|
|
106
|
+
tokenUsage: null,
|
|
107
|
+
duration: null,
|
|
108
|
+
outcome: null
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Add to executions array
|
|
112
|
+
this.data.ceremonies[ceremonyName].executions.push(execution);
|
|
113
|
+
this.data.ceremonies[ceremonyName].lastRun = now.toISOString();
|
|
114
|
+
this.data.lastUpdated = now.toISOString();
|
|
115
|
+
|
|
116
|
+
this._writeData(this.data);
|
|
117
|
+
|
|
118
|
+
return executionId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Update an existing execution
|
|
123
|
+
* @param {string} ceremonyName - Ceremony name
|
|
124
|
+
* @param {string} executionId - Execution ID
|
|
125
|
+
* @param {Object} updates - Fields to update
|
|
126
|
+
*/
|
|
127
|
+
updateExecution(ceremonyName, executionId, updates) {
|
|
128
|
+
this.load();
|
|
129
|
+
|
|
130
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
131
|
+
throw new Error(`Ceremony '${ceremonyName}' not found in history`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const execution = this.data.ceremonies[ceremonyName].executions.find(
|
|
135
|
+
e => e.id === executionId
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!execution) {
|
|
139
|
+
throw new Error(`Execution '${executionId}' not found for ceremony '${ceremonyName}'`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Update fields
|
|
143
|
+
Object.assign(execution, updates);
|
|
144
|
+
this.data.lastUpdated = new Date().toISOString();
|
|
145
|
+
|
|
146
|
+
this._writeData(this.data);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Complete an execution
|
|
151
|
+
* @param {string} ceremonyName - Ceremony name
|
|
152
|
+
* @param {string} executionId - Execution ID
|
|
153
|
+
* @param {string} outcome - 'success', 'user-cancelled', or 'abrupt-termination'
|
|
154
|
+
* @param {Object} metadata - Additional data (answers, filesGenerated, tokenUsage, etc.)
|
|
155
|
+
*/
|
|
156
|
+
completeExecution(ceremonyName, executionId, outcome, metadata = {}) {
|
|
157
|
+
this.load();
|
|
158
|
+
|
|
159
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
160
|
+
throw new Error(`Ceremony '${ceremonyName}' not found in history`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const execution = this.data.ceremonies[ceremonyName].executions.find(
|
|
164
|
+
e => e.id === executionId
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!execution) {
|
|
168
|
+
throw new Error(`Execution '${executionId}' not found for ceremony '${ceremonyName}'`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const now = new Date();
|
|
172
|
+
const startTime = new Date(execution.startTime);
|
|
173
|
+
const duration = now - startTime;
|
|
174
|
+
|
|
175
|
+
// Determine status based on outcome
|
|
176
|
+
let status;
|
|
177
|
+
switch (outcome) {
|
|
178
|
+
case 'success':
|
|
179
|
+
status = 'completed';
|
|
180
|
+
break;
|
|
181
|
+
case 'user-cancelled':
|
|
182
|
+
status = 'cancelled';
|
|
183
|
+
break;
|
|
184
|
+
case 'abrupt-termination':
|
|
185
|
+
status = 'aborted';
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
status = 'completed';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update execution
|
|
192
|
+
execution.status = status;
|
|
193
|
+
execution.endTime = now.toISOString();
|
|
194
|
+
execution.outcome = outcome;
|
|
195
|
+
execution.duration = duration;
|
|
196
|
+
|
|
197
|
+
// Merge metadata
|
|
198
|
+
if (metadata.answers) execution.answers = metadata.answers;
|
|
199
|
+
if (metadata.filesGenerated) execution.filesGenerated = metadata.filesGenerated;
|
|
200
|
+
if (metadata.tokenUsage) execution.tokenUsage = metadata.tokenUsage;
|
|
201
|
+
if (metadata.cost) execution.cost = metadata.cost;
|
|
202
|
+
if (metadata.model) execution.model = metadata.model;
|
|
203
|
+
if (metadata.stage) execution.stage = metadata.stage;
|
|
204
|
+
if (metadata.error) execution.error = metadata.error;
|
|
205
|
+
if (metadata.note) execution.note = metadata.note;
|
|
206
|
+
|
|
207
|
+
// Update ceremony totals
|
|
208
|
+
this.data.ceremonies[ceremonyName].totalExecutions =
|
|
209
|
+
this.data.ceremonies[ceremonyName].executions.length;
|
|
210
|
+
|
|
211
|
+
if (outcome === 'success') {
|
|
212
|
+
this.data.ceremonies[ceremonyName].lastSuccess = now.toISOString();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.data.lastUpdated = now.toISOString();
|
|
216
|
+
|
|
217
|
+
this._writeData(this.data);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Archive answers to an execution (before LLM generation starts)
|
|
222
|
+
* @param {string} ceremonyName - Ceremony name
|
|
223
|
+
* @param {string} executionId - Execution ID
|
|
224
|
+
* @param {Object} answers - Questionnaire answers
|
|
225
|
+
*/
|
|
226
|
+
archiveAnswers(ceremonyName, executionId, answers) {
|
|
227
|
+
this.updateExecution(ceremonyName, executionId, {
|
|
228
|
+
answers: { ...answers }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the most recent execution for a ceremony
|
|
234
|
+
* @param {string} ceremonyName - Ceremony name
|
|
235
|
+
* @returns {Object|null} Execution record or null
|
|
236
|
+
*/
|
|
237
|
+
getLastExecution(ceremonyName) {
|
|
238
|
+
this.load();
|
|
239
|
+
|
|
240
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const executions = this.data.ceremonies[ceremonyName].executions;
|
|
245
|
+
if (executions.length === 0) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Return most recent (last in array)
|
|
250
|
+
return executions[executions.length - 1];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get a specific execution by ID
|
|
255
|
+
* @param {string} ceremonyName - Ceremony name
|
|
256
|
+
* @param {string} executionId - Execution ID
|
|
257
|
+
* @returns {Object|null} Execution record or null
|
|
258
|
+
*/
|
|
259
|
+
getExecutionById(ceremonyName, executionId) {
|
|
260
|
+
this.load();
|
|
261
|
+
|
|
262
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.data.ceremonies[ceremonyName].executions.find(
|
|
267
|
+
e => e.id === executionId
|
|
268
|
+
) || null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get all executions for a ceremony
|
|
273
|
+
* @param {string} ceremonyName - Ceremony name
|
|
274
|
+
* @returns {Array} Array of execution records (newest first)
|
|
275
|
+
*/
|
|
276
|
+
getAllExecutions(ceremonyName) {
|
|
277
|
+
this.load();
|
|
278
|
+
|
|
279
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Return copy, sorted by startTime descending
|
|
284
|
+
return [...this.data.ceremonies[ceremonyName].executions]
|
|
285
|
+
.sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Detect if the last execution was abruptly terminated
|
|
290
|
+
* @param {string} ceremonyName - Ceremony name
|
|
291
|
+
* @returns {boolean} True if abrupt termination detected
|
|
292
|
+
*/
|
|
293
|
+
detectAbruptTermination(ceremonyName) {
|
|
294
|
+
const lastExecution = this.getLastExecution(ceremonyName);
|
|
295
|
+
|
|
296
|
+
if (!lastExecution) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check if last execution is still in-progress and in LLM generation stage
|
|
301
|
+
return lastExecution.status === 'in-progress' &&
|
|
302
|
+
lastExecution.stage === 'llm-generation';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Clean up abrupt termination (mark as aborted)
|
|
307
|
+
* @param {string} ceremonyName - Ceremony name
|
|
308
|
+
*/
|
|
309
|
+
cleanupAbruptTermination(ceremonyName) {
|
|
310
|
+
const lastExecution = this.getLastExecution(ceremonyName);
|
|
311
|
+
|
|
312
|
+
if (!lastExecution || lastExecution.status !== 'in-progress') {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.completeExecution(ceremonyName, lastExecution.id, 'abrupt-termination', {
|
|
317
|
+
note: 'Process was interrupted during LLM generation',
|
|
318
|
+
stage: lastExecution.stage
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get ceremony statistics
|
|
324
|
+
* @param {string} ceremonyName - Ceremony name
|
|
325
|
+
* @returns {Object} Statistics object
|
|
326
|
+
*/
|
|
327
|
+
getStats(ceremonyName) {
|
|
328
|
+
this.load();
|
|
329
|
+
|
|
330
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
331
|
+
return {
|
|
332
|
+
totalExecutions: 0,
|
|
333
|
+
successful: 0,
|
|
334
|
+
cancelled: 0,
|
|
335
|
+
aborted: 0,
|
|
336
|
+
lastRun: null,
|
|
337
|
+
lastSuccess: null
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const ceremony = this.data.ceremonies[ceremonyName];
|
|
342
|
+
const executions = ceremony.executions;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
totalExecutions: ceremony.totalExecutions,
|
|
346
|
+
successful: executions.filter(e => e.outcome === 'success').length,
|
|
347
|
+
cancelled: executions.filter(e => e.outcome === 'user-cancelled').length,
|
|
348
|
+
aborted: executions.filter(e => e.outcome === 'abrupt-termination').length,
|
|
349
|
+
lastRun: ceremony.lastRun,
|
|
350
|
+
lastSuccess: ceremony.lastSuccess
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check if a ceremony has completed successfully at least once
|
|
356
|
+
* @param {string} ceremonyName - Ceremony name
|
|
357
|
+
* @returns {boolean} True if ceremony has a successful completion
|
|
358
|
+
*/
|
|
359
|
+
hasSuccessfulCompletion(ceremonyName) {
|
|
360
|
+
this.load();
|
|
361
|
+
|
|
362
|
+
if (!this.data.ceremonies[ceremonyName]) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check if lastSuccess is set (indicates at least one successful execution)
|
|
367
|
+
return this.data.ceremonies[ceremonyName].lastSuccess !== null;
|
|
368
|
+
}
|
|
369
|
+
}
|