@girardmedia/bootspring 1.2.0 → 2.0.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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Git Autopilot
|
|
3
|
+
*
|
|
4
|
+
* Automatically triggers workflows based on git events.
|
|
5
|
+
* Monitors git activity and starts appropriate workflows.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Real-time git event watching (commits, branches, merges)
|
|
9
|
+
* - Configurable via bootspring.config.js
|
|
10
|
+
* - Integration with orchestrator workflows
|
|
11
|
+
* - Smart pattern matching for branch names and commit messages
|
|
12
|
+
*
|
|
13
|
+
* @package bootspring
|
|
14
|
+
* @module hooks/git-autopilot
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const EventEmitter = require('events');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Git event to workflow mapping
|
|
24
|
+
*/
|
|
25
|
+
const EVENT_WORKFLOWS = {
|
|
26
|
+
// Branch patterns
|
|
27
|
+
'branch:feature/*': 'feature-development',
|
|
28
|
+
'branch:feat/*': 'feature-development',
|
|
29
|
+
'branch:fix/*': 'feature-development',
|
|
30
|
+
'branch:hotfix/*': 'security-audit',
|
|
31
|
+
'branch:release/*': 'launch-preparation',
|
|
32
|
+
'branch:perf/*': 'performance-optimization',
|
|
33
|
+
'branch:security/*': 'security-audit',
|
|
34
|
+
'branch:api/*': 'api-development',
|
|
35
|
+
'branch:db/*': 'database-migration',
|
|
36
|
+
'branch:migration/*': 'database-migration',
|
|
37
|
+
|
|
38
|
+
// Commit message patterns
|
|
39
|
+
'commit:feat:': 'feature-development',
|
|
40
|
+
'commit:feature:': 'feature-development',
|
|
41
|
+
'commit:fix:': 'feature-development',
|
|
42
|
+
'commit:security:': 'security-audit',
|
|
43
|
+
'commit:perf:': 'performance-optimization',
|
|
44
|
+
'commit:refactor:': 'performance-optimization',
|
|
45
|
+
'commit:db:': 'database-migration',
|
|
46
|
+
'commit:migration:': 'database-migration',
|
|
47
|
+
|
|
48
|
+
// File change patterns
|
|
49
|
+
'files:prisma/': 'database-migration',
|
|
50
|
+
'files:schema.prisma': 'database-migration',
|
|
51
|
+
'files:src/api/': 'api-development',
|
|
52
|
+
'files:app/api/': 'api-development',
|
|
53
|
+
'files:pages/api/': 'api-development',
|
|
54
|
+
|
|
55
|
+
// Merge patterns
|
|
56
|
+
'merge:main': 'launch-preparation',
|
|
57
|
+
'merge:master': 'launch-preparation',
|
|
58
|
+
'merge:production': 'launch-preparation',
|
|
59
|
+
|
|
60
|
+
// Tag patterns
|
|
61
|
+
'tag:v*': 'launch-preparation',
|
|
62
|
+
'tag:release-*': 'launch-preparation'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Autopilot configuration defaults
|
|
67
|
+
*/
|
|
68
|
+
const DEFAULT_CONFIG = {
|
|
69
|
+
enabled: false,
|
|
70
|
+
autoStart: false, // Automatically start suggested workflow
|
|
71
|
+
requireConfirmation: true,
|
|
72
|
+
watchInterval: 5000, // Interval for polling git state (ms)
|
|
73
|
+
ignoreBranches: ['main', 'master', 'develop'],
|
|
74
|
+
ignorePatterns: ['wip:', 'WIP:', '[skip ci]', '[skip workflow]'],
|
|
75
|
+
eventMapping: EVENT_WORKFLOWS,
|
|
76
|
+
hooks: {
|
|
77
|
+
onBranchCreate: true,
|
|
78
|
+
onCommit: true,
|
|
79
|
+
onMerge: true,
|
|
80
|
+
onTag: true,
|
|
81
|
+
onFileChange: true
|
|
82
|
+
},
|
|
83
|
+
// Security: only allow specific workflows to auto-start
|
|
84
|
+
allowedAutoStartWorkflows: [
|
|
85
|
+
'feature-development',
|
|
86
|
+
'api-development'
|
|
87
|
+
],
|
|
88
|
+
// Cooldown period between workflow triggers (ms)
|
|
89
|
+
cooldownPeriod: 60000
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Git Autopilot Event Emitter
|
|
94
|
+
* Emits events for git activities
|
|
95
|
+
*/
|
|
96
|
+
class GitAutopilotEmitter extends EventEmitter {}
|
|
97
|
+
const autopilotEmitter = new GitAutopilotEmitter();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* In-memory state for the watcher
|
|
101
|
+
*/
|
|
102
|
+
const watcherState = {
|
|
103
|
+
isWatching: false,
|
|
104
|
+
watcherId: null,
|
|
105
|
+
lastBranch: null,
|
|
106
|
+
lastCommit: null,
|
|
107
|
+
lastMergeBase: null,
|
|
108
|
+
lastTags: [],
|
|
109
|
+
lastTriggerTime: 0,
|
|
110
|
+
processedCommits: new Set()
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get autopilot state file path
|
|
115
|
+
*/
|
|
116
|
+
function getAutopilotPath() {
|
|
117
|
+
const projectRoot = process.cwd();
|
|
118
|
+
return path.join(projectRoot, '.bootspring', 'autopilot-state.json');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Load configuration from bootspring.config.js
|
|
123
|
+
*/
|
|
124
|
+
function loadConfig() {
|
|
125
|
+
const projectRoot = process.cwd();
|
|
126
|
+
const configPaths = [
|
|
127
|
+
path.join(projectRoot, 'bootspring.config.js'),
|
|
128
|
+
path.join(projectRoot, 'bootspring.config.mjs'),
|
|
129
|
+
path.join(projectRoot, '.bootspringrc.js')
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const configPath of configPaths) {
|
|
133
|
+
if (fs.existsSync(configPath)) {
|
|
134
|
+
try {
|
|
135
|
+
// Clear require cache to get fresh config
|
|
136
|
+
delete require.cache[require.resolve(configPath)];
|
|
137
|
+
const config = require(configPath);
|
|
138
|
+
|
|
139
|
+
if (config.autopilot || config.gitAutopilot) {
|
|
140
|
+
return mergeConfig(DEFAULT_CONFIG, config.autopilot || config.gitAutopilot);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.warn(`Failed to load config from ${configPath}:`, err.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return DEFAULT_CONFIG;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Deep merge configuration objects
|
|
153
|
+
*/
|
|
154
|
+
function mergeConfig(defaults, overrides) {
|
|
155
|
+
const result = { ...defaults };
|
|
156
|
+
|
|
157
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
158
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
159
|
+
result[key] = mergeConfig(defaults[key] || {}, value);
|
|
160
|
+
} else {
|
|
161
|
+
result[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Load autopilot state
|
|
170
|
+
*/
|
|
171
|
+
function loadAutopilotState() {
|
|
172
|
+
const statePath = getAutopilotPath();
|
|
173
|
+
if (fs.existsSync(statePath)) {
|
|
174
|
+
try {
|
|
175
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
176
|
+
// Load config from bootspring.config.js first
|
|
177
|
+
const fileConfig = loadConfig();
|
|
178
|
+
// Merge: defaults <- file config <- persisted state config
|
|
179
|
+
// This ensures persisted runtime state (enabled, autoStart) takes precedence
|
|
180
|
+
state.config = mergeConfig(mergeConfig(DEFAULT_CONFIG, fileConfig), state.config || {});
|
|
181
|
+
return state;
|
|
182
|
+
} catch {
|
|
183
|
+
return createDefaultState();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return createDefaultState();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create default autopilot state
|
|
191
|
+
*/
|
|
192
|
+
function createDefaultState() {
|
|
193
|
+
const config = loadConfig();
|
|
194
|
+
return {
|
|
195
|
+
config,
|
|
196
|
+
lastCheck: null,
|
|
197
|
+
pendingSuggestions: [],
|
|
198
|
+
history: [],
|
|
199
|
+
triggeredWorkflows: [],
|
|
200
|
+
watcherActive: false
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Save autopilot state
|
|
206
|
+
*/
|
|
207
|
+
function saveAutopilotState(state) {
|
|
208
|
+
const statePath = getAutopilotPath();
|
|
209
|
+
const dir = path.dirname(statePath);
|
|
210
|
+
if (!fs.existsSync(dir)) {
|
|
211
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
state.lastUpdated = new Date().toISOString();
|
|
214
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get current git branch
|
|
219
|
+
*/
|
|
220
|
+
function getCurrentBranch() {
|
|
221
|
+
try {
|
|
222
|
+
return execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the current HEAD commit hash
|
|
230
|
+
*/
|
|
231
|
+
function getCurrentCommit() {
|
|
232
|
+
try {
|
|
233
|
+
return execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get recent commits
|
|
241
|
+
*/
|
|
242
|
+
function getRecentCommits(count = 5) {
|
|
243
|
+
try {
|
|
244
|
+
const output = execSync(`git log -${count} --pretty=format:"%H|%s|%an|%ai"`, { encoding: 'utf-8' });
|
|
245
|
+
return output.split('\n').filter(Boolean).map(line => {
|
|
246
|
+
const [hash, message, author, date] = line.split('|');
|
|
247
|
+
return { hash, message, author, date };
|
|
248
|
+
});
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get changed files in recent commits
|
|
256
|
+
*/
|
|
257
|
+
function getChangedFiles(count = 5) {
|
|
258
|
+
try {
|
|
259
|
+
const output = execSync(`git diff --name-only HEAD~${count}..HEAD 2>/dev/null || git diff --name-only HEAD`, { encoding: 'utf-8' });
|
|
260
|
+
return output.split('\n').filter(Boolean);
|
|
261
|
+
} catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get recent tags
|
|
268
|
+
*/
|
|
269
|
+
function getRecentTags(count = 3) {
|
|
270
|
+
try {
|
|
271
|
+
const output = execSync(`git tag --sort=-creatordate 2>/dev/null | head -${count}`, { encoding: 'utf-8', shell: true });
|
|
272
|
+
return output.split('\n').filter(Boolean);
|
|
273
|
+
} catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if current branch was recently merged from another
|
|
280
|
+
*/
|
|
281
|
+
function checkForMerge(targetBranch = 'main') {
|
|
282
|
+
try {
|
|
283
|
+
// Get the merge base between HEAD and the target branch
|
|
284
|
+
const mergeBase = execSync(`git merge-base HEAD origin/${targetBranch} 2>/dev/null || echo ""`, {
|
|
285
|
+
encoding: 'utf-8',
|
|
286
|
+
shell: true
|
|
287
|
+
}).trim();
|
|
288
|
+
|
|
289
|
+
// Check if HEAD is an ancestor of target branch (meaning we merged TO it)
|
|
290
|
+
const isMerged = execSync(`git branch -r --contains HEAD 2>/dev/null | grep -E "origin/${targetBranch}$" || echo ""`, {
|
|
291
|
+
encoding: 'utf-8',
|
|
292
|
+
shell: true
|
|
293
|
+
}).trim();
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
mergeBase,
|
|
297
|
+
isMerged: isMerged.length > 0,
|
|
298
|
+
targetBranch
|
|
299
|
+
};
|
|
300
|
+
} catch {
|
|
301
|
+
return { mergeBase: null, isMerged: false, targetBranch };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Detect if a new branch was created
|
|
307
|
+
*/
|
|
308
|
+
function detectBranchCreation() {
|
|
309
|
+
try {
|
|
310
|
+
// Check if this branch has only a few commits that aren't on main/master
|
|
311
|
+
const baseCommitCount = execSync(
|
|
312
|
+
'git rev-list --count HEAD ^origin/main 2>/dev/null || git rev-list --count HEAD ^origin/master 2>/dev/null || echo "0"',
|
|
313
|
+
{ encoding: 'utf-8', shell: true }
|
|
314
|
+
).trim();
|
|
315
|
+
|
|
316
|
+
const count = parseInt(baseCommitCount, 10);
|
|
317
|
+
|
|
318
|
+
// If less than 5 unique commits, likely a new feature branch
|
|
319
|
+
return count > 0 && count < 5;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Match a pattern against a value
|
|
327
|
+
*/
|
|
328
|
+
function matchPattern(pattern, value) {
|
|
329
|
+
// Simple glob matching
|
|
330
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
331
|
+
return regex.test(value);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if a message should be ignored
|
|
336
|
+
*/
|
|
337
|
+
function shouldIgnoreMessage(message, ignorePatterns) {
|
|
338
|
+
const msgLower = message.toLowerCase();
|
|
339
|
+
return ignorePatterns.some(pattern =>
|
|
340
|
+
msgLower.includes(pattern.toLowerCase())
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Analyze git state and suggest workflow
|
|
346
|
+
*/
|
|
347
|
+
function analyzeGitState(options = {}) {
|
|
348
|
+
const state = loadAutopilotState();
|
|
349
|
+
|
|
350
|
+
if (!state.config.enabled && !options.force) {
|
|
351
|
+
return { enabled: false, suggestions: [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const config = state.config;
|
|
355
|
+
const branch = getCurrentBranch();
|
|
356
|
+
const commits = getRecentCommits();
|
|
357
|
+
const files = getChangedFiles();
|
|
358
|
+
const tags = getRecentTags();
|
|
359
|
+
const mergeInfo = checkForMerge('main');
|
|
360
|
+
const isNewBranch = detectBranchCreation();
|
|
361
|
+
|
|
362
|
+
const suggestions = [];
|
|
363
|
+
const mapping = config.eventMapping || EVENT_WORKFLOWS;
|
|
364
|
+
|
|
365
|
+
// Check branch patterns
|
|
366
|
+
if (branch && !config.ignoreBranches.includes(branch)) {
|
|
367
|
+
for (const [pattern, workflow] of Object.entries(mapping)) {
|
|
368
|
+
if (pattern.startsWith('branch:')) {
|
|
369
|
+
const branchPattern = pattern.replace('branch:', '');
|
|
370
|
+
if (matchPattern(branchPattern, branch)) {
|
|
371
|
+
suggestions.push({
|
|
372
|
+
trigger: 'branch',
|
|
373
|
+
triggerType: isNewBranch ? 'branch_create' : 'branch_active',
|
|
374
|
+
pattern: branchPattern,
|
|
375
|
+
value: branch,
|
|
376
|
+
workflow,
|
|
377
|
+
confidence: isNewBranch ? 0.9 : 0.7,
|
|
378
|
+
isNewBranch,
|
|
379
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check merge patterns
|
|
387
|
+
if (config.hooks.onMerge && mergeInfo.isMerged) {
|
|
388
|
+
for (const [pattern, workflow] of Object.entries(mapping)) {
|
|
389
|
+
if (pattern.startsWith('merge:')) {
|
|
390
|
+
const targetBranch = pattern.replace('merge:', '');
|
|
391
|
+
if (mergeInfo.targetBranch === targetBranch ||
|
|
392
|
+
matchPattern(targetBranch, mergeInfo.targetBranch)) {
|
|
393
|
+
suggestions.push({
|
|
394
|
+
trigger: 'merge',
|
|
395
|
+
triggerType: 'merge_to_main',
|
|
396
|
+
pattern: targetBranch,
|
|
397
|
+
value: `Merged to ${mergeInfo.targetBranch}`,
|
|
398
|
+
workflow,
|
|
399
|
+
confidence: 0.95,
|
|
400
|
+
mergeBase: mergeInfo.mergeBase,
|
|
401
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Check commit message patterns
|
|
409
|
+
if (config.hooks.onCommit) {
|
|
410
|
+
for (const commit of commits) {
|
|
411
|
+
// Skip ignored patterns
|
|
412
|
+
if (shouldIgnoreMessage(commit.message, config.ignorePatterns)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Skip already processed commits
|
|
417
|
+
if (watcherState.processedCommits.has(commit.hash)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const [pattern, workflow] of Object.entries(mapping)) {
|
|
422
|
+
if (pattern.startsWith('commit:')) {
|
|
423
|
+
const messagePattern = pattern.replace('commit:', '');
|
|
424
|
+
if (commit.message.toLowerCase().startsWith(messagePattern)) {
|
|
425
|
+
suggestions.push({
|
|
426
|
+
trigger: 'commit',
|
|
427
|
+
triggerType: 'commit_message',
|
|
428
|
+
pattern: messagePattern,
|
|
429
|
+
value: commit.message,
|
|
430
|
+
workflow,
|
|
431
|
+
confidence: 0.8,
|
|
432
|
+
commitHash: commit.hash,
|
|
433
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
|
|
434
|
+
});
|
|
435
|
+
break; // Only one suggestion per commit
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Check file patterns
|
|
443
|
+
if (config.hooks.onFileChange) {
|
|
444
|
+
for (const file of files) {
|
|
445
|
+
for (const [pattern, workflow] of Object.entries(mapping)) {
|
|
446
|
+
if (pattern.startsWith('files:')) {
|
|
447
|
+
const filePattern = pattern.replace('files:', '');
|
|
448
|
+
if (file.startsWith(filePattern) || file.includes(filePattern)) {
|
|
449
|
+
suggestions.push({
|
|
450
|
+
trigger: 'file',
|
|
451
|
+
triggerType: 'file_change',
|
|
452
|
+
pattern: filePattern,
|
|
453
|
+
value: file,
|
|
454
|
+
workflow,
|
|
455
|
+
confidence: 0.7,
|
|
456
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
|
|
457
|
+
});
|
|
458
|
+
break; // Only one suggestion per file
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Check tag patterns
|
|
466
|
+
if (config.hooks.onTag) {
|
|
467
|
+
for (const tag of tags) {
|
|
468
|
+
for (const [pattern, workflow] of Object.entries(mapping)) {
|
|
469
|
+
if (pattern.startsWith('tag:')) {
|
|
470
|
+
const tagPattern = pattern.replace('tag:', '');
|
|
471
|
+
if (matchPattern(tagPattern, tag)) {
|
|
472
|
+
suggestions.push({
|
|
473
|
+
trigger: 'tag',
|
|
474
|
+
triggerType: 'tag_create',
|
|
475
|
+
pattern: tagPattern,
|
|
476
|
+
value: tag,
|
|
477
|
+
workflow,
|
|
478
|
+
confidence: 0.95,
|
|
479
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Deduplicate by workflow (keep highest confidence)
|
|
488
|
+
const uniqueWorkflows = new Map();
|
|
489
|
+
for (const suggestion of suggestions) {
|
|
490
|
+
const existing = uniqueWorkflows.get(suggestion.workflow);
|
|
491
|
+
if (!existing || existing.confidence < suggestion.confidence) {
|
|
492
|
+
uniqueWorkflows.set(suggestion.workflow, suggestion);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const finalSuggestions = Array.from(uniqueWorkflows.values())
|
|
497
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
498
|
+
|
|
499
|
+
// Update state
|
|
500
|
+
state.lastCheck = new Date().toISOString();
|
|
501
|
+
state.pendingSuggestions = finalSuggestions;
|
|
502
|
+
saveAutopilotState(state);
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
enabled: true,
|
|
506
|
+
branch,
|
|
507
|
+
isNewBranch,
|
|
508
|
+
mergeInfo,
|
|
509
|
+
suggestions: finalSuggestions,
|
|
510
|
+
autoStartEnabled: config.autoStart
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Enable autopilot
|
|
516
|
+
*/
|
|
517
|
+
function enable(options = {}) {
|
|
518
|
+
const state = loadAutopilotState();
|
|
519
|
+
state.config.enabled = true;
|
|
520
|
+
state.config = { ...state.config, ...options };
|
|
521
|
+
|
|
522
|
+
state.history.push({
|
|
523
|
+
action: 'enabled',
|
|
524
|
+
timestamp: new Date().toISOString(),
|
|
525
|
+
options
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
saveAutopilotState(state);
|
|
529
|
+
|
|
530
|
+
return { success: true, config: state.config };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Disable autopilot
|
|
535
|
+
*/
|
|
536
|
+
function disable() {
|
|
537
|
+
const state = loadAutopilotState();
|
|
538
|
+
state.config.enabled = false;
|
|
539
|
+
|
|
540
|
+
// Stop watcher if running
|
|
541
|
+
if (watcherState.isWatching) {
|
|
542
|
+
stopWatcher();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
state.history.push({
|
|
546
|
+
action: 'disabled',
|
|
547
|
+
timestamp: new Date().toISOString()
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
saveAutopilotState(state);
|
|
551
|
+
|
|
552
|
+
return { success: true };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get autopilot status
|
|
557
|
+
*/
|
|
558
|
+
function getStatus() {
|
|
559
|
+
const state = loadAutopilotState();
|
|
560
|
+
return {
|
|
561
|
+
enabled: state.config.enabled,
|
|
562
|
+
config: state.config,
|
|
563
|
+
lastCheck: state.lastCheck,
|
|
564
|
+
pendingSuggestions: state.pendingSuggestions.length,
|
|
565
|
+
suggestions: state.pendingSuggestions,
|
|
566
|
+
watcherActive: watcherState.isWatching,
|
|
567
|
+
triggeredWorkflows: state.triggeredWorkflows || []
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Accept a workflow suggestion
|
|
573
|
+
* @param {string} workflow - Workflow name to accept
|
|
574
|
+
*/
|
|
575
|
+
function acceptSuggestion(workflow) {
|
|
576
|
+
const state = loadAutopilotState();
|
|
577
|
+
|
|
578
|
+
const suggestion = state.pendingSuggestions.find(s => s.workflow === workflow);
|
|
579
|
+
if (!suggestion) {
|
|
580
|
+
return { success: false, error: 'Suggestion not found' };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Remove from pending
|
|
584
|
+
state.pendingSuggestions = state.pendingSuggestions.filter(s => s.workflow !== workflow);
|
|
585
|
+
|
|
586
|
+
state.history.push({
|
|
587
|
+
action: 'suggestion_accepted',
|
|
588
|
+
workflow,
|
|
589
|
+
trigger: suggestion.trigger,
|
|
590
|
+
triggerType: suggestion.triggerType,
|
|
591
|
+
timestamp: new Date().toISOString()
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Track triggered workflows
|
|
595
|
+
state.triggeredWorkflows = state.triggeredWorkflows || [];
|
|
596
|
+
state.triggeredWorkflows.push({
|
|
597
|
+
workflow,
|
|
598
|
+
trigger: suggestion.trigger,
|
|
599
|
+
timestamp: new Date().toISOString()
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
saveAutopilotState(state);
|
|
603
|
+
|
|
604
|
+
// Start the workflow
|
|
605
|
+
const orchestrator = require('../intelligence/orchestrator');
|
|
606
|
+
const result = orchestrator.startWorkflow(workflow);
|
|
607
|
+
|
|
608
|
+
// Emit event
|
|
609
|
+
autopilotEmitter.emit('workflow:started', {
|
|
610
|
+
workflow,
|
|
611
|
+
trigger: suggestion.trigger,
|
|
612
|
+
triggerType: suggestion.triggerType,
|
|
613
|
+
result
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
success: true,
|
|
618
|
+
workflow,
|
|
619
|
+
startResult: result
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Dismiss a workflow suggestion
|
|
625
|
+
* @param {string} workflow - Workflow name to dismiss
|
|
626
|
+
*/
|
|
627
|
+
function dismissSuggestion(workflow) {
|
|
628
|
+
const state = loadAutopilotState();
|
|
629
|
+
|
|
630
|
+
state.pendingSuggestions = state.pendingSuggestions.filter(s => s.workflow !== workflow);
|
|
631
|
+
|
|
632
|
+
state.history.push({
|
|
633
|
+
action: 'suggestion_dismissed',
|
|
634
|
+
workflow,
|
|
635
|
+
timestamp: new Date().toISOString()
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
saveAutopilotState(state);
|
|
639
|
+
|
|
640
|
+
return { success: true, workflow };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Add custom event mapping
|
|
645
|
+
* @param {string} pattern - Event pattern (e.g., 'branch:custom/*')
|
|
646
|
+
* @param {string} workflow - Workflow to trigger
|
|
647
|
+
*/
|
|
648
|
+
function addEventMapping(pattern, workflow) {
|
|
649
|
+
const state = loadAutopilotState();
|
|
650
|
+
state.config.eventMapping = state.config.eventMapping || {};
|
|
651
|
+
state.config.eventMapping[pattern] = workflow;
|
|
652
|
+
saveAutopilotState(state);
|
|
653
|
+
|
|
654
|
+
return { success: true, pattern, workflow };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Remove event mapping
|
|
659
|
+
* @param {string} pattern - Event pattern to remove
|
|
660
|
+
*/
|
|
661
|
+
function removeEventMapping(pattern) {
|
|
662
|
+
const state = loadAutopilotState();
|
|
663
|
+
if (state.config.eventMapping && state.config.eventMapping[pattern]) {
|
|
664
|
+
delete state.config.eventMapping[pattern];
|
|
665
|
+
saveAutopilotState(state);
|
|
666
|
+
return { success: true, pattern };
|
|
667
|
+
}
|
|
668
|
+
return { success: false, error: 'Pattern not found' };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Start the git watcher
|
|
673
|
+
* Monitors git state and triggers workflows automatically
|
|
674
|
+
*/
|
|
675
|
+
function startWatcher() {
|
|
676
|
+
const state = loadAutopilotState();
|
|
677
|
+
|
|
678
|
+
if (!state.config.enabled) {
|
|
679
|
+
return { success: false, error: 'Autopilot is not enabled. Call enable() first.' };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (watcherState.isWatching) {
|
|
683
|
+
return { success: false, error: 'Watcher is already running' };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Initialize watcher state
|
|
687
|
+
watcherState.lastBranch = getCurrentBranch();
|
|
688
|
+
watcherState.lastCommit = getCurrentCommit();
|
|
689
|
+
watcherState.lastTags = getRecentTags();
|
|
690
|
+
watcherState.isWatching = true;
|
|
691
|
+
watcherState.processedCommits = new Set();
|
|
692
|
+
|
|
693
|
+
const interval = state.config.watchInterval || 5000;
|
|
694
|
+
|
|
695
|
+
// Start the polling interval
|
|
696
|
+
watcherState.watcherId = setInterval(() => {
|
|
697
|
+
pollGitState();
|
|
698
|
+
}, interval);
|
|
699
|
+
|
|
700
|
+
// Update state
|
|
701
|
+
state.watcherActive = true;
|
|
702
|
+
state.watcherStartedAt = new Date().toISOString();
|
|
703
|
+
saveAutopilotState(state);
|
|
704
|
+
|
|
705
|
+
autopilotEmitter.emit('watcher:started', { interval });
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
success: true,
|
|
709
|
+
interval,
|
|
710
|
+
message: `Git watcher started. Polling every ${interval}ms`
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Stop the git watcher
|
|
716
|
+
*/
|
|
717
|
+
function stopWatcher() {
|
|
718
|
+
if (!watcherState.isWatching) {
|
|
719
|
+
return { success: false, error: 'Watcher is not running' };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (watcherState.watcherId) {
|
|
723
|
+
clearInterval(watcherState.watcherId);
|
|
724
|
+
watcherState.watcherId = null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
watcherState.isWatching = false;
|
|
728
|
+
|
|
729
|
+
const state = loadAutopilotState();
|
|
730
|
+
state.watcherActive = false;
|
|
731
|
+
state.watcherStoppedAt = new Date().toISOString();
|
|
732
|
+
saveAutopilotState(state);
|
|
733
|
+
|
|
734
|
+
autopilotEmitter.emit('watcher:stopped', {});
|
|
735
|
+
|
|
736
|
+
return { success: true, message: 'Git watcher stopped' };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Poll git state for changes
|
|
741
|
+
* Called periodically by the watcher
|
|
742
|
+
*/
|
|
743
|
+
function pollGitState() {
|
|
744
|
+
const state = loadAutopilotState();
|
|
745
|
+
const config = state.config;
|
|
746
|
+
|
|
747
|
+
// Check for branch change
|
|
748
|
+
const currentBranch = getCurrentBranch();
|
|
749
|
+
if (currentBranch !== watcherState.lastBranch) {
|
|
750
|
+
const isNewBranch = detectBranchCreation();
|
|
751
|
+
|
|
752
|
+
autopilotEmitter.emit('git:branch_change', {
|
|
753
|
+
from: watcherState.lastBranch,
|
|
754
|
+
to: currentBranch,
|
|
755
|
+
isNew: isNewBranch
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
if (isNewBranch && config.hooks.onBranchCreate) {
|
|
759
|
+
handleBranchCreate(currentBranch);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
watcherState.lastBranch = currentBranch;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check for new commits
|
|
766
|
+
const currentCommit = getCurrentCommit();
|
|
767
|
+
if (currentCommit !== watcherState.lastCommit) {
|
|
768
|
+
autopilotEmitter.emit('git:commit', {
|
|
769
|
+
from: watcherState.lastCommit,
|
|
770
|
+
to: currentCommit
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
if (config.hooks.onCommit) {
|
|
774
|
+
handleNewCommit(currentCommit);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
watcherState.lastCommit = currentCommit;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Check for merges to main
|
|
781
|
+
if (config.hooks.onMerge) {
|
|
782
|
+
const mergeInfo = checkForMerge('main');
|
|
783
|
+
if (mergeInfo.isMerged && mergeInfo.mergeBase !== watcherState.lastMergeBase) {
|
|
784
|
+
autopilotEmitter.emit('git:merge', {
|
|
785
|
+
targetBranch: mergeInfo.targetBranch,
|
|
786
|
+
mergeBase: mergeInfo.mergeBase
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
handleMerge(mergeInfo);
|
|
790
|
+
watcherState.lastMergeBase = mergeInfo.mergeBase;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Check for new tags
|
|
795
|
+
if (config.hooks.onTag) {
|
|
796
|
+
const currentTags = getRecentTags();
|
|
797
|
+
const newTags = currentTags.filter(t => !watcherState.lastTags.includes(t));
|
|
798
|
+
|
|
799
|
+
if (newTags.length > 0) {
|
|
800
|
+
autopilotEmitter.emit('git:tag', { newTags });
|
|
801
|
+
handleNewTags(newTags);
|
|
802
|
+
watcherState.lastTags = currentTags;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Handle branch creation event
|
|
809
|
+
*/
|
|
810
|
+
function handleBranchCreate(branch) {
|
|
811
|
+
const state = loadAutopilotState();
|
|
812
|
+
const config = state.config;
|
|
813
|
+
|
|
814
|
+
// Skip ignored branches
|
|
815
|
+
if (config.ignoreBranches.includes(branch)) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Check cooldown
|
|
820
|
+
if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Find matching workflow
|
|
825
|
+
for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
|
|
826
|
+
if (pattern.startsWith('branch:')) {
|
|
827
|
+
const branchPattern = pattern.replace('branch:', '');
|
|
828
|
+
if (matchPattern(branchPattern, branch)) {
|
|
829
|
+
const suggestion = {
|
|
830
|
+
trigger: 'branch',
|
|
831
|
+
triggerType: 'branch_create',
|
|
832
|
+
pattern: branchPattern,
|
|
833
|
+
value: branch,
|
|
834
|
+
workflow,
|
|
835
|
+
confidence: 0.9,
|
|
836
|
+
isNewBranch: true,
|
|
837
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
|
|
838
|
+
timestamp: new Date().toISOString()
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// Add to pending suggestions
|
|
842
|
+
state.pendingSuggestions = state.pendingSuggestions || [];
|
|
843
|
+
state.pendingSuggestions.push(suggestion);
|
|
844
|
+
saveAutopilotState(state);
|
|
845
|
+
|
|
846
|
+
autopilotEmitter.emit('suggestion:created', suggestion);
|
|
847
|
+
|
|
848
|
+
// Auto-start if configured
|
|
849
|
+
if (config.autoStart && suggestion.autoStartEligible) {
|
|
850
|
+
watcherState.lastTriggerTime = Date.now();
|
|
851
|
+
acceptSuggestion(workflow);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return; // Only one workflow per branch
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Handle new commit event
|
|
862
|
+
*/
|
|
863
|
+
function handleNewCommit(commitHash) {
|
|
864
|
+
const state = loadAutopilotState();
|
|
865
|
+
const config = state.config;
|
|
866
|
+
|
|
867
|
+
// Skip if already processed
|
|
868
|
+
if (watcherState.processedCommits.has(commitHash)) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Get commit details
|
|
873
|
+
const commits = getRecentCommits(1);
|
|
874
|
+
if (commits.length === 0) return;
|
|
875
|
+
|
|
876
|
+
const commit = commits[0];
|
|
877
|
+
|
|
878
|
+
// Skip ignored patterns
|
|
879
|
+
if (shouldIgnoreMessage(commit.message, config.ignorePatterns)) {
|
|
880
|
+
watcherState.processedCommits.add(commitHash);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Check cooldown
|
|
885
|
+
if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
|
|
886
|
+
watcherState.processedCommits.add(commitHash);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Find matching workflow
|
|
891
|
+
for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
|
|
892
|
+
if (pattern.startsWith('commit:')) {
|
|
893
|
+
const messagePattern = pattern.replace('commit:', '');
|
|
894
|
+
if (commit.message.toLowerCase().startsWith(messagePattern)) {
|
|
895
|
+
const suggestion = {
|
|
896
|
+
trigger: 'commit',
|
|
897
|
+
triggerType: 'commit_message',
|
|
898
|
+
pattern: messagePattern,
|
|
899
|
+
value: commit.message,
|
|
900
|
+
workflow,
|
|
901
|
+
confidence: 0.8,
|
|
902
|
+
commitHash,
|
|
903
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
|
|
904
|
+
timestamp: new Date().toISOString()
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// Add to pending suggestions
|
|
908
|
+
state.pendingSuggestions = state.pendingSuggestions || [];
|
|
909
|
+
state.pendingSuggestions.push(suggestion);
|
|
910
|
+
saveAutopilotState(state);
|
|
911
|
+
|
|
912
|
+
autopilotEmitter.emit('suggestion:created', suggestion);
|
|
913
|
+
|
|
914
|
+
// Auto-start if configured
|
|
915
|
+
if (config.autoStart && suggestion.autoStartEligible) {
|
|
916
|
+
watcherState.lastTriggerTime = Date.now();
|
|
917
|
+
acceptSuggestion(workflow);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
watcherState.processedCommits.add(commitHash);
|
|
921
|
+
return; // Only one workflow per commit
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
watcherState.processedCommits.add(commitHash);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Handle merge event
|
|
931
|
+
*/
|
|
932
|
+
function handleMerge(mergeInfo) {
|
|
933
|
+
const state = loadAutopilotState();
|
|
934
|
+
const config = state.config;
|
|
935
|
+
|
|
936
|
+
// Check cooldown
|
|
937
|
+
if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Find matching workflow
|
|
942
|
+
for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
|
|
943
|
+
if (pattern.startsWith('merge:')) {
|
|
944
|
+
const targetBranch = pattern.replace('merge:', '');
|
|
945
|
+
if (mergeInfo.targetBranch === targetBranch ||
|
|
946
|
+
matchPattern(targetBranch, mergeInfo.targetBranch)) {
|
|
947
|
+
const suggestion = {
|
|
948
|
+
trigger: 'merge',
|
|
949
|
+
triggerType: 'merge_to_main',
|
|
950
|
+
pattern: targetBranch,
|
|
951
|
+
value: `Merged to ${mergeInfo.targetBranch}`,
|
|
952
|
+
workflow,
|
|
953
|
+
confidence: 0.95,
|
|
954
|
+
mergeBase: mergeInfo.mergeBase,
|
|
955
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
|
|
956
|
+
timestamp: new Date().toISOString()
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// Add to pending suggestions
|
|
960
|
+
state.pendingSuggestions = state.pendingSuggestions || [];
|
|
961
|
+
state.pendingSuggestions.push(suggestion);
|
|
962
|
+
saveAutopilotState(state);
|
|
963
|
+
|
|
964
|
+
autopilotEmitter.emit('suggestion:created', suggestion);
|
|
965
|
+
|
|
966
|
+
// Always suggest launch-preparation on merge to main, but don't auto-start
|
|
967
|
+
// as it's a critical workflow
|
|
968
|
+
if (config.autoStart && suggestion.autoStartEligible) {
|
|
969
|
+
watcherState.lastTriggerTime = Date.now();
|
|
970
|
+
acceptSuggestion(workflow);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return; // Only one workflow per merge
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Handle new tags event
|
|
981
|
+
*/
|
|
982
|
+
function handleNewTags(tags) {
|
|
983
|
+
const state = loadAutopilotState();
|
|
984
|
+
const config = state.config;
|
|
985
|
+
|
|
986
|
+
// Check cooldown
|
|
987
|
+
if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
for (const tag of tags) {
|
|
992
|
+
for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
|
|
993
|
+
if (pattern.startsWith('tag:')) {
|
|
994
|
+
const tagPattern = pattern.replace('tag:', '');
|
|
995
|
+
if (matchPattern(tagPattern, tag)) {
|
|
996
|
+
const suggestion = {
|
|
997
|
+
trigger: 'tag',
|
|
998
|
+
triggerType: 'tag_create',
|
|
999
|
+
pattern: tagPattern,
|
|
1000
|
+
value: tag,
|
|
1001
|
+
workflow,
|
|
1002
|
+
confidence: 0.95,
|
|
1003
|
+
autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
|
|
1004
|
+
timestamp: new Date().toISOString()
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// Add to pending suggestions
|
|
1008
|
+
state.pendingSuggestions = state.pendingSuggestions || [];
|
|
1009
|
+
state.pendingSuggestions.push(suggestion);
|
|
1010
|
+
saveAutopilotState(state);
|
|
1011
|
+
|
|
1012
|
+
autopilotEmitter.emit('suggestion:created', suggestion);
|
|
1013
|
+
|
|
1014
|
+
// Tags usually indicate releases - don't auto-start
|
|
1015
|
+
if (config.autoStart && suggestion.autoStartEligible) {
|
|
1016
|
+
watcherState.lastTriggerTime = Date.now();
|
|
1017
|
+
acceptSuggestion(workflow);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return; // Only one workflow per tag
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Install git hooks for real-time triggering
|
|
1029
|
+
* This provides more immediate response than polling
|
|
1030
|
+
*/
|
|
1031
|
+
function installGitHooks() {
|
|
1032
|
+
const projectRoot = process.cwd();
|
|
1033
|
+
const hooksDir = path.join(projectRoot, '.git', 'hooks');
|
|
1034
|
+
|
|
1035
|
+
if (!fs.existsSync(hooksDir)) {
|
|
1036
|
+
return { success: false, error: 'Not a git repository or .git/hooks not found' };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const hooks = {
|
|
1040
|
+
'post-commit': `#!/bin/sh
|
|
1041
|
+
# Bootspring Git Autopilot - Post Commit Hook
|
|
1042
|
+
node -e "try { require('bootspring/hooks/git-autopilot').onGitHook('post-commit'); } catch(e) {}" 2>/dev/null || true
|
|
1043
|
+
`,
|
|
1044
|
+
'post-checkout': `#!/bin/sh
|
|
1045
|
+
# Bootspring Git Autopilot - Post Checkout Hook
|
|
1046
|
+
# $3 is 1 if this is a branch checkout
|
|
1047
|
+
if [ "$3" = "1" ]; then
|
|
1048
|
+
node -e "try { require('bootspring/hooks/git-autopilot').onGitHook('post-checkout', { branch: true }); } catch(e) {}" 2>/dev/null || true
|
|
1049
|
+
fi
|
|
1050
|
+
`,
|
|
1051
|
+
'post-merge': `#!/bin/sh
|
|
1052
|
+
# Bootspring Git Autopilot - Post Merge Hook
|
|
1053
|
+
node -e "try { require('bootspring/hooks/git-autopilot').onGitHook('post-merge'); } catch(e) {}" 2>/dev/null || true
|
|
1054
|
+
`
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
const installed = [];
|
|
1058
|
+
const skipped = [];
|
|
1059
|
+
|
|
1060
|
+
for (const [hookName, hookContent] of Object.entries(hooks)) {
|
|
1061
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
1062
|
+
|
|
1063
|
+
// Check if hook already exists
|
|
1064
|
+
if (fs.existsSync(hookPath)) {
|
|
1065
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
1066
|
+
if (existing.includes('Bootspring Git Autopilot')) {
|
|
1067
|
+
skipped.push(hookName);
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
// Append to existing hook
|
|
1071
|
+
fs.appendFileSync(hookPath, '\n' + hookContent);
|
|
1072
|
+
} else {
|
|
1073
|
+
fs.writeFileSync(hookPath, hookContent);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Make executable
|
|
1077
|
+
fs.chmodSync(hookPath, '755');
|
|
1078
|
+
installed.push(hookName);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return {
|
|
1082
|
+
success: true,
|
|
1083
|
+
installed,
|
|
1084
|
+
skipped,
|
|
1085
|
+
message: `Installed ${installed.length} hooks, skipped ${skipped.length} (already present)`
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Remove git hooks
|
|
1091
|
+
*/
|
|
1092
|
+
function removeGitHooks() {
|
|
1093
|
+
const projectRoot = process.cwd();
|
|
1094
|
+
const hooksDir = path.join(projectRoot, '.git', 'hooks');
|
|
1095
|
+
|
|
1096
|
+
if (!fs.existsSync(hooksDir)) {
|
|
1097
|
+
return { success: false, error: 'Not a git repository' };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const hookNames = ['post-commit', 'post-checkout', 'post-merge'];
|
|
1101
|
+
const removed = [];
|
|
1102
|
+
|
|
1103
|
+
for (const hookName of hookNames) {
|
|
1104
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
1105
|
+
|
|
1106
|
+
if (fs.existsSync(hookPath)) {
|
|
1107
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
1108
|
+
|
|
1109
|
+
if (content.includes('Bootspring Git Autopilot')) {
|
|
1110
|
+
// Remove bootspring-specific lines
|
|
1111
|
+
const lines = content.split('\n');
|
|
1112
|
+
const filtered = lines.filter(line =>
|
|
1113
|
+
!line.includes('Bootspring Git Autopilot') &&
|
|
1114
|
+
!line.includes('bootspring/hooks/git-autopilot')
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
if (filtered.filter(l => l.trim() && !l.startsWith('#!')).length === 0) {
|
|
1118
|
+
// If only shebang remains, delete the file
|
|
1119
|
+
fs.unlinkSync(hookPath);
|
|
1120
|
+
} else {
|
|
1121
|
+
fs.writeFileSync(hookPath, filtered.join('\n'));
|
|
1122
|
+
}
|
|
1123
|
+
removed.push(hookName);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
success: true,
|
|
1130
|
+
removed,
|
|
1131
|
+
message: `Removed ${removed.length} hooks`
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Git hook handler - called from git hooks
|
|
1137
|
+
*/
|
|
1138
|
+
function onGitHook(hookType, options = {}) {
|
|
1139
|
+
const state = loadAutopilotState();
|
|
1140
|
+
|
|
1141
|
+
if (!state.config.enabled) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
switch (hookType) {
|
|
1146
|
+
case 'post-commit':
|
|
1147
|
+
if (state.config.hooks.onCommit) {
|
|
1148
|
+
const commit = getCurrentCommit();
|
|
1149
|
+
handleNewCommit(commit);
|
|
1150
|
+
}
|
|
1151
|
+
break;
|
|
1152
|
+
|
|
1153
|
+
case 'post-checkout':
|
|
1154
|
+
if (options.branch && state.config.hooks.onBranchCreate) {
|
|
1155
|
+
const branch = getCurrentBranch();
|
|
1156
|
+
const isNew = detectBranchCreation();
|
|
1157
|
+
if (isNew) {
|
|
1158
|
+
handleBranchCreate(branch);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
break;
|
|
1162
|
+
|
|
1163
|
+
case 'post-merge':
|
|
1164
|
+
if (state.config.hooks.onMerge) {
|
|
1165
|
+
const mergeInfo = checkForMerge('main');
|
|
1166
|
+
if (mergeInfo.isMerged) {
|
|
1167
|
+
handleMerge(mergeInfo);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Update configuration
|
|
1176
|
+
*/
|
|
1177
|
+
function updateConfig(updates) {
|
|
1178
|
+
const state = loadAutopilotState();
|
|
1179
|
+
state.config = mergeConfig(state.config, updates);
|
|
1180
|
+
saveAutopilotState(state);
|
|
1181
|
+
return { success: true, config: state.config };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Get event emitter for custom handlers
|
|
1186
|
+
*/
|
|
1187
|
+
function getEmitter() {
|
|
1188
|
+
return autopilotEmitter;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Clear all pending suggestions
|
|
1193
|
+
*/
|
|
1194
|
+
function clearSuggestions() {
|
|
1195
|
+
const state = loadAutopilotState();
|
|
1196
|
+
const count = state.pendingSuggestions.length;
|
|
1197
|
+
state.pendingSuggestions = [];
|
|
1198
|
+
saveAutopilotState(state);
|
|
1199
|
+
return { success: true, cleared: count };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Get history of autopilot actions
|
|
1204
|
+
*/
|
|
1205
|
+
function getHistory(limit = 50) {
|
|
1206
|
+
const state = loadAutopilotState();
|
|
1207
|
+
return (state.history || []).slice(-limit);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
module.exports = {
|
|
1211
|
+
// Core functions
|
|
1212
|
+
analyzeGitState,
|
|
1213
|
+
enable,
|
|
1214
|
+
disable,
|
|
1215
|
+
getStatus,
|
|
1216
|
+
|
|
1217
|
+
// Suggestion management
|
|
1218
|
+
acceptSuggestion,
|
|
1219
|
+
dismissSuggestion,
|
|
1220
|
+
clearSuggestions,
|
|
1221
|
+
|
|
1222
|
+
// Event mapping
|
|
1223
|
+
addEventMapping,
|
|
1224
|
+
removeEventMapping,
|
|
1225
|
+
|
|
1226
|
+
// Watcher
|
|
1227
|
+
startWatcher,
|
|
1228
|
+
stopWatcher,
|
|
1229
|
+
pollGitState,
|
|
1230
|
+
|
|
1231
|
+
// Git hooks
|
|
1232
|
+
installGitHooks,
|
|
1233
|
+
removeGitHooks,
|
|
1234
|
+
onGitHook,
|
|
1235
|
+
|
|
1236
|
+
// Configuration
|
|
1237
|
+
loadConfig,
|
|
1238
|
+
updateConfig,
|
|
1239
|
+
loadAutopilotState,
|
|
1240
|
+
|
|
1241
|
+
// Events
|
|
1242
|
+
getEmitter,
|
|
1243
|
+
|
|
1244
|
+
// History
|
|
1245
|
+
getHistory,
|
|
1246
|
+
|
|
1247
|
+
// Constants
|
|
1248
|
+
EVENT_WORKFLOWS,
|
|
1249
|
+
DEFAULT_CONFIG
|
|
1250
|
+
};
|