voidforge-build 23.11.0 → 23.11.2
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/dist/.claude/commands/git.md +36 -3
- package/dist/CHANGELOG.md +46 -0
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/RELEASE_MANAGER.md +26 -0
- package/dist/scripts/voidforge.d.ts +4 -2
- package/dist/scripts/voidforge.js +106 -17
- package/package.json +1 -1
- package/dist/wizard/lib/anomaly-detection.d.ts +0 -59
- package/dist/wizard/lib/anomaly-detection.js +0 -122
- package/dist/wizard/lib/asset-scanner.d.ts +0 -23
- package/dist/wizard/lib/asset-scanner.js +0 -107
- package/dist/wizard/lib/build-analytics.d.ts +0 -39
- package/dist/wizard/lib/build-analytics.js +0 -91
- package/dist/wizard/lib/codegen/erd-gen.d.ts +0 -16
- package/dist/wizard/lib/codegen/erd-gen.js +0 -98
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +0 -15
- package/dist/wizard/lib/codegen/openapi-gen.js +0 -79
- package/dist/wizard/lib/codegen/prisma-types.d.ts +0 -15
- package/dist/wizard/lib/codegen/prisma-types.js +0 -44
- package/dist/wizard/lib/codegen/seed-gen.d.ts +0 -16
- package/dist/wizard/lib/codegen/seed-gen.js +0 -128
- package/dist/wizard/lib/correlation-engine.d.ts +0 -59
- package/dist/wizard/lib/correlation-engine.js +0 -152
- package/dist/wizard/lib/desktop-notify.d.ts +0 -27
- package/dist/wizard/lib/desktop-notify.js +0 -98
- package/dist/wizard/lib/image-gen.d.ts +0 -56
- package/dist/wizard/lib/image-gen.js +0 -159
- package/dist/wizard/lib/natural-language-deploy.d.ts +0 -30
- package/dist/wizard/lib/natural-language-deploy.js +0 -186
- package/dist/wizard/lib/route-optimizer.d.ts +0 -28
- package/dist/wizard/lib/route-optimizer.js +0 -93
- package/dist/wizard/lib/service-install.d.ts +0 -18
- package/dist/wizard/lib/service-install.js +0 -182
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Natural Language Deploy — resolve prose deployment descriptions to YAML frontmatter.
|
|
3
|
-
*
|
|
4
|
-
* Parse: "I want a $20/month server with SSL and daily backups"
|
|
5
|
-
* → { deploy: 'vps', instanceType: 't3.small', hostname: '', resilience: { backups: 'daily', ... } }
|
|
6
|
-
*
|
|
7
|
-
* Uses keyword matching and heuristics — no AI API call required.
|
|
8
|
-
*/
|
|
9
|
-
const BUDGET_TIERS = [
|
|
10
|
-
{ maxMonthly: 10, instanceType: 't3.micro', label: '~$8/mo' },
|
|
11
|
-
{ maxMonthly: 25, instanceType: 't3.small', label: '~$17/mo' },
|
|
12
|
-
{ maxMonthly: 50, instanceType: 't3.medium', label: '~$34/mo' },
|
|
13
|
-
{ maxMonthly: 100, instanceType: 't3.large', label: '~$68/mo' },
|
|
14
|
-
{ maxMonthly: Infinity, instanceType: 't3.xlarge', label: '~$136/mo' },
|
|
15
|
-
];
|
|
16
|
-
function resolveInstanceFromBudget(budget) {
|
|
17
|
-
return BUDGET_TIERS.find(t => budget <= t.maxMonthly) ?? BUDGET_TIERS[BUDGET_TIERS.length - 1];
|
|
18
|
-
}
|
|
19
|
-
// ── Keyword patterns ────────────────────────────
|
|
20
|
-
const PLATFORM_KEYWORDS = [
|
|
21
|
-
{ pattern: /\bvercel\b/i, target: 'vercel', reason: 'Vercel mentioned explicitly' },
|
|
22
|
-
{ pattern: /\brailway\b/i, target: 'railway', reason: 'Railway mentioned explicitly' },
|
|
23
|
-
{ pattern: /\bcloudflare\b/i, target: 'cloudflare', reason: 'Cloudflare mentioned explicitly' },
|
|
24
|
-
{ pattern: /\bdocker\b|\bcontainer\b/i, target: 'docker', reason: 'Docker/container mentioned' },
|
|
25
|
-
{ pattern: /\bstatic\s*(?:site|hosting|files?)\b/i, target: 'static', reason: 'Static site hosting' },
|
|
26
|
-
{ pattern: /\bvps\b|\bserver\b|\bec2\b|\baws\b|\bssh\b/i, target: 'vps', reason: 'Server/VPS/AWS mentioned' },
|
|
27
|
-
{ pattern: /\bserverless\b|\bedge\b/i, target: 'vercel', reason: 'Serverless/edge → Vercel' },
|
|
28
|
-
{ pattern: /\bfree\s*tier\b|\bno\s*cost\b|\bfree\b/i, target: 'railway', reason: 'Free tier → Railway' },
|
|
29
|
-
];
|
|
30
|
-
const FEATURE_KEYWORDS = [
|
|
31
|
-
{ pattern: /\bbackup/i, key: 'backups', reason: 'Backups requested' },
|
|
32
|
-
{ pattern: /\bssl\b|\bhttps\b|\btls\b/i, key: 'ssl', reason: 'SSL/TLS requested' },
|
|
33
|
-
{ pattern: /\bcustom\s*domain\b|\bmy\s*domain\b/i, key: 'customDomain', reason: 'Custom domain' },
|
|
34
|
-
{ pattern: /\brollback\b|\brevert\b/i, key: 'rollback', reason: 'Rollback requested' },
|
|
35
|
-
{ pattern: /\bpreview\b|\bpr\s*deploy/i, key: 'previewDeploys', reason: 'Preview deploys' },
|
|
36
|
-
{ pattern: /\bhealth\s*check\b|\bmonitoring\b|\buptime\b/i, key: 'healthCheck', reason: 'Health monitoring' },
|
|
37
|
-
{ pattern: /\brate\s*limit/i, key: 'rateLimiting', reason: 'Rate limiting' },
|
|
38
|
-
{ pattern: /\bgraceful\b|\bzero\s*downtime\b/i, key: 'gracefulShutdown', reason: 'Zero-downtime' },
|
|
39
|
-
{ pattern: /\bmulti\s*(?:env|environment)\b|\bstaging\b/i, key: 'multiEnv', reason: 'Multi-environment' },
|
|
40
|
-
{ pattern: /\berror\s*boundar/i, key: 'errorBoundaries', reason: 'Error boundaries' },
|
|
41
|
-
{ pattern: /\bdead\s*letter\b|\bdlq\b|\bretry\s*queue\b/i, key: 'deadLetterQueue', reason: 'Dead letter queue' },
|
|
42
|
-
{ pattern: /\bmigration/i, key: 'migrations', reason: 'Database migrations' },
|
|
43
|
-
];
|
|
44
|
-
const SCALE_KEYWORDS = [
|
|
45
|
-
{ pattern: /\bsmall\b|\bsimple\b|\bblog\b|\bpersonal\b|\bside\s*project\b|\bmvp\b|\bprototype\b/i, scale: 'small', reason: 'Small/simple project' },
|
|
46
|
-
{ pattern: /\bmedium\b|\bstartup\b|\bsaas\b|\bteam\b|\bgrow/i, scale: 'medium', reason: 'Medium/startup scale' },
|
|
47
|
-
{ pattern: /\blarge\b|\benterprise\b|\bthousands\b|\bhigh\s*traffic\b|\bscale\b|\bproduction\b/i, scale: 'large', reason: 'Large/production scale' },
|
|
48
|
-
];
|
|
49
|
-
// ── Main resolver ───────────────────────────────
|
|
50
|
-
export function resolveDeployConfig(prose) {
|
|
51
|
-
if (!prose.trim())
|
|
52
|
-
return null;
|
|
53
|
-
const reasoning = [];
|
|
54
|
-
const features = new Set();
|
|
55
|
-
// Extract budget — prefer amounts near cost keywords, fall back to first $N
|
|
56
|
-
const costContextMatch = prose.match(/(?:budget|spend|cost|month|mo)[^$]*\$(\d+(?:\.\d+)?)/i)
|
|
57
|
-
?? prose.match(/\$(\d+(?:\.\d+)?)(?:\s*\/\s*mo(?:nth)?)/i)
|
|
58
|
-
?? prose.match(/\$(\d+(?:\.\d+)?)/i);
|
|
59
|
-
const budget = costContextMatch ? Math.round(parseFloat(costContextMatch[1])) : -1;
|
|
60
|
-
// Detect explicit platform
|
|
61
|
-
let deploy = 'vps'; // default
|
|
62
|
-
let platformDetected = false;
|
|
63
|
-
for (const kw of PLATFORM_KEYWORDS) {
|
|
64
|
-
if (kw.pattern.test(prose)) {
|
|
65
|
-
deploy = kw.target;
|
|
66
|
-
reasoning.push(kw.reason);
|
|
67
|
-
platformDetected = true;
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Detect features
|
|
72
|
-
for (const kw of FEATURE_KEYWORDS) {
|
|
73
|
-
if (kw.pattern.test(prose)) {
|
|
74
|
-
features.add(kw.key);
|
|
75
|
-
reasoning.push(kw.reason);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// Detect scale
|
|
79
|
-
let scale = 'small';
|
|
80
|
-
for (const kw of SCALE_KEYWORDS) {
|
|
81
|
-
if (kw.pattern.test(prose)) {
|
|
82
|
-
scale = kw.scale;
|
|
83
|
-
reasoning.push(kw.reason);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// If no platform detected, infer from features and scale
|
|
88
|
-
if (!platformDetected) {
|
|
89
|
-
if (features.has('previewDeploys') || features.has('errorBoundaries')) {
|
|
90
|
-
deploy = 'vercel';
|
|
91
|
-
reasoning.push('Preview deploys/error boundaries → Vercel (best support)');
|
|
92
|
-
}
|
|
93
|
-
else if (scale === 'large' || features.has('customDomain') || budget > 30) {
|
|
94
|
-
deploy = 'vps';
|
|
95
|
-
reasoning.push('Large scale or custom domain with budget → VPS');
|
|
96
|
-
}
|
|
97
|
-
else if (scale === 'small' && budget < 0) {
|
|
98
|
-
deploy = 'railway';
|
|
99
|
-
reasoning.push('Small project, no budget specified → Railway (easiest start)');
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
deploy = 'vps';
|
|
103
|
-
reasoning.push('Default → VPS (most flexible)');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Resolve instance type from budget or scale
|
|
107
|
-
let instanceType = '';
|
|
108
|
-
let estimatedCost = '';
|
|
109
|
-
if (deploy === 'vps') {
|
|
110
|
-
if (budget >= 0) {
|
|
111
|
-
const tier = resolveInstanceFromBudget(budget);
|
|
112
|
-
instanceType = tier.instanceType;
|
|
113
|
-
estimatedCost = tier.label;
|
|
114
|
-
reasoning.push(`Budget $${budget}/mo → ${tier.instanceType} (${tier.label})`);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
const scaleMap = { small: 't3.micro', medium: 't3.small', large: 't3.medium' };
|
|
118
|
-
const costMap = { small: '~$8/mo', medium: '~$17/mo', large: '~$34/mo' };
|
|
119
|
-
instanceType = scaleMap[scale];
|
|
120
|
-
estimatedCost = costMap[scale];
|
|
121
|
-
reasoning.push(`${scale} scale → ${instanceType} (${estimatedCost})`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
estimatedCost = deploy === 'railway' ? 'Free tier available' :
|
|
126
|
-
deploy === 'vercel' ? 'Free tier available' :
|
|
127
|
-
deploy === 'cloudflare' ? 'Free tier available' :
|
|
128
|
-
deploy === 'static' ? 'Minimal (~$1/mo S3)' : 'Varies';
|
|
129
|
-
}
|
|
130
|
-
// Extract hostname if mentioned
|
|
131
|
-
const hostnameMatch = prose.match(/(?:domain|hostname|url)[\s:]*([a-z0-9.-]+\.[a-z]{2,})/i);
|
|
132
|
-
const hostname = hostnameMatch ? hostnameMatch[1] : '';
|
|
133
|
-
if (hostname)
|
|
134
|
-
reasoning.push(`Hostname detected: ${hostname}`);
|
|
135
|
-
// Build resilience config — defaults based on deploy target + detected features
|
|
136
|
-
const isVps = deploy === 'vps';
|
|
137
|
-
const isPlatform = ['vercel', 'railway', 'cloudflare'].includes(deploy);
|
|
138
|
-
const resilience = {
|
|
139
|
-
multiEnv: features.has('multiEnv') || scale !== 'small',
|
|
140
|
-
previewDeploys: features.has('previewDeploys') || (isPlatform && scale !== 'small'),
|
|
141
|
-
rollback: features.has('rollback') || isPlatform,
|
|
142
|
-
migrations: features.has('migrations') ? 'auto' : (isVps ? 'manual' : 'no'),
|
|
143
|
-
backups: features.has('backups') ? 'daily' : (isVps && scale !== 'small' ? 'weekly' : 'no'),
|
|
144
|
-
healthCheck: features.has('healthCheck') || isVps || scale !== 'small',
|
|
145
|
-
gracefulShutdown: features.has('gracefulShutdown') || isVps,
|
|
146
|
-
errorBoundaries: features.has('errorBoundaries'),
|
|
147
|
-
rateLimiting: features.has('rateLimiting') || scale === 'large',
|
|
148
|
-
deadLetterQueue: features.has('deadLetterQueue'),
|
|
149
|
-
};
|
|
150
|
-
return {
|
|
151
|
-
deploy,
|
|
152
|
-
instanceType,
|
|
153
|
-
hostname,
|
|
154
|
-
estimatedMonthlyCost: estimatedCost,
|
|
155
|
-
resilience,
|
|
156
|
-
reasoning,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
/** Sanitize a string for safe YAML double-quoted interpolation. */
|
|
160
|
-
function yamlSafe(value) {
|
|
161
|
-
return value.replace(/[\\"]/g, '');
|
|
162
|
-
}
|
|
163
|
-
/** Convert a DeployConfig to YAML frontmatter fragment. */
|
|
164
|
-
export function toFrontmatter(config) {
|
|
165
|
-
const lines = [
|
|
166
|
-
`deploy: "${yamlSafe(config.deploy)}"`,
|
|
167
|
-
];
|
|
168
|
-
if (config.instanceType) {
|
|
169
|
-
lines.push(`instance_type: "${yamlSafe(config.instanceType)}"`);
|
|
170
|
-
}
|
|
171
|
-
if (config.hostname) {
|
|
172
|
-
lines.push(`hostname: "${yamlSafe(config.hostname.toLowerCase())}"`);
|
|
173
|
-
}
|
|
174
|
-
lines.push('resilience:');
|
|
175
|
-
lines.push(` multi-env: ${config.resilience.multiEnv ? 'yes' : 'no'}`);
|
|
176
|
-
lines.push(` preview-deploys: ${config.resilience.previewDeploys ? 'yes' : 'no'}`);
|
|
177
|
-
lines.push(` rollback: ${config.resilience.rollback ? 'yes' : 'no'}`);
|
|
178
|
-
lines.push(` migrations: "${yamlSafe(config.resilience.migrations)}"`);
|
|
179
|
-
lines.push(` backups: "${yamlSafe(config.resilience.backups)}"`);
|
|
180
|
-
lines.push(` health-check: ${config.resilience.healthCheck ? 'yes' : 'no'}`);
|
|
181
|
-
lines.push(` graceful-shutdown: ${config.resilience.gracefulShutdown ? 'yes' : 'no'}`);
|
|
182
|
-
lines.push(` error-boundaries: ${config.resilience.errorBoundaries ? 'yes' : 'no'}`);
|
|
183
|
-
lines.push(` rate-limiting: ${config.resilience.rateLimiting ? 'yes' : 'no'}`);
|
|
184
|
-
lines.push(` dead-letter-queue: ${config.resilience.deadLetterQueue ? 'yes' : 'no'}`);
|
|
185
|
-
return lines.join('\n');
|
|
186
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paris's Route Optimizer — ROI-weighted campaign sequencing (v12.3).
|
|
3
|
-
*
|
|
4
|
-
* Given multiple possible campaigns (from the proposal generator), Paris
|
|
5
|
-
* computes the optimal execution order based on estimated ROI, dependencies,
|
|
6
|
-
* risk, and urgency.
|
|
7
|
-
*
|
|
8
|
-
* PRD Reference: ROADMAP v12.3, DEEP_CURRENT.md Paris's role
|
|
9
|
-
*/
|
|
10
|
-
import type { CampaignProposal } from './campaign-proposer.js';
|
|
11
|
-
import type { SituationModel } from './deep-current.js';
|
|
12
|
-
interface RouteScore {
|
|
13
|
-
proposal: CampaignProposal;
|
|
14
|
-
roiScore: number;
|
|
15
|
-
urgencyScore: number;
|
|
16
|
-
riskScore: number;
|
|
17
|
-
totalScore: number;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Score and rank campaign proposals by optimal execution order.
|
|
21
|
-
* Returns proposals sorted by total score (highest first = execute first).
|
|
22
|
-
*/
|
|
23
|
-
export declare function optimizeRoute(proposals: CampaignProposal[], model: SituationModel): RouteScore[];
|
|
24
|
-
/**
|
|
25
|
-
* Pick the single best campaign to execute next.
|
|
26
|
-
*/
|
|
27
|
-
export declare function pickBestCampaign(proposals: CampaignProposal[], model: SituationModel): CampaignProposal | null;
|
|
28
|
-
export type { RouteScore };
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paris's Route Optimizer — ROI-weighted campaign sequencing (v12.3).
|
|
3
|
-
*
|
|
4
|
-
* Given multiple possible campaigns (from the proposal generator), Paris
|
|
5
|
-
* computes the optimal execution order based on estimated ROI, dependencies,
|
|
6
|
-
* risk, and urgency.
|
|
7
|
-
*
|
|
8
|
-
* PRD Reference: ROADMAP v12.3, DEEP_CURRENT.md Paris's role
|
|
9
|
-
*/
|
|
10
|
-
// ── Scoring Weights ───────────────────────────────────
|
|
11
|
-
const WEIGHTS = {
|
|
12
|
-
roi: 0.40,
|
|
13
|
-
urgency: 0.35,
|
|
14
|
-
risk: 0.25, // Inverted — lower risk gets higher score
|
|
15
|
-
};
|
|
16
|
-
// ── ROI Estimation ────────────────────────────────────
|
|
17
|
-
function estimateRoi(proposal, model) {
|
|
18
|
-
// ROI = (dimension improvement potential) / (estimated effort)
|
|
19
|
-
const currentScore = proposal.dimensionScore;
|
|
20
|
-
const potentialGain = Math.min(30, 100 - currentScore); // Cap at 30-point improvement
|
|
21
|
-
const effort = proposal.estimatedSessions;
|
|
22
|
-
// Higher gain per session = higher ROI
|
|
23
|
-
const roiRatio = potentialGain / Math.max(effort, 1);
|
|
24
|
-
return Math.min(100, Math.round(roiRatio * 10)); // Scale to 0-100
|
|
25
|
-
}
|
|
26
|
-
// ── Urgency Scoring ───────────────────────────────────
|
|
27
|
-
function scoreUrgency(proposal, model) {
|
|
28
|
-
const dim = proposal.dimension;
|
|
29
|
-
const score = proposal.dimensionScore;
|
|
30
|
-
// Critical defects are always urgent
|
|
31
|
-
if (dim === 'quality' && score < 30)
|
|
32
|
-
return 100;
|
|
33
|
-
// Security issues are urgent
|
|
34
|
-
if (dim === 'performance' && model.lastSiteScan && !model.lastSiteScan.security.https)
|
|
35
|
-
return 90;
|
|
36
|
-
// Low scores are more urgent
|
|
37
|
-
if (score < 20)
|
|
38
|
-
return 80;
|
|
39
|
-
if (score < 40)
|
|
40
|
-
return 60;
|
|
41
|
-
if (score < 60)
|
|
42
|
-
return 40;
|
|
43
|
-
// Revenue is urgent for OPERATING projects
|
|
44
|
-
if (dim === 'revenuePotential' && model.projectState === 'OPERATING')
|
|
45
|
-
return 70;
|
|
46
|
-
return 20; // Low urgency by default
|
|
47
|
-
}
|
|
48
|
-
// ── Risk Scoring ──────────────────────────────────────
|
|
49
|
-
function scoreRisk(proposal) {
|
|
50
|
-
// Revenue/payment campaigns are higher risk (real money)
|
|
51
|
-
if (proposal.dimension === 'revenuePotential')
|
|
52
|
-
return 70;
|
|
53
|
-
// Feature campaigns have moderate risk (new code)
|
|
54
|
-
if (proposal.dimension === 'featureCompleteness')
|
|
55
|
-
return 50;
|
|
56
|
-
// Quality and performance campaigns are low risk
|
|
57
|
-
if (proposal.dimension === 'quality')
|
|
58
|
-
return 20;
|
|
59
|
-
if (proposal.dimension === 'performance')
|
|
60
|
-
return 25;
|
|
61
|
-
// Growth foundation is low risk (additive, no existing code modified)
|
|
62
|
-
if (proposal.dimension === 'growthReadiness')
|
|
63
|
-
return 30;
|
|
64
|
-
return 40;
|
|
65
|
-
}
|
|
66
|
-
// ── Route Optimization ────────────────────────────────
|
|
67
|
-
/**
|
|
68
|
-
* Score and rank campaign proposals by optimal execution order.
|
|
69
|
-
* Returns proposals sorted by total score (highest first = execute first).
|
|
70
|
-
*/
|
|
71
|
-
export function optimizeRoute(proposals, model) {
|
|
72
|
-
const scored = proposals.map(proposal => {
|
|
73
|
-
const roiScore = estimateRoi(proposal, model);
|
|
74
|
-
const urgencyScore = scoreUrgency(proposal, model);
|
|
75
|
-
const riskScore = 100 - scoreRisk(proposal); // Invert: low risk = high score
|
|
76
|
-
const totalScore = Math.round(roiScore * WEIGHTS.roi +
|
|
77
|
-
urgencyScore * WEIGHTS.urgency +
|
|
78
|
-
riskScore * WEIGHTS.risk);
|
|
79
|
-
return { proposal, roiScore, urgencyScore, riskScore, totalScore };
|
|
80
|
-
});
|
|
81
|
-
// Sort by total score descending (best first)
|
|
82
|
-
scored.sort((a, b) => b.totalScore - a.totalScore);
|
|
83
|
-
return scored;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Pick the single best campaign to execute next.
|
|
87
|
-
*/
|
|
88
|
-
export function pickBestCampaign(proposals, model) {
|
|
89
|
-
if (proposals.length === 0)
|
|
90
|
-
return null;
|
|
91
|
-
const ranked = optimizeRoute(proposals, model);
|
|
92
|
-
return ranked[0].proposal;
|
|
93
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Service Install — launchd/systemd/Task Scheduler integration (§9.18, §9.19.2).
|
|
3
|
-
*
|
|
4
|
-
* Creates system services for:
|
|
5
|
-
* 1. Heartbeat daemon (com.voidforge.heartbeat)
|
|
6
|
-
* 2. Wizard server (com.voidforge.server) — persistent when Cultivation is installed
|
|
7
|
-
*
|
|
8
|
-
* PRD Reference: §9.18 (macOS LaunchAgent), §9.19.2 (two services)
|
|
9
|
-
*/
|
|
10
|
-
export declare function installHeartbeatService(): Promise<{
|
|
11
|
-
method: string;
|
|
12
|
-
path: string;
|
|
13
|
-
}>;
|
|
14
|
-
export declare function installServerService(port?: number): Promise<{
|
|
15
|
-
method: string;
|
|
16
|
-
path: string;
|
|
17
|
-
}>;
|
|
18
|
-
export declare function uninstallServices(): Promise<void>;
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Service Install — launchd/systemd/Task Scheduler integration (§9.18, §9.19.2).
|
|
3
|
-
*
|
|
4
|
-
* Creates system services for:
|
|
5
|
-
* 1. Heartbeat daemon (com.voidforge.heartbeat)
|
|
6
|
-
* 2. Wizard server (com.voidforge.server) — persistent when Cultivation is installed
|
|
7
|
-
*
|
|
8
|
-
* PRD Reference: §9.18 (macOS LaunchAgent), §9.19.2 (two services)
|
|
9
|
-
*/
|
|
10
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { homedir, platform } from 'node:os';
|
|
13
|
-
import { execSync } from 'node:child_process';
|
|
14
|
-
const VOIDFORGE_DIR = join(homedir(), '.voidforge');
|
|
15
|
-
// ── macOS LaunchAgent ─────────────────────────────────
|
|
16
|
-
function heartbeatPlist() {
|
|
17
|
-
const nodePath = process.execPath;
|
|
18
|
-
// Assuming the heartbeat entry point is at wizard/lib/heartbeat.js
|
|
19
|
-
// In production, this would be the installed package path
|
|
20
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
21
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
22
|
-
<plist version="1.0">
|
|
23
|
-
<dict>
|
|
24
|
-
<key>Label</key>
|
|
25
|
-
<string>com.voidforge.heartbeat</string>
|
|
26
|
-
<key>ProgramArguments</key>
|
|
27
|
-
<array>
|
|
28
|
-
<string>${nodePath}</string>
|
|
29
|
-
<string>${join(VOIDFORGE_DIR, 'heartbeat-entry.js')}</string>
|
|
30
|
-
</array>
|
|
31
|
-
<key>KeepAlive</key>
|
|
32
|
-
<true/>
|
|
33
|
-
<key>RunAtLoad</key>
|
|
34
|
-
<true/>
|
|
35
|
-
<key>ProcessType</key>
|
|
36
|
-
<string>Background</string>
|
|
37
|
-
<key>ThrottleInterval</key>
|
|
38
|
-
<integer>10</integer>
|
|
39
|
-
<key>StandardOutPath</key>
|
|
40
|
-
<string>${join(VOIDFORGE_DIR, 'heartbeat-launchd.log')}</string>
|
|
41
|
-
<key>StandardErrorPath</key>
|
|
42
|
-
<string>${join(VOIDFORGE_DIR, 'heartbeat-launchd.log')}</string>
|
|
43
|
-
</dict>
|
|
44
|
-
</plist>`;
|
|
45
|
-
}
|
|
46
|
-
function serverPlist(port = 3141) {
|
|
47
|
-
const nodePath = process.execPath;
|
|
48
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
49
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
50
|
-
<plist version="1.0">
|
|
51
|
-
<dict>
|
|
52
|
-
<key>Label</key>
|
|
53
|
-
<string>com.voidforge.server</string>
|
|
54
|
-
<key>ProgramArguments</key>
|
|
55
|
-
<array>
|
|
56
|
-
<string>${nodePath}</string>
|
|
57
|
-
<string>${join(VOIDFORGE_DIR, 'server-entry.js')}</string>
|
|
58
|
-
</array>
|
|
59
|
-
<key>KeepAlive</key>
|
|
60
|
-
<true/>
|
|
61
|
-
<key>RunAtLoad</key>
|
|
62
|
-
<true/>
|
|
63
|
-
<key>ProcessType</key>
|
|
64
|
-
<string>Background</string>
|
|
65
|
-
<key>ThrottleInterval</key>
|
|
66
|
-
<integer>10</integer>
|
|
67
|
-
<key>EnvironmentVariables</key>
|
|
68
|
-
<dict>
|
|
69
|
-
<key>VOIDFORGE_PORT</key>
|
|
70
|
-
<string>${port}</string>
|
|
71
|
-
</dict>
|
|
72
|
-
<key>StandardOutPath</key>
|
|
73
|
-
<string>${join(VOIDFORGE_DIR, 'server-launchd.log')}</string>
|
|
74
|
-
<key>StandardErrorPath</key>
|
|
75
|
-
<string>${join(VOIDFORGE_DIR, 'server-launchd.log')}</string>
|
|
76
|
-
</dict>
|
|
77
|
-
</plist>`;
|
|
78
|
-
}
|
|
79
|
-
// ── Linux systemd ─────────────────────────────────────
|
|
80
|
-
function heartbeatSystemdUnit() {
|
|
81
|
-
return `[Unit]
|
|
82
|
-
Description=VoidForge Heartbeat Daemon
|
|
83
|
-
After=network.target
|
|
84
|
-
|
|
85
|
-
[Service]
|
|
86
|
-
Type=simple
|
|
87
|
-
ExecStart=${process.execPath} ${join(VOIDFORGE_DIR, 'heartbeat-entry.js')}
|
|
88
|
-
Restart=always
|
|
89
|
-
RestartSec=10
|
|
90
|
-
StandardOutput=append:${join(VOIDFORGE_DIR, 'heartbeat-systemd.log')}
|
|
91
|
-
StandardError=append:${join(VOIDFORGE_DIR, 'heartbeat-systemd.log')}
|
|
92
|
-
|
|
93
|
-
[Install]
|
|
94
|
-
WantedBy=default.target`;
|
|
95
|
-
}
|
|
96
|
-
function serverSystemdUnit(port = 3141) {
|
|
97
|
-
return `[Unit]
|
|
98
|
-
Description=VoidForge Server (Danger Room + Cultivation)
|
|
99
|
-
After=network.target
|
|
100
|
-
|
|
101
|
-
[Service]
|
|
102
|
-
Type=simple
|
|
103
|
-
ExecStart=${process.execPath} ${join(VOIDFORGE_DIR, 'server-entry.js')}
|
|
104
|
-
Environment=VOIDFORGE_PORT=${port}
|
|
105
|
-
Restart=always
|
|
106
|
-
RestartSec=10
|
|
107
|
-
StandardOutput=append:${join(VOIDFORGE_DIR, 'server-systemd.log')}
|
|
108
|
-
StandardError=append:${join(VOIDFORGE_DIR, 'server-systemd.log')}
|
|
109
|
-
|
|
110
|
-
[Install]
|
|
111
|
-
WantedBy=default.target`;
|
|
112
|
-
}
|
|
113
|
-
// ── Install Functions ─────────────────────────────────
|
|
114
|
-
export async function installHeartbeatService() {
|
|
115
|
-
if (platform() === 'darwin') {
|
|
116
|
-
const plistDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
117
|
-
const plistPath = join(plistDir, 'com.voidforge.heartbeat.plist');
|
|
118
|
-
await mkdir(plistDir, { recursive: true });
|
|
119
|
-
await writeFile(plistPath, heartbeatPlist());
|
|
120
|
-
execSync(`launchctl load "${plistPath}" 2>/dev/null || true`);
|
|
121
|
-
return { method: 'launchd', path: plistPath };
|
|
122
|
-
}
|
|
123
|
-
if (platform() === 'linux') {
|
|
124
|
-
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
125
|
-
const unitPath = join(unitDir, 'voidforge-heartbeat.service');
|
|
126
|
-
await mkdir(unitDir, { recursive: true });
|
|
127
|
-
await writeFile(unitPath, heartbeatSystemdUnit());
|
|
128
|
-
execSync('systemctl --user daemon-reload 2>/dev/null || true');
|
|
129
|
-
execSync('systemctl --user enable voidforge-heartbeat 2>/dev/null || true');
|
|
130
|
-
return { method: 'systemd', path: unitPath };
|
|
131
|
-
}
|
|
132
|
-
// Windows: Task Scheduler (recommend WSL2 for full support per §9.17)
|
|
133
|
-
return { method: 'manual', path: 'Windows: use WSL2 or run `voidforge heartbeat start --daemon` manually' };
|
|
134
|
-
}
|
|
135
|
-
export async function installServerService(port = 3141) {
|
|
136
|
-
if (platform() === 'darwin') {
|
|
137
|
-
const plistDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
138
|
-
const plistPath = join(plistDir, 'com.voidforge.server.plist');
|
|
139
|
-
await mkdir(plistDir, { recursive: true });
|
|
140
|
-
await writeFile(plistPath, serverPlist(port));
|
|
141
|
-
execSync(`launchctl load "${plistPath}" 2>/dev/null || true`);
|
|
142
|
-
return { method: 'launchd', path: plistPath };
|
|
143
|
-
}
|
|
144
|
-
if (platform() === 'linux') {
|
|
145
|
-
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
146
|
-
const unitPath = join(unitDir, 'voidforge-server.service');
|
|
147
|
-
await mkdir(unitDir, { recursive: true });
|
|
148
|
-
await writeFile(unitPath, serverSystemdUnit(port));
|
|
149
|
-
execSync('systemctl --user daemon-reload 2>/dev/null || true');
|
|
150
|
-
execSync('systemctl --user enable voidforge-server 2>/dev/null || true');
|
|
151
|
-
return { method: 'systemd', path: unitPath };
|
|
152
|
-
}
|
|
153
|
-
return { method: 'manual', path: 'Windows: use WSL2 or run the wizard server manually' };
|
|
154
|
-
}
|
|
155
|
-
export async function uninstallServices() {
|
|
156
|
-
if (platform() === 'darwin') {
|
|
157
|
-
const agents = ['com.voidforge.heartbeat', 'com.voidforge.server'];
|
|
158
|
-
for (const label of agents) {
|
|
159
|
-
const path = join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
160
|
-
try {
|
|
161
|
-
execSync(`launchctl unload "${path}" 2>/dev/null`);
|
|
162
|
-
const { unlink } = await import('node:fs/promises');
|
|
163
|
-
await unlink(path);
|
|
164
|
-
}
|
|
165
|
-
catch { /* not installed */ }
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (platform() === 'linux') {
|
|
169
|
-
const units = ['voidforge-heartbeat', 'voidforge-server'];
|
|
170
|
-
for (const unit of units) {
|
|
171
|
-
try {
|
|
172
|
-
execSync(`systemctl --user stop ${unit} 2>/dev/null`);
|
|
173
|
-
execSync(`systemctl --user disable ${unit} 2>/dev/null`);
|
|
174
|
-
const path = join(homedir(), '.config', 'systemd', 'user', `${unit}.service`);
|
|
175
|
-
const { unlink } = await import('node:fs/promises');
|
|
176
|
-
await unlink(path);
|
|
177
|
-
}
|
|
178
|
-
catch { /* not installed */ }
|
|
179
|
-
}
|
|
180
|
-
execSync('systemctl --user daemon-reload 2>/dev/null || true');
|
|
181
|
-
}
|
|
182
|
-
}
|