thevoidforge 21.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/vault-read.d.ts +11 -0
- package/dist/scripts/vault-read.js +89 -0
- package/dist/scripts/voidforge.d.ts +20 -0
- package/dist/scripts/voidforge.js +404 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/wizard/api/auth.d.ts +5 -0
- package/dist/wizard/api/auth.js +133 -0
- package/dist/wizard/api/blueprint.d.ts +45 -0
- package/dist/wizard/api/blueprint.js +184 -0
- package/dist/wizard/api/cloud-providers.d.ts +16 -0
- package/dist/wizard/api/cloud-providers.js +363 -0
- package/dist/wizard/api/credentials.d.ts +1 -0
- package/dist/wizard/api/credentials.js +258 -0
- package/dist/wizard/api/danger-room.d.ts +18 -0
- package/dist/wizard/api/danger-room.js +401 -0
- package/dist/wizard/api/deploy.d.ts +4 -0
- package/dist/wizard/api/deploy.js +164 -0
- package/dist/wizard/api/prd.d.ts +1 -0
- package/dist/wizard/api/prd.js +363 -0
- package/dist/wizard/api/project.d.ts +1 -0
- package/dist/wizard/api/project.js +239 -0
- package/dist/wizard/api/projects.d.ts +6 -0
- package/dist/wizard/api/projects.js +648 -0
- package/dist/wizard/api/provision.d.ts +4 -0
- package/dist/wizard/api/provision.js +535 -0
- package/dist/wizard/api/terminal.d.ts +25 -0
- package/dist/wizard/api/terminal.js +241 -0
- package/dist/wizard/api/users.d.ts +6 -0
- package/dist/wizard/api/users.js +244 -0
- package/dist/wizard/api/war-room.d.ts +14 -0
- package/dist/wizard/api/war-room.js +45 -0
- package/dist/wizard/lib/ad-platform-core.d.ts +6 -0
- package/dist/wizard/lib/ad-platform-core.js +1 -0
- package/dist/wizard/lib/adapters/index.d.ts +52 -0
- package/dist/wizard/lib/adapters/index.js +38 -0
- package/dist/wizard/lib/adapters/sandbox-bank.d.ts +17 -0
- package/dist/wizard/lib/adapters/sandbox-bank.js +77 -0
- package/dist/wizard/lib/adapters/sandbox.d.ts +39 -0
- package/dist/wizard/lib/adapters/sandbox.js +174 -0
- package/dist/wizard/lib/adapters/stripe.d.ts +19 -0
- package/dist/wizard/lib/adapters/stripe.js +143 -0
- package/dist/wizard/lib/adapters/types.d.ts +9 -0
- package/dist/wizard/lib/adapters/types.js +10 -0
- package/dist/wizard/lib/agent-memory.d.ts +36 -0
- package/dist/wizard/lib/agent-memory.js +114 -0
- package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
- package/dist/wizard/lib/anomaly-detection.js +122 -0
- package/dist/wizard/lib/anthropic.d.ts +21 -0
- package/dist/wizard/lib/anthropic.js +105 -0
- package/dist/wizard/lib/asset-scanner.d.ts +23 -0
- package/dist/wizard/lib/asset-scanner.js +107 -0
- package/dist/wizard/lib/audit-log.d.ts +23 -0
- package/dist/wizard/lib/audit-log.js +70 -0
- package/dist/wizard/lib/autonomy-controller.d.ts +76 -0
- package/dist/wizard/lib/autonomy-controller.js +183 -0
- package/dist/wizard/lib/body-parser.d.ts +2 -0
- package/dist/wizard/lib/body-parser.js +36 -0
- package/dist/wizard/lib/build-analytics.d.ts +39 -0
- package/dist/wizard/lib/build-analytics.js +91 -0
- package/dist/wizard/lib/build-step.d.ts +21 -0
- package/dist/wizard/lib/build-step.js +104 -0
- package/dist/wizard/lib/campaign-proposer.d.ts +39 -0
- package/dist/wizard/lib/campaign-proposer.js +180 -0
- package/dist/wizard/lib/campaign-state-machine.d.ts +63 -0
- package/dist/wizard/lib/campaign-state-machine.js +114 -0
- package/dist/wizard/lib/ci-generator.d.ts +14 -0
- package/dist/wizard/lib/ci-generator.js +187 -0
- package/dist/wizard/lib/claude-merge.d.ts +38 -0
- package/dist/wizard/lib/claude-merge.js +115 -0
- package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/erd-gen.js +98 -0
- package/dist/wizard/lib/codegen/integrations.d.ts +18 -0
- package/dist/wizard/lib/codegen/integrations.js +189 -0
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
- package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
- package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
- package/dist/wizard/lib/codegen/prisma-types.js +44 -0
- package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/seed-gen.js +128 -0
- package/dist/wizard/lib/compliance.d.ts +51 -0
- package/dist/wizard/lib/compliance.js +112 -0
- package/dist/wizard/lib/correlation-engine.d.ts +59 -0
- package/dist/wizard/lib/correlation-engine.js +151 -0
- package/dist/wizard/lib/cost-estimator.d.ts +22 -0
- package/dist/wizard/lib/cost-estimator.js +72 -0
- package/dist/wizard/lib/cost-tracker.d.ts +27 -0
- package/dist/wizard/lib/cost-tracker.js +37 -0
- package/dist/wizard/lib/daemon-aggregator.d.ts +71 -0
- package/dist/wizard/lib/daemon-aggregator.js +204 -0
- package/dist/wizard/lib/daemon-core.d.ts +6 -0
- package/dist/wizard/lib/daemon-core.js +5 -0
- package/dist/wizard/lib/dashboard-data.d.ts +132 -0
- package/dist/wizard/lib/dashboard-data.js +336 -0
- package/dist/wizard/lib/dashboard-ws.d.ts +25 -0
- package/dist/wizard/lib/dashboard-ws.js +91 -0
- package/dist/wizard/lib/deep-current.d.ts +77 -0
- package/dist/wizard/lib/deep-current.js +234 -0
- package/dist/wizard/lib/deploy-coordinator.d.ts +40 -0
- package/dist/wizard/lib/deploy-coordinator.js +86 -0
- package/dist/wizard/lib/deploy-log.d.ts +28 -0
- package/dist/wizard/lib/deploy-log.js +52 -0
- package/dist/wizard/lib/desktop-notify.d.ts +27 -0
- package/dist/wizard/lib/desktop-notify.js +98 -0
- package/dist/wizard/lib/dns/cloudflare-dns.d.ts +35 -0
- package/dist/wizard/lib/dns/cloudflare-dns.js +216 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.d.ts +31 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.js +148 -0
- package/dist/wizard/lib/dns/types.d.ts +22 -0
- package/dist/wizard/lib/dns/types.js +4 -0
- package/dist/wizard/lib/document-discovery.d.ts +33 -0
- package/dist/wizard/lib/document-discovery.js +145 -0
- package/dist/wizard/lib/env-validator.d.ts +14 -0
- package/dist/wizard/lib/env-validator.js +205 -0
- package/dist/wizard/lib/env-writer.d.ts +13 -0
- package/dist/wizard/lib/env-writer.js +26 -0
- package/dist/wizard/lib/exec.d.ts +30 -0
- package/dist/wizard/lib/exec.js +52 -0
- package/dist/wizard/lib/experiment.d.ts +70 -0
- package/dist/wizard/lib/experiment.js +169 -0
- package/dist/wizard/lib/extensions.d.ts +20 -0
- package/dist/wizard/lib/extensions.js +183 -0
- package/dist/wizard/lib/financial/adapter-factory.d.ts +47 -0
- package/dist/wizard/lib/financial/adapter-factory.js +225 -0
- package/dist/wizard/lib/financial/billing/base.d.ts +6 -0
- package/dist/wizard/lib/financial/billing/base.js +1 -0
- package/dist/wizard/lib/financial/billing/google-billing.d.ts +56 -0
- package/dist/wizard/lib/financial/billing/google-billing.js +298 -0
- package/dist/wizard/lib/financial/billing/meta-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/meta-billing.js +243 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.js +260 -0
- package/dist/wizard/lib/financial/campaign/base.d.ts +13 -0
- package/dist/wizard/lib/financial/campaign/base.js +1 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.d.ts +42 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.js +388 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.d.ts +41 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.js +311 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.d.ts +45 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.js +261 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.d.ts +40 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.js +350 -0
- package/dist/wizard/lib/financial/funding-auto.d.ts +44 -0
- package/dist/wizard/lib/financial/funding-auto.js +52 -0
- package/dist/wizard/lib/financial/funding-policy.d.ts +60 -0
- package/dist/wizard/lib/financial/funding-policy.js +179 -0
- package/dist/wizard/lib/financial/platform-planner.d.ts +47 -0
- package/dist/wizard/lib/financial/platform-planner.js +134 -0
- package/dist/wizard/lib/financial/reconciliation-engine.d.ts +78 -0
- package/dist/wizard/lib/financial/reconciliation-engine.js +193 -0
- package/dist/wizard/lib/financial/registry.d.ts +22 -0
- package/dist/wizard/lib/financial/registry.js +26 -0
- package/dist/wizard/lib/financial/reporting.d.ts +96 -0
- package/dist/wizard/lib/financial/reporting.js +198 -0
- package/dist/wizard/lib/financial/stablecoin/base.d.ts +6 -0
- package/dist/wizard/lib/financial/stablecoin/base.js +1 -0
- package/dist/wizard/lib/financial/stablecoin/circle.d.ts +54 -0
- package/dist/wizard/lib/financial/stablecoin/circle.js +367 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.d.ts +24 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.js +171 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.d.ts +47 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.js +202 -0
- package/dist/wizard/lib/financial/treasury-planner.d.ts +52 -0
- package/dist/wizard/lib/financial/treasury-planner.js +128 -0
- package/dist/wizard/lib/financial-core.d.ts +6 -0
- package/dist/wizard/lib/financial-core.js +5 -0
- package/dist/wizard/lib/financial-vault.d.ts +34 -0
- package/dist/wizard/lib/financial-vault.js +199 -0
- package/dist/wizard/lib/frontmatter.d.ts +30 -0
- package/dist/wizard/lib/frontmatter.js +96 -0
- package/dist/wizard/lib/gap-analysis.d.ts +37 -0
- package/dist/wizard/lib/gap-analysis.js +218 -0
- package/dist/wizard/lib/github.d.ts +22 -0
- package/dist/wizard/lib/github.js +261 -0
- package/dist/wizard/lib/headless-deploy.d.ts +14 -0
- package/dist/wizard/lib/headless-deploy.js +452 -0
- package/dist/wizard/lib/health-monitor.d.ts +15 -0
- package/dist/wizard/lib/health-monitor.js +91 -0
- package/dist/wizard/lib/health-poller.d.ts +9 -0
- package/dist/wizard/lib/health-poller.js +123 -0
- package/dist/wizard/lib/heartbeat.d.ts +15 -0
- package/dist/wizard/lib/heartbeat.js +827 -0
- package/dist/wizard/lib/http-helpers.d.ts +9 -0
- package/dist/wizard/lib/http-helpers.js +24 -0
- package/dist/wizard/lib/image-gen.d.ts +56 -0
- package/dist/wizard/lib/image-gen.js +159 -0
- package/dist/wizard/lib/instance-sizing.d.ts +26 -0
- package/dist/wizard/lib/instance-sizing.js +51 -0
- package/dist/wizard/lib/kongo/analytics.d.ts +29 -0
- package/dist/wizard/lib/kongo/analytics.js +179 -0
- package/dist/wizard/lib/kongo/campaigns.d.ts +52 -0
- package/dist/wizard/lib/kongo/campaigns.js +91 -0
- package/dist/wizard/lib/kongo/client.d.ts +58 -0
- package/dist/wizard/lib/kongo/client.js +221 -0
- package/dist/wizard/lib/kongo/jobs.d.ts +57 -0
- package/dist/wizard/lib/kongo/jobs.js +122 -0
- package/dist/wizard/lib/kongo/pages.d.ts +60 -0
- package/dist/wizard/lib/kongo/pages.js +150 -0
- package/dist/wizard/lib/kongo/provisioner.d.ts +64 -0
- package/dist/wizard/lib/kongo/provisioner.js +116 -0
- package/dist/wizard/lib/kongo/seed.d.ts +49 -0
- package/dist/wizard/lib/kongo/seed.js +237 -0
- package/dist/wizard/lib/kongo/types.d.ts +323 -0
- package/dist/wizard/lib/kongo/types.js +11 -0
- package/dist/wizard/lib/kongo/variants.d.ts +57 -0
- package/dist/wizard/lib/kongo/variants.js +88 -0
- package/dist/wizard/lib/kongo/webhooks.d.ts +41 -0
- package/dist/wizard/lib/kongo/webhooks.js +112 -0
- package/dist/wizard/lib/marker.d.ts +28 -0
- package/dist/wizard/lib/marker.js +79 -0
- package/dist/wizard/lib/migrator.d.ts +35 -0
- package/dist/wizard/lib/migrator.js +190 -0
- package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
- package/dist/wizard/lib/natural-language-deploy.js +186 -0
- package/dist/wizard/lib/network.d.ts +22 -0
- package/dist/wizard/lib/network.js +72 -0
- package/dist/wizard/lib/oauth-core.d.ts +6 -0
- package/dist/wizard/lib/oauth-core.js +5 -0
- package/dist/wizard/lib/open-browser.d.ts +1 -0
- package/dist/wizard/lib/open-browser.js +26 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.d.ts +209 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.js +269 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.d.ts +200 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.js +212 -0
- package/dist/wizard/lib/patterns/daemon-process.d.ts +88 -0
- package/dist/wizard/lib/patterns/daemon-process.js +271 -0
- package/dist/wizard/lib/patterns/financial-transaction.d.ts +161 -0
- package/dist/wizard/lib/patterns/financial-transaction.js +132 -0
- package/dist/wizard/lib/patterns/funding-plan.d.ts +136 -0
- package/dist/wizard/lib/patterns/funding-plan.js +200 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +94 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +139 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.d.ts +67 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.js +216 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.d.ts +96 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.js +182 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.d.ts +218 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.js +264 -0
- package/dist/wizard/lib/prd-validator.d.ts +39 -0
- package/dist/wizard/lib/prd-validator.js +137 -0
- package/dist/wizard/lib/project-init.d.ts +24 -0
- package/dist/wizard/lib/project-init.js +193 -0
- package/dist/wizard/lib/project-registry.d.ts +86 -0
- package/dist/wizard/lib/project-registry.js +359 -0
- package/dist/wizard/lib/provision-manifest.d.ts +44 -0
- package/dist/wizard/lib/provision-manifest.js +164 -0
- package/dist/wizard/lib/provisioner-registry.d.ts +15 -0
- package/dist/wizard/lib/provisioner-registry.js +34 -0
- package/dist/wizard/lib/provisioners/aws-vps.d.ts +6 -0
- package/dist/wizard/lib/provisioners/aws-vps.js +643 -0
- package/dist/wizard/lib/provisioners/cloudflare.d.ts +6 -0
- package/dist/wizard/lib/provisioners/cloudflare.js +300 -0
- package/dist/wizard/lib/provisioners/docker.d.ts +6 -0
- package/dist/wizard/lib/provisioners/docker.js +75 -0
- package/dist/wizard/lib/provisioners/http-client.d.ts +20 -0
- package/dist/wizard/lib/provisioners/http-client.js +79 -0
- package/dist/wizard/lib/provisioners/railway.d.ts +6 -0
- package/dist/wizard/lib/provisioners/railway.js +413 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.js +54 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.js +112 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.d.ts +11 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.js +91 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.d.ts +5 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.js +185 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.js +36 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.d.ts +14 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.js +202 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.js +67 -0
- package/dist/wizard/lib/provisioners/self-deploy.d.ts +41 -0
- package/dist/wizard/lib/provisioners/self-deploy.js +185 -0
- package/dist/wizard/lib/provisioners/static-s3.d.ts +6 -0
- package/dist/wizard/lib/provisioners/static-s3.js +235 -0
- package/dist/wizard/lib/provisioners/types.d.ts +40 -0
- package/dist/wizard/lib/provisioners/types.js +4 -0
- package/dist/wizard/lib/provisioners/vercel.d.ts +6 -0
- package/dist/wizard/lib/provisioners/vercel.js +287 -0
- package/dist/wizard/lib/pty-manager.d.ts +42 -0
- package/dist/wizard/lib/pty-manager.js +231 -0
- package/dist/wizard/lib/rate-limiter-core.d.ts +5 -0
- package/dist/wizard/lib/rate-limiter-core.js +5 -0
- package/dist/wizard/lib/reconciliation.d.ts +43 -0
- package/dist/wizard/lib/reconciliation.js +173 -0
- package/dist/wizard/lib/revenue-types.d.ts +5 -0
- package/dist/wizard/lib/revenue-types.js +1 -0
- package/dist/wizard/lib/route-optimizer.d.ts +28 -0
- package/dist/wizard/lib/route-optimizer.js +93 -0
- package/dist/wizard/lib/s3-deploy.d.ts +19 -0
- package/dist/wizard/lib/s3-deploy.js +156 -0
- package/dist/wizard/lib/safety-tiers.d.ts +76 -0
- package/dist/wizard/lib/safety-tiers.js +134 -0
- package/dist/wizard/lib/sentry-generator.d.ts +15 -0
- package/dist/wizard/lib/sentry-generator.js +116 -0
- package/dist/wizard/lib/server-config.d.ts +13 -0
- package/dist/wizard/lib/server-config.js +23 -0
- package/dist/wizard/lib/service-install.d.ts +18 -0
- package/dist/wizard/lib/service-install.js +182 -0
- package/dist/wizard/lib/site-scanner.d.ts +80 -0
- package/dist/wizard/lib/site-scanner.js +262 -0
- package/dist/wizard/lib/ssh-deploy.d.ts +25 -0
- package/dist/wizard/lib/ssh-deploy.js +225 -0
- package/dist/wizard/lib/templates.d.ts +24 -0
- package/dist/wizard/lib/templates.js +219 -0
- package/dist/wizard/lib/totp.d.ts +35 -0
- package/dist/wizard/lib/totp.js +276 -0
- package/dist/wizard/lib/tower-auth.d.ts +43 -0
- package/dist/wizard/lib/tower-auth.js +352 -0
- package/dist/wizard/lib/tower-rate-limit.d.ts +14 -0
- package/dist/wizard/lib/tower-rate-limit.js +61 -0
- package/dist/wizard/lib/tower-session.d.ts +28 -0
- package/dist/wizard/lib/tower-session.js +119 -0
- package/dist/wizard/lib/treasury-backup.d.ts +23 -0
- package/dist/wizard/lib/treasury-backup.js +126 -0
- package/dist/wizard/lib/treasury-heartbeat.d.ts +82 -0
- package/dist/wizard/lib/treasury-heartbeat.js +1104 -0
- package/dist/wizard/lib/updater.d.ts +29 -0
- package/dist/wizard/lib/updater.js +190 -0
- package/dist/wizard/lib/user-manager.d.ts +39 -0
- package/dist/wizard/lib/user-manager.js +182 -0
- package/dist/wizard/lib/vault.d.ts +26 -0
- package/dist/wizard/lib/vault.js +161 -0
- package/dist/wizard/router.d.ts +5 -0
- package/dist/wizard/router.js +15 -0
- package/dist/wizard/server.d.ts +18 -0
- package/dist/wizard/server.js +436 -0
- package/package.json +59 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { request as httpsRequest } from 'node:https';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { addRoute } from '../router.js';
|
|
5
|
+
import { vaultGet } from '../lib/vault.js';
|
|
6
|
+
import { getSessionPassword } from './credentials.js';
|
|
7
|
+
import { resolveModelWithLimits } from '../lib/anthropic.js';
|
|
8
|
+
import { parseFrontmatter, validateFrontmatter } from '../lib/frontmatter.js';
|
|
9
|
+
import { parseJsonBody } from '../lib/body-parser.js';
|
|
10
|
+
import { listTemplates, getTemplate } from '../lib/templates.js';
|
|
11
|
+
import { sendJson } from '../lib/http-helpers.js';
|
|
12
|
+
/**
|
|
13
|
+
* Extract the prompt from inside the outer ``` fence after "## The Prompt".
|
|
14
|
+
* Tracks fence nesting so inner ```yaml / ``` pairs don't end extraction early.
|
|
15
|
+
*/
|
|
16
|
+
function extractFencedPrompt(markdown) {
|
|
17
|
+
const promptIdx = markdown.indexOf('## The Prompt');
|
|
18
|
+
if (promptIdx === -1)
|
|
19
|
+
return markdown;
|
|
20
|
+
const afterHeading = markdown.slice(promptIdx);
|
|
21
|
+
// Find the opening fence (a line that is exactly ```)
|
|
22
|
+
const openMatch = afterHeading.match(/\n```\s*\n/);
|
|
23
|
+
if (!openMatch || openMatch.index === undefined)
|
|
24
|
+
return markdown;
|
|
25
|
+
const contentStart = promptIdx + openMatch.index + openMatch[0].length;
|
|
26
|
+
const lines = markdown.slice(contentStart).split('\n');
|
|
27
|
+
const resultLines = [];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
// Opening fence: ```something or just ```
|
|
31
|
+
if (/^```\S/.test(line)) {
|
|
32
|
+
depth++;
|
|
33
|
+
resultLines.push(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Closing fence: exactly ```
|
|
37
|
+
if (/^```\s*$/.test(line)) {
|
|
38
|
+
if (depth > 0) {
|
|
39
|
+
depth--;
|
|
40
|
+
resultLines.push(line);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// depth === 0 means this closes the outer fence
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
resultLines.push(line);
|
|
47
|
+
}
|
|
48
|
+
return resultLines.join('\n').trim();
|
|
49
|
+
}
|
|
50
|
+
// POST /api/prd/validate — validate PRD content
|
|
51
|
+
addRoute('POST', '/api/prd/validate', async (req, res) => {
|
|
52
|
+
const body = await parseJsonBody(req);
|
|
53
|
+
if (!body.content || typeof body.content !== 'string') {
|
|
54
|
+
sendJson(res, 400, { error: 'content is required' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const { frontmatter } = parseFrontmatter(body.content);
|
|
58
|
+
const errors = validateFrontmatter(frontmatter);
|
|
59
|
+
sendJson(res, 200, {
|
|
60
|
+
valid: errors.length === 0,
|
|
61
|
+
errors,
|
|
62
|
+
frontmatter,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
// POST /api/prd/generate — generate PRD from idea using Claude
|
|
66
|
+
addRoute('POST', '/api/prd/generate', async (req, res) => {
|
|
67
|
+
const body = await parseJsonBody(req);
|
|
68
|
+
if (!body.idea || typeof body.idea !== 'string') {
|
|
69
|
+
sendJson(res, 400, { error: 'idea is required' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const password = getSessionPassword();
|
|
73
|
+
if (!password) {
|
|
74
|
+
sendJson(res, 401, { error: 'Vault is locked. Unlock with your password first.' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const apiKey = await vaultGet(password, 'anthropic-api-key');
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
sendJson(res, 400, { error: 'Anthropic API key not configured. Complete step 1 first.' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Load the PRD generator prompt
|
|
83
|
+
const promptPath = join(import.meta.dirname, '..', '..', 'docs', 'methods', 'PRD_GENERATOR.md');
|
|
84
|
+
let generatorPrompt;
|
|
85
|
+
try {
|
|
86
|
+
generatorPrompt = await readFile(promptPath, 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
sendJson(res, 500, { error: 'Could not load PRD generator prompt' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Extract prompt from inside the outer ``` fence block after "## The Prompt"
|
|
93
|
+
const basePrompt = extractFencedPrompt(generatorPrompt);
|
|
94
|
+
// Build the idea with any preferences
|
|
95
|
+
let userIdea = body.idea;
|
|
96
|
+
const prefs = [];
|
|
97
|
+
if (body.name)
|
|
98
|
+
prefs.push(`Project name: ${body.name}`);
|
|
99
|
+
if (body.framework)
|
|
100
|
+
prefs.push(`Framework preference: ${body.framework}`);
|
|
101
|
+
if (body.database)
|
|
102
|
+
prefs.push(`Database preference: ${body.database}`);
|
|
103
|
+
if (body.deploy)
|
|
104
|
+
prefs.push(`Deploy target: ${body.deploy}`);
|
|
105
|
+
if (prefs.length > 0) {
|
|
106
|
+
userIdea += '\n\nPreferences:\n' + prefs.join('\n');
|
|
107
|
+
}
|
|
108
|
+
// Resolve the best available model with its max output capacity
|
|
109
|
+
const { id: model, maxTokens } = await resolveModelWithLimits(apiKey);
|
|
110
|
+
// Stream response via SSE
|
|
111
|
+
res.writeHead(200, {
|
|
112
|
+
'Content-Type': 'text/event-stream',
|
|
113
|
+
'Cache-Control': 'no-cache',
|
|
114
|
+
'Connection': 'keep-alive',
|
|
115
|
+
});
|
|
116
|
+
const postData = JSON.stringify({
|
|
117
|
+
model,
|
|
118
|
+
max_tokens: maxTokens,
|
|
119
|
+
stream: true,
|
|
120
|
+
messages: [
|
|
121
|
+
{
|
|
122
|
+
role: 'user',
|
|
123
|
+
content: `${basePrompt}\n\nTHE PRODUCT IDEA:\n${userIdea}`,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
let clientDisconnected = false;
|
|
128
|
+
/** Safe write — no-op if client already disconnected */
|
|
129
|
+
function sseWrite(chunk) {
|
|
130
|
+
if (clientDisconnected || res.writableEnded)
|
|
131
|
+
return;
|
|
132
|
+
try {
|
|
133
|
+
res.write(chunk);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
clientDisconnected = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function sseEnd() {
|
|
140
|
+
if (clientDisconnected || res.writableEnded)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
res.end();
|
|
144
|
+
}
|
|
145
|
+
catch { /* already closed */ }
|
|
146
|
+
}
|
|
147
|
+
req.on('close', () => {
|
|
148
|
+
clientDisconnected = true;
|
|
149
|
+
clearInterval(keepaliveTimer);
|
|
150
|
+
apiReq.destroy();
|
|
151
|
+
});
|
|
152
|
+
// SSE keepalive — prevents proxy/VPN/browser timeout during generation
|
|
153
|
+
const keepaliveTimer = setInterval(() => {
|
|
154
|
+
sseWrite(': keepalive\n\n');
|
|
155
|
+
}, 15000);
|
|
156
|
+
const apiReq = httpsRequest({
|
|
157
|
+
hostname: 'api.anthropic.com',
|
|
158
|
+
path: '/v1/messages',
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
'x-api-key': apiKey,
|
|
163
|
+
'anthropic-version': '2023-06-01',
|
|
164
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
165
|
+
},
|
|
166
|
+
timeout: 120000,
|
|
167
|
+
}, (apiRes) => {
|
|
168
|
+
if (apiRes.statusCode !== 200) {
|
|
169
|
+
let errBody = '';
|
|
170
|
+
apiRes.on('data', (chunk) => { errBody += chunk.toString(); });
|
|
171
|
+
apiRes.on('end', () => {
|
|
172
|
+
clearInterval(keepaliveTimer);
|
|
173
|
+
sseWrite(`data: ${JSON.stringify({ error: `API error: ${apiRes.statusCode}` })}\n\n`);
|
|
174
|
+
sseWrite('data: [DONE]\n\n');
|
|
175
|
+
sseEnd();
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let buffer = '';
|
|
180
|
+
let stopReason = null;
|
|
181
|
+
apiRes.on('data', (chunk) => {
|
|
182
|
+
buffer += chunk.toString();
|
|
183
|
+
const lines = buffer.split('\n');
|
|
184
|
+
buffer = lines.pop() ?? '';
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
if (!line.startsWith('data: '))
|
|
187
|
+
continue;
|
|
188
|
+
const data = line.slice(6);
|
|
189
|
+
if (data === '[DONE]')
|
|
190
|
+
continue;
|
|
191
|
+
try {
|
|
192
|
+
const event = JSON.parse(data);
|
|
193
|
+
if (event.type === 'content_block_delta' && event.delta?.text) {
|
|
194
|
+
sseWrite(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`);
|
|
195
|
+
}
|
|
196
|
+
if (event.type === 'message_delta' && event.delta?.stop_reason) {
|
|
197
|
+
stopReason = event.delta.stop_reason;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Skip unparseable chunks
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
apiRes.on('end', () => {
|
|
206
|
+
clearInterval(keepaliveTimer);
|
|
207
|
+
if (stopReason === 'max_tokens') {
|
|
208
|
+
sseWrite(`data: ${JSON.stringify({ truncated: true })}\n\n`);
|
|
209
|
+
}
|
|
210
|
+
sseWrite('data: [DONE]\n\n');
|
|
211
|
+
sseEnd();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
apiReq.on('error', (err) => {
|
|
215
|
+
clearInterval(keepaliveTimer);
|
|
216
|
+
sseWrite(`data: ${JSON.stringify({ error: 'Generation failed' })}\n\n`);
|
|
217
|
+
sseWrite('data: [DONE]\n\n');
|
|
218
|
+
sseEnd();
|
|
219
|
+
});
|
|
220
|
+
apiReq.write(postData);
|
|
221
|
+
apiReq.end();
|
|
222
|
+
});
|
|
223
|
+
// POST /api/prd/env-requirements — parse PRD for project-specific credentials
|
|
224
|
+
addRoute('POST', '/api/prd/env-requirements', async (req, res) => {
|
|
225
|
+
const body = await parseJsonBody(req);
|
|
226
|
+
if (!body.content || typeof body.content !== 'string') {
|
|
227
|
+
sendJson(res, 400, { error: 'content is required' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const groups = parseEnvRequirements(body.content);
|
|
231
|
+
sendJson(res, 200, { groups });
|
|
232
|
+
});
|
|
233
|
+
/**
|
|
234
|
+
* Env vars that are auto-generated, infrastructure, or app config — never collect from user.
|
|
235
|
+
* These are either provisioned by the deploy pipeline, generated at build time,
|
|
236
|
+
* or derived from project config. Generic across all projects.
|
|
237
|
+
*/
|
|
238
|
+
const SKIP_VARS = new Set([
|
|
239
|
+
// App config (derived from project setup)
|
|
240
|
+
'NODE_ENV', 'PORT',
|
|
241
|
+
'NEXT_PUBLIC_APP_URL', 'NEXT_PUBLIC_APP_NAME',
|
|
242
|
+
// Infrastructure (provisioned by deploy pipeline)
|
|
243
|
+
'DATABASE_URL', 'REDIS_URL', 'REDIS_PASSWORD',
|
|
244
|
+
// Secrets (auto-generated at build time)
|
|
245
|
+
'SESSION_SECRET', 'SESSION_COOKIE_NAME', 'SESSION_TTL_DAYS',
|
|
246
|
+
'CSRF_SECRET',
|
|
247
|
+
// Storage (provisioned by deploy pipeline)
|
|
248
|
+
'S3_ENDPOINT', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'S3_BUCKET_NAME',
|
|
249
|
+
'S3_REGION', 'S3_PUBLIC_URL',
|
|
250
|
+
]);
|
|
251
|
+
/** Prefixes that indicate feature flags — always skip. */
|
|
252
|
+
const SKIP_PREFIXES = ['ENABLE_'];
|
|
253
|
+
function parseEnvRequirements(prdContent) {
|
|
254
|
+
// Find the env vars section — look for a block with multiple VAR="value" lines
|
|
255
|
+
const lines = prdContent.split('\n');
|
|
256
|
+
const groups = [];
|
|
257
|
+
let currentGroup = null;
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
// Detect section headers like "# ─── WhatsApp Business API ───────"
|
|
260
|
+
const headerMatch = line.match(/^#\s*[─\-]+\s*(.+?)\s*[─\-]*$/);
|
|
261
|
+
if (headerMatch) {
|
|
262
|
+
const name = headerMatch[1].trim();
|
|
263
|
+
currentGroup = { name, fields: [] };
|
|
264
|
+
groups.push(currentGroup);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// Detect env var assignments: VAR_NAME="value" or VAR_NAME=value
|
|
268
|
+
const varMatch = line.match(/^([A-Z][A-Z0-9_]+)=["']?(.*?)["']?\s*(?:#.*)?$/);
|
|
269
|
+
if (!varMatch || !currentGroup)
|
|
270
|
+
continue;
|
|
271
|
+
const [, key, rawValue] = varMatch;
|
|
272
|
+
// Skip auto-generated / infrastructure / tuning vars
|
|
273
|
+
if (SKIP_VARS.has(key))
|
|
274
|
+
continue;
|
|
275
|
+
if (SKIP_PREFIXES.some((p) => key.startsWith(p)))
|
|
276
|
+
continue;
|
|
277
|
+
const value = rawValue.trim();
|
|
278
|
+
// Skip tuning params — values that are purely numeric, boolean, or duration-like
|
|
279
|
+
// These are config knobs, not API credentials (e.g., MAX_RETRIES="5", TIMEOUT_MS="30000")
|
|
280
|
+
if (/^\d+$/.test(value) || value === 'true' || value === 'false')
|
|
281
|
+
continue;
|
|
282
|
+
// Skip if the value looks like a real config URL (not a placeholder)
|
|
283
|
+
const isApiUrl = key.endsWith('_API_URL') || key.endsWith('_URL');
|
|
284
|
+
if (isApiUrl && value.startsWith('http'))
|
|
285
|
+
continue;
|
|
286
|
+
// Only collect vars that look like they need user-provided values
|
|
287
|
+
// (empty, placeholder prefixes, or common API key patterns)
|
|
288
|
+
const isPlaceholder = !value
|
|
289
|
+
|| value.includes('your-')
|
|
290
|
+
|| value.includes('your_')
|
|
291
|
+
|| /^(sk-|pk\.|AIza|re_|ghp_)/.test(value)
|
|
292
|
+
|| value.endsWith('...')
|
|
293
|
+
|| value.startsWith('Bearer ');
|
|
294
|
+
if (!isPlaceholder && value.length > 0)
|
|
295
|
+
continue;
|
|
296
|
+
// Determine if this is a secret field
|
|
297
|
+
const secretPatterns = ['KEY', 'SECRET', 'TOKEN', 'PASSWORD'];
|
|
298
|
+
const isSecret = secretPatterns.some((p) => key.includes(p));
|
|
299
|
+
// Generate human-readable label from var name
|
|
300
|
+
const label = key
|
|
301
|
+
.replace(/_/g, ' ')
|
|
302
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
303
|
+
.replace(/\bApi\b/g, 'API')
|
|
304
|
+
.replace(/\bUrl\b/g, 'URL')
|
|
305
|
+
.replace(/\bId\b/g, 'ID')
|
|
306
|
+
.replace(/\bSdk\b/g, 'SDK')
|
|
307
|
+
.replace(/\bApp\b/g, 'App');
|
|
308
|
+
currentGroup.fields.push({
|
|
309
|
+
key,
|
|
310
|
+
label,
|
|
311
|
+
placeholder: value || key.toLowerCase().replace(/_/g, '-'),
|
|
312
|
+
secret: isSecret,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// Filter out empty groups
|
|
316
|
+
return groups.filter((g) => g.fields.length > 0);
|
|
317
|
+
}
|
|
318
|
+
// GET /api/prd/template — return the PRD template
|
|
319
|
+
addRoute('GET', '/api/prd/template', async (_req, res) => {
|
|
320
|
+
const templatePath = join(import.meta.dirname, '..', '..', 'docs', 'PRD.md');
|
|
321
|
+
try {
|
|
322
|
+
const content = await readFile(templatePath, 'utf-8');
|
|
323
|
+
sendJson(res, 200, { content });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
sendJson(res, 500, { error: 'Could not load PRD template' });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
// GET /api/prd/prompt — return the PRD generator prompt for use with other AIs
|
|
330
|
+
addRoute('GET', '/api/prd/prompt', async (_req, res) => {
|
|
331
|
+
const promptPath = join(import.meta.dirname, '..', '..', 'docs', 'methods', 'PRD_GENERATOR.md');
|
|
332
|
+
try {
|
|
333
|
+
const content = await readFile(promptPath, 'utf-8');
|
|
334
|
+
sendJson(res, 200, { prompt: extractFencedPrompt(content) });
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
sendJson(res, 500, { error: 'Could not load PRD generator prompt' });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
// GET /api/prd/templates — list available project templates
|
|
341
|
+
addRoute('GET', '/api/prd/templates', async (_req, res) => {
|
|
342
|
+
sendJson(res, 200, { templates: listTemplates() });
|
|
343
|
+
});
|
|
344
|
+
// GET /api/prd/templates/:id — get a specific template's full PRD content
|
|
345
|
+
addRoute('GET', '/api/prd/templates/get', async (req, res) => {
|
|
346
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
347
|
+
const id = url.searchParams.get('id');
|
|
348
|
+
if (!id) {
|
|
349
|
+
sendJson(res, 400, { error: 'id query parameter is required' });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const template = getTemplate(id);
|
|
353
|
+
if (!template) {
|
|
354
|
+
sendJson(res, 404, { error: `Template not found: ${id}` });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// Build a complete PRD with frontmatter
|
|
358
|
+
const frontmatterYaml = Object.entries(template.frontmatter)
|
|
359
|
+
.map(([k, v]) => `${k}: "${v}"`)
|
|
360
|
+
.join('\n');
|
|
361
|
+
const prd = `\`\`\`yaml\nname: "[PROJECT_NAME]"\n${frontmatterYaml}\n\`\`\`\n\n---\n\n${template.prdSections}`;
|
|
362
|
+
sendJson(res, 200, { template: { ...template, prd } });
|
|
363
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, copyFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { addRoute } from '../router.js';
|
|
6
|
+
import { parseJsonBody } from '../lib/body-parser.js';
|
|
7
|
+
import { addProject } from '../lib/project-registry.js';
|
|
8
|
+
import { validateSession, parseSessionCookie, getClientIp, isRemoteMode } from '../lib/tower-auth.js';
|
|
9
|
+
import { sendJson } from '../lib/http-helpers.js';
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const SCAFFOLD_DIR = resolve(import.meta.dirname, '..', '..');
|
|
12
|
+
function sanitizeDirName(name) {
|
|
13
|
+
return name
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9\-_]/g, '-')
|
|
16
|
+
.replace(/-+/g, '-')
|
|
17
|
+
.replace(/^-|-$/g, '');
|
|
18
|
+
}
|
|
19
|
+
// POST /api/project/validate — validate project config
|
|
20
|
+
addRoute('POST', '/api/project/validate', async (req, res) => {
|
|
21
|
+
const body = await parseJsonBody(req);
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!body.name || body.name.trim().length === 0) {
|
|
24
|
+
errors.push('Project name is required');
|
|
25
|
+
}
|
|
26
|
+
if (!body.directory || body.directory.trim().length === 0) {
|
|
27
|
+
errors.push('Project directory is required');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const dir = resolve(body.directory);
|
|
31
|
+
try {
|
|
32
|
+
const s = await stat(dir);
|
|
33
|
+
if (s.isDirectory()) {
|
|
34
|
+
const entries = await readdir(dir);
|
|
35
|
+
if (entries.length > 0) {
|
|
36
|
+
errors.push('Directory already exists and is not empty');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Directory doesn't exist — that's fine
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const suggestedDir = body.name
|
|
45
|
+
? resolve(process.cwd(), '..', sanitizeDirName(body.name))
|
|
46
|
+
: undefined;
|
|
47
|
+
sendJson(res, 200, { valid: errors.length === 0, errors, suggestedDir });
|
|
48
|
+
});
|
|
49
|
+
/** Recursively copy a directory, excluding specified paths */
|
|
50
|
+
async function copyDir(src, dest, exclude = []) {
|
|
51
|
+
await mkdir(dest, { recursive: true });
|
|
52
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const srcPath = join(src, entry.name);
|
|
55
|
+
const destPath = join(dest, entry.name);
|
|
56
|
+
// Check exclusions
|
|
57
|
+
const relative = srcPath.slice(SCAFFOLD_DIR.length + 1);
|
|
58
|
+
if (exclude.some(ex => relative.startsWith(ex))) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
await copyDir(srcPath, destPath, exclude);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await copyFile(srcPath, destPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// POST /api/project/create — create the project
|
|
70
|
+
addRoute('POST', '/api/project/create', async (req, res) => {
|
|
71
|
+
const raw = await parseJsonBody(req);
|
|
72
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
73
|
+
sendJson(res, 400, { error: 'Request body must be a JSON object' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const body = raw;
|
|
77
|
+
if (typeof body.name !== 'string' || body.name.trim().length === 0 ||
|
|
78
|
+
typeof body.directory !== 'string' || body.directory.trim().length === 0) {
|
|
79
|
+
sendJson(res, 400, { error: 'name and directory are required strings' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Coerce optional fields to safe strings (strip newlines for .env injection prevention)
|
|
83
|
+
const safeName = String(body.name).replace(/[\r\n]/g, ' ').trim();
|
|
84
|
+
const safeDir = String(body.directory).trim();
|
|
85
|
+
const safeDescription = typeof body.description === 'string' ? body.description.replace(/[\r\n]/g, ' ').trim() : '';
|
|
86
|
+
const safeDomain = typeof body.domain === 'string' ? body.domain.replace(/[\r\n]/g, '').trim() : '';
|
|
87
|
+
const safeHostname = typeof body.hostname === 'string' ? body.hostname.replace(/[\r\n]/g, '').trim() : '';
|
|
88
|
+
const safeDeploy = typeof body.deploy === 'string' ? body.deploy.replace(/[\r\n]/g, '').trim() : '';
|
|
89
|
+
const safePrd = typeof body.prd === 'string' ? body.prd : '';
|
|
90
|
+
const projectDir = resolve(safeDir);
|
|
91
|
+
try {
|
|
92
|
+
// Create project directory
|
|
93
|
+
await mkdir(projectDir, { recursive: true });
|
|
94
|
+
// Copy CLAUDE.md
|
|
95
|
+
await copyFile(join(SCAFFOLD_DIR, 'CLAUDE.md'), join(projectDir, 'CLAUDE.md'));
|
|
96
|
+
// Copy docs/
|
|
97
|
+
await copyDir(join(SCAFFOLD_DIR, 'docs'), join(projectDir, 'docs'));
|
|
98
|
+
// Copy .claude/
|
|
99
|
+
const claudeDir = join(SCAFFOLD_DIR, '.claude');
|
|
100
|
+
try {
|
|
101
|
+
await copyDir(claudeDir, join(projectDir, '.claude'));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// .claude dir might not exist
|
|
105
|
+
}
|
|
106
|
+
// Copy .gitignore
|
|
107
|
+
try {
|
|
108
|
+
await copyFile(join(SCAFFOLD_DIR, '.gitignore'), join(projectDir, '.gitignore'));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// OK
|
|
112
|
+
}
|
|
113
|
+
// Create logs directory with build-state.md
|
|
114
|
+
await mkdir(join(projectDir, 'logs'), { recursive: true });
|
|
115
|
+
const buildState = `# Build State
|
|
116
|
+
|
|
117
|
+
**Project:** ${safeName}
|
|
118
|
+
**Current Phase:** 0 (not started)
|
|
119
|
+
**Last Updated:** ${new Date().toISOString()}
|
|
120
|
+
**Active Agent:** None
|
|
121
|
+
|
|
122
|
+
## Phase Status
|
|
123
|
+
| Phase | Status | Gate Passed |
|
|
124
|
+
|-------|--------|-------------|
|
|
125
|
+
| 0-13 | not started | — |
|
|
126
|
+
|
|
127
|
+
## Current Blockers
|
|
128
|
+
- None — ready to start. Run /build to begin.
|
|
129
|
+
|
|
130
|
+
## Next Steps
|
|
131
|
+
1. Review docs/PRD.md
|
|
132
|
+
2. Run /build to start Phase 0
|
|
133
|
+
`;
|
|
134
|
+
await writeFile(join(projectDir, 'logs', 'build-state.md'), buildState);
|
|
135
|
+
// Replace placeholder in CLAUDE.md
|
|
136
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
137
|
+
let claudeMd = await readFile(claudeMdPath, 'utf-8');
|
|
138
|
+
claudeMd = claudeMd.replace(/\[PROJECT_NAME\]/g, safeName);
|
|
139
|
+
if (safeDescription) {
|
|
140
|
+
claudeMd = claudeMd.replace(/\[ONE_LINE_DESCRIPTION\]/g, safeDescription);
|
|
141
|
+
}
|
|
142
|
+
if (safeDomain) {
|
|
143
|
+
claudeMd = claudeMd.replace(/\[DOMAIN\]/g, safeDomain);
|
|
144
|
+
}
|
|
145
|
+
await writeFile(claudeMdPath, claudeMd);
|
|
146
|
+
// Write PRD if provided
|
|
147
|
+
if (safePrd) {
|
|
148
|
+
await writeFile(join(projectDir, 'docs', 'PRD.md'), safePrd);
|
|
149
|
+
}
|
|
150
|
+
// Create .env from template (newlines stripped from values to prevent injection)
|
|
151
|
+
const deployLine = safeDeploy ? `\n# Deploy target: ${safeDeploy}\nDEPLOY_TARGET=${safeDeploy}\n` : '';
|
|
152
|
+
const hostnameLine = safeHostname ? `\n# DNS hostname (Cloudflare)\nHOSTNAME=${safeHostname}\n` : '';
|
|
153
|
+
const envContent = `# ${safeName} — Environment Variables
|
|
154
|
+
# Generated by VoidForge wizard on ${new Date().toISOString()}
|
|
155
|
+
${deployLine}${hostnameLine}
|
|
156
|
+
# Add your environment variables here
|
|
157
|
+
# NODE_ENV=development
|
|
158
|
+
`;
|
|
159
|
+
await writeFile(join(projectDir, '.env'), envContent);
|
|
160
|
+
// Initialize git repo
|
|
161
|
+
try {
|
|
162
|
+
await execFileAsync('git', ['init'], { cwd: projectDir });
|
|
163
|
+
await execFileAsync('git', ['add', '-A'], { cwd: projectDir });
|
|
164
|
+
await execFileAsync('git', ['commit', '-m', `Initial commit: ${safeName} via VoidForge`], { cwd: projectDir });
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
// Git init is best-effort
|
|
168
|
+
console.warn('Git initialization warning:', err);
|
|
169
|
+
}
|
|
170
|
+
// Register in project registry for The Lobby
|
|
171
|
+
try {
|
|
172
|
+
await addProject({
|
|
173
|
+
name: safeName,
|
|
174
|
+
directory: projectDir,
|
|
175
|
+
deployTarget: safeDeploy || 'unknown',
|
|
176
|
+
deployUrl: safeHostname ? `https://${safeHostname}` : '',
|
|
177
|
+
sshHost: '',
|
|
178
|
+
framework: 'unknown',
|
|
179
|
+
database: 'none',
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
lastBuildPhase: 0,
|
|
182
|
+
lastDeployAt: '',
|
|
183
|
+
healthCheckUrl: '',
|
|
184
|
+
monthlyCost: 0,
|
|
185
|
+
owner: (() => {
|
|
186
|
+
if (!isRemoteMode())
|
|
187
|
+
return 'local';
|
|
188
|
+
const token = parseSessionCookie(req.headers.cookie);
|
|
189
|
+
const ip = getClientIp(req);
|
|
190
|
+
const session = token ? validateSession(token, ip) : null;
|
|
191
|
+
return session?.username ?? '';
|
|
192
|
+
})(),
|
|
193
|
+
access: [],
|
|
194
|
+
linkedProjects: [],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Registry write is best-effort — don't fail project creation
|
|
199
|
+
}
|
|
200
|
+
// Write .voidforge marker file for CLI compatibility
|
|
201
|
+
try {
|
|
202
|
+
const { createMarker, writeMarker } = await import('../lib/marker.js');
|
|
203
|
+
const marker = createMarker('21.0.0', 'full');
|
|
204
|
+
await writeMarker(projectDir, marker);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Marker write is best-effort — don't fail project creation
|
|
208
|
+
}
|
|
209
|
+
sendJson(res, 200, {
|
|
210
|
+
created: true,
|
|
211
|
+
directory: projectDir,
|
|
212
|
+
files: [
|
|
213
|
+
'CLAUDE.md',
|
|
214
|
+
'.claude/commands/',
|
|
215
|
+
'.claude/settings.json',
|
|
216
|
+
'docs/PRD.md',
|
|
217
|
+
'docs/methods/',
|
|
218
|
+
'docs/patterns/',
|
|
219
|
+
'docs/LESSONS.md',
|
|
220
|
+
'logs/build-state.md',
|
|
221
|
+
'.env',
|
|
222
|
+
'.gitignore',
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : 'Failed to create project';
|
|
228
|
+
console.error('Project creation error:', message);
|
|
229
|
+
sendJson(res, 500, { error: 'Failed to create project' });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
// GET /api/project/defaults — return default values
|
|
233
|
+
addRoute('GET', '/api/project/defaults', async (_req, res) => {
|
|
234
|
+
const homeDir = process.env['HOME'] ?? '/tmp';
|
|
235
|
+
sendJson(res, 200, {
|
|
236
|
+
baseDir: resolve(homeDir, 'Projects'),
|
|
237
|
+
scaffoldDir: SCAFFOLD_DIR,
|
|
238
|
+
});
|
|
239
|
+
});
|