voidforge-build 23.11.0 → 23.11.1
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 +21 -0
- package/dist/VERSION.md +2 -1
- package/dist/docs/methods/RELEASE_MANAGER.md +26 -0
- package/dist/scripts/voidforge.js +0 -0
- 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,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Desktop Notifications — macOS/Linux native notifications for daemon events.
|
|
3
|
-
*
|
|
4
|
-
* Uses osascript on macOS and notify-send on Linux. No dependencies.
|
|
5
|
-
* Notifications are non-blocking and failure-tolerant (notification failure
|
|
6
|
-
* should never crash the daemon).
|
|
7
|
-
*
|
|
8
|
-
* PRD Reference: §9.7 (Danger Room shows warning), v11.3 deliverables
|
|
9
|
-
*/
|
|
10
|
-
import { execFileSync } from 'node:child_process';
|
|
11
|
-
import { platform } from 'node:os';
|
|
12
|
-
/**
|
|
13
|
-
* Send a desktop notification. Fails silently — never throws.
|
|
14
|
-
*/
|
|
15
|
-
export function notify(opts) {
|
|
16
|
-
try {
|
|
17
|
-
if (platform() === 'darwin') {
|
|
18
|
-
notifyMacOS(opts);
|
|
19
|
-
}
|
|
20
|
-
else if (platform() === 'linux') {
|
|
21
|
-
notifyLinux(opts);
|
|
22
|
-
}
|
|
23
|
-
// Windows: notifications deferred — WSL2 path recommended
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
// Notification failure is never fatal
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function notifyMacOS(opts) {
|
|
30
|
-
// SEC-005: Use execFileSync with args array to prevent shell injection
|
|
31
|
-
const sound = opts.sound !== false ? ' sound name "Submarine"' : '';
|
|
32
|
-
const script = `display notification "${sanitize(opts.message)}" with title "VoidForge"${sound} subtitle "${sanitize(opts.title)}"`;
|
|
33
|
-
try {
|
|
34
|
-
execFileSync('osascript', ['-e', script], { timeout: 5000, stdio: 'ignore' });
|
|
35
|
-
}
|
|
36
|
-
catch { /* notification failure is never fatal */ }
|
|
37
|
-
}
|
|
38
|
-
function notifyLinux(opts) {
|
|
39
|
-
// SEC-006: Use execFileSync with args array, validate urgency enum
|
|
40
|
-
const validUrgencies = ['low', 'normal', 'critical'];
|
|
41
|
-
const urgency = validUrgencies.includes(opts.urgency || '') ? opts.urgency : 'normal';
|
|
42
|
-
try {
|
|
43
|
-
execFileSync('notify-send', ['-u', urgency, '-a', 'VoidForge', sanitize(opts.title), sanitize(opts.message)], { timeout: 5000, stdio: 'ignore' });
|
|
44
|
-
}
|
|
45
|
-
catch { /* notification failure is never fatal */ }
|
|
46
|
-
}
|
|
47
|
-
/** Strip characters that could be dangerous in shell/AppleScript contexts */
|
|
48
|
-
function sanitize(input) {
|
|
49
|
-
return input.replace(/[`$\\"\n\r\0]/g, '').slice(0, 200);
|
|
50
|
-
}
|
|
51
|
-
// ── Daemon Event Notifications ────────────────────────
|
|
52
|
-
// Pre-built notifications for common daemon events (§9.20.7 agent voice)
|
|
53
|
-
export function notifySpendSpike(platform, amount) {
|
|
54
|
-
notify({
|
|
55
|
-
title: `Spend Spike — ${platform}`,
|
|
56
|
-
message: `Wax reports: ${platform} spend is ${amount} above average this hour.`,
|
|
57
|
-
urgency: 'critical',
|
|
58
|
-
sound: true,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
export function notifyCampaignKilled(name, reason) {
|
|
62
|
-
notify({
|
|
63
|
-
title: 'Campaign Paused',
|
|
64
|
-
message: `Wax pulled the trigger on "${name}" — ${reason}.`,
|
|
65
|
-
urgency: 'normal',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
export function notifyTokenExpiring(platform, hoursLeft) {
|
|
69
|
-
notify({
|
|
70
|
-
title: `Token Expiring — ${platform}`,
|
|
71
|
-
message: `Breeze warns: ${platform} token expires in ${hoursLeft} hours. Refresh needed.`,
|
|
72
|
-
urgency: hoursLeft < 2 ? 'critical' : 'normal',
|
|
73
|
-
sound: hoursLeft < 2,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
export function notifyReconciliationDiscrepancy(platform, amount) {
|
|
77
|
-
notify({
|
|
78
|
-
title: 'Reconciliation Alert',
|
|
79
|
-
message: `Dockson: Numbers don't match on ${platform} — ${amount} discrepancy.`,
|
|
80
|
-
urgency: 'critical',
|
|
81
|
-
sound: true,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
export function notifyVaultExpiring(hoursLeft) {
|
|
85
|
-
notify({
|
|
86
|
-
title: 'Vault Session Expiring',
|
|
87
|
-
message: `Vault session expires in ${hoursLeft} hour(s). Run \`voidforge heartbeat unlock\` to extend.`,
|
|
88
|
-
urgency: 'critical',
|
|
89
|
-
sound: true,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
export function notifyRevenueMilestone(amount) {
|
|
93
|
-
notify({
|
|
94
|
-
title: 'Revenue Milestone!',
|
|
95
|
-
message: `Dockson: ${amount} total revenue. Every coin has a story — this one's a good chapter.`,
|
|
96
|
-
urgency: 'low',
|
|
97
|
-
});
|
|
98
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image generation provider abstraction — Celebrimbor's forge tools.
|
|
3
|
-
* Default: OpenAI (gpt-image-1). Extensible to other providers.
|
|
4
|
-
* Uses the same vault system as other VoidForge credentials.
|
|
5
|
-
*/
|
|
6
|
-
import type { ProvisionEmitter } from './provisioners/types.js';
|
|
7
|
-
export interface ImageGenerationOptions {
|
|
8
|
-
prompt: string;
|
|
9
|
-
width: number;
|
|
10
|
-
height: number;
|
|
11
|
-
model?: string;
|
|
12
|
-
quality?: 'low' | 'medium' | 'high';
|
|
13
|
-
}
|
|
14
|
-
export interface GeneratedAsset {
|
|
15
|
-
name: string;
|
|
16
|
-
filename: string;
|
|
17
|
-
prompt: string;
|
|
18
|
-
size: string;
|
|
19
|
-
generatedAt: string;
|
|
20
|
-
hash: string;
|
|
21
|
-
}
|
|
22
|
-
export interface AssetManifest {
|
|
23
|
-
generated: string;
|
|
24
|
-
model: string;
|
|
25
|
-
style: string;
|
|
26
|
-
assets: GeneratedAsset[];
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Generate an image via OpenAI's API.
|
|
30
|
-
* Returns the raw image bytes as a Buffer.
|
|
31
|
-
*/
|
|
32
|
-
export declare function generateImage(apiKey: string, options: ImageGenerationOptions, emit: ProvisionEmitter): Promise<Buffer | null>;
|
|
33
|
-
/**
|
|
34
|
-
* Validate an OpenAI API key by making a lightweight models list request.
|
|
35
|
-
*/
|
|
36
|
-
export declare function validateOpenAIKey(apiKey: string): Promise<boolean>;
|
|
37
|
-
/**
|
|
38
|
-
* Estimate the cost of generating N images.
|
|
39
|
-
*/
|
|
40
|
-
export declare function estimateImageCost(count: number, model?: string): number;
|
|
41
|
-
/**
|
|
42
|
-
* Read the asset manifest from disk.
|
|
43
|
-
*/
|
|
44
|
-
export declare function readManifest(imagesDir: string): Promise<AssetManifest | null>;
|
|
45
|
-
/**
|
|
46
|
-
* Write the asset manifest to disk.
|
|
47
|
-
*/
|
|
48
|
-
export declare function writeManifest(imagesDir: string, manifest: AssetManifest): Promise<void>;
|
|
49
|
-
/**
|
|
50
|
-
* Save a generated image to disk and update the manifest.
|
|
51
|
-
*/
|
|
52
|
-
export declare function saveGeneratedImage(imagesDir: string, category: string, name: string, imageBuffer: Buffer, prompt: string, size: string, manifest: AssetManifest): Promise<string>;
|
|
53
|
-
/**
|
|
54
|
-
* Check if an asset already exists on disk.
|
|
55
|
-
*/
|
|
56
|
-
export declare function assetExists(imagesDir: string, category: string, name: string): boolean;
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image generation provider abstraction — Celebrimbor's forge tools.
|
|
3
|
-
* Default: OpenAI (gpt-image-1). Extensible to other providers.
|
|
4
|
-
* Uses the same vault system as other VoidForge credentials.
|
|
5
|
-
*/
|
|
6
|
-
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { createHash } from 'node:crypto';
|
|
10
|
-
import { httpsPost, httpsGet, safeJsonParse } from './provisioners/http-client.js';
|
|
11
|
-
// ── OpenAI Provider ──────────────────────────────────────
|
|
12
|
-
const OPENAI_API = 'api.openai.com';
|
|
13
|
-
/**
|
|
14
|
-
* Generate an image via OpenAI's API.
|
|
15
|
-
* Returns the raw image bytes as a Buffer.
|
|
16
|
-
*/
|
|
17
|
-
export async function generateImage(apiKey, options, emit) {
|
|
18
|
-
const model = options.model || 'gpt-image-1';
|
|
19
|
-
const size = `${options.width}x${options.height}`;
|
|
20
|
-
// OpenAI only supports specific sizes — map to nearest and warn
|
|
21
|
-
const validSizes = ['1024x1024', '1792x1024', '1024x1792'];
|
|
22
|
-
const actualSize = validSizes.includes(size) ? size : '1024x1024';
|
|
23
|
-
if (actualSize !== size) {
|
|
24
|
-
emit({ step: 'image-gen', status: 'started', message: `Requested ${size} → using ${actualSize} (API constraint)` });
|
|
25
|
-
}
|
|
26
|
-
const body = JSON.stringify({
|
|
27
|
-
model,
|
|
28
|
-
prompt: options.prompt,
|
|
29
|
-
n: 1,
|
|
30
|
-
size: actualSize,
|
|
31
|
-
quality: options.quality || 'medium',
|
|
32
|
-
response_format: 'b64_json',
|
|
33
|
-
});
|
|
34
|
-
// Retry logic: 3 attempts with exponential backoff (1s, 3s, 9s)
|
|
35
|
-
// DALL-E 3 returns 500 errors on ~15% of requests (field report #1)
|
|
36
|
-
const MAX_RETRIES = 3;
|
|
37
|
-
const BACKOFF_BASE_MS = 1000;
|
|
38
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
39
|
-
try {
|
|
40
|
-
const res = await httpsPost(OPENAI_API, '/v1/images/generations', {
|
|
41
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
42
|
-
'Content-Type': 'application/json',
|
|
43
|
-
}, body, 120_000); // 2 min timeout for image generation
|
|
44
|
-
if (res.status === 500 || res.status === 502 || res.status === 503) {
|
|
45
|
-
// Server error — retry with backoff
|
|
46
|
-
if (attempt < MAX_RETRIES) {
|
|
47
|
-
const delay = BACKOFF_BASE_MS * Math.pow(3, attempt - 1);
|
|
48
|
-
emit({ step: 'image-gen', status: 'started', message: `Server error (${res.status}), retrying in ${delay / 1000}s (attempt ${attempt}/${MAX_RETRIES})` });
|
|
49
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
emit({ step: 'image-gen', status: 'error', message: `Server error (${res.status}) after ${MAX_RETRIES} attempts` });
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
if (res.status !== 200) {
|
|
56
|
-
const errData = safeJsonParse(res.body);
|
|
57
|
-
const errMsg = errData?.error?.message || `API returned ${res.status}`;
|
|
58
|
-
emit({ step: 'image-gen', status: 'error', message: `Generation failed: ${errMsg}` });
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
const data = safeJsonParse(res.body);
|
|
62
|
-
const b64 = data?.data?.[0]?.b64_json;
|
|
63
|
-
if (!b64) {
|
|
64
|
-
emit({ step: 'image-gen', status: 'error', message: 'No image data in API response' });
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
if (attempt > 1) {
|
|
68
|
-
emit({ step: 'image-gen', status: 'done', message: `Succeeded on attempt ${attempt}` });
|
|
69
|
-
}
|
|
70
|
-
return Buffer.from(b64, 'base64');
|
|
71
|
-
}
|
|
72
|
-
catch (err) {
|
|
73
|
-
if (attempt < MAX_RETRIES) {
|
|
74
|
-
const delay = BACKOFF_BASE_MS * Math.pow(3, attempt - 1);
|
|
75
|
-
emit({ step: 'image-gen', status: 'started', message: `Request failed, retrying in ${delay / 1000}s (attempt ${attempt}/${MAX_RETRIES})`, detail: err.message });
|
|
76
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
emit({ step: 'image-gen', status: 'error', message: `Image generation failed after ${MAX_RETRIES} attempts`, detail: err.message });
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Validate an OpenAI API key by making a lightweight models list request.
|
|
87
|
-
*/
|
|
88
|
-
export async function validateOpenAIKey(apiKey) {
|
|
89
|
-
try {
|
|
90
|
-
const res = await httpsGet(OPENAI_API, '/v1/models', {
|
|
91
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
92
|
-
}, 10_000);
|
|
93
|
-
return res.status === 200;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Estimate the cost of generating N images.
|
|
101
|
-
*/
|
|
102
|
-
export function estimateImageCost(count, model = 'gpt-image-1') {
|
|
103
|
-
const costPerImage = {
|
|
104
|
-
'gpt-image-1': 0.04,
|
|
105
|
-
'dall-e-3': 0.08,
|
|
106
|
-
};
|
|
107
|
-
return count * (costPerImage[model] || 0.04);
|
|
108
|
-
}
|
|
109
|
-
// ── Asset Manifest ──────────────────────────────────────
|
|
110
|
-
const MANIFEST_FILENAME = 'manifest.json';
|
|
111
|
-
/**
|
|
112
|
-
* Read the asset manifest from disk.
|
|
113
|
-
*/
|
|
114
|
-
export async function readManifest(imagesDir) {
|
|
115
|
-
const manifestPath = join(imagesDir, MANIFEST_FILENAME);
|
|
116
|
-
try {
|
|
117
|
-
const content = await readFile(manifestPath, 'utf-8');
|
|
118
|
-
return JSON.parse(content);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Write the asset manifest to disk.
|
|
126
|
-
*/
|
|
127
|
-
export async function writeManifest(imagesDir, manifest) {
|
|
128
|
-
await mkdir(imagesDir, { recursive: true });
|
|
129
|
-
await writeFile(join(imagesDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Save a generated image to disk and update the manifest.
|
|
133
|
-
*/
|
|
134
|
-
export async function saveGeneratedImage(imagesDir, category, name, imageBuffer, prompt, size, manifest) {
|
|
135
|
-
const categoryDir = join(imagesDir, category);
|
|
136
|
-
await mkdir(categoryDir, { recursive: true });
|
|
137
|
-
const filename = `${category}/${name}.png`;
|
|
138
|
-
const filepath = join(imagesDir, filename);
|
|
139
|
-
await writeFile(filepath, imageBuffer);
|
|
140
|
-
const hash = createHash('sha256').update(imageBuffer).digest('hex');
|
|
141
|
-
// Remove any existing entry with the same filename (dedup for --regen)
|
|
142
|
-
manifest.assets = manifest.assets.filter(a => a.filename !== filename);
|
|
143
|
-
manifest.assets.push({
|
|
144
|
-
name,
|
|
145
|
-
filename,
|
|
146
|
-
prompt,
|
|
147
|
-
size,
|
|
148
|
-
generatedAt: new Date().toISOString(),
|
|
149
|
-
hash: `sha256:${hash}`,
|
|
150
|
-
});
|
|
151
|
-
await writeManifest(imagesDir, manifest);
|
|
152
|
-
return filepath;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Check if an asset already exists on disk.
|
|
156
|
-
*/
|
|
157
|
-
export function assetExists(imagesDir, category, name) {
|
|
158
|
-
return existsSync(join(imagesDir, category, `${name}.png`));
|
|
159
|
-
}
|
|
@@ -1,30 +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
|
-
export interface DeployConfig {
|
|
10
|
-
deploy: 'vps' | 'vercel' | 'railway' | 'cloudflare' | 'static' | 'docker';
|
|
11
|
-
instanceType: string;
|
|
12
|
-
hostname: string;
|
|
13
|
-
estimatedMonthlyCost: string;
|
|
14
|
-
resilience: {
|
|
15
|
-
multiEnv: boolean;
|
|
16
|
-
previewDeploys: boolean;
|
|
17
|
-
rollback: boolean;
|
|
18
|
-
migrations: 'auto' | 'manual' | 'no';
|
|
19
|
-
backups: 'daily' | 'weekly' | 'no';
|
|
20
|
-
healthCheck: boolean;
|
|
21
|
-
gracefulShutdown: boolean;
|
|
22
|
-
errorBoundaries: boolean;
|
|
23
|
-
rateLimiting: boolean;
|
|
24
|
-
deadLetterQueue: boolean;
|
|
25
|
-
};
|
|
26
|
-
reasoning: string[];
|
|
27
|
-
}
|
|
28
|
-
export declare function resolveDeployConfig(prose: string): DeployConfig | null;
|
|
29
|
-
/** Convert a DeployConfig to YAML frontmatter fragment. */
|
|
30
|
-
export declare function toFrontmatter(config: DeployConfig): string;
|
|
@@ -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>;
|