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,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Registry — CRUD for ~/.voidforge/projects.json.
|
|
3
|
+
* Zero-dep JSON file storage for multi-project Avengers Tower.
|
|
4
|
+
* File permissions: 0600 (owner read/write only).
|
|
5
|
+
*
|
|
6
|
+
* Follows vault.ts patterns: serialized writes, atomic file ops, homedir().
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, rename, mkdir, open, copyFile, chmod } from 'node:fs/promises';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
const VOIDFORGE_DIR = join(homedir(), '.voidforge');
|
|
13
|
+
const REGISTRY_PATH = join(VOIDFORGE_DIR, 'projects.json');
|
|
14
|
+
// ── Write serialization (from vault.ts) ────────────
|
|
15
|
+
let writeQueue = Promise.resolve();
|
|
16
|
+
function serialized(fn) {
|
|
17
|
+
const result = writeQueue.then(fn, () => fn());
|
|
18
|
+
writeQueue = result.then(() => { }, () => { });
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
// ── Validation ─────────────────────────────────────
|
|
22
|
+
const VALID_HEALTH_STATUSES = new Set(['healthy', 'degraded', 'down', 'unchecked']);
|
|
23
|
+
function isValidProject(obj) {
|
|
24
|
+
if (typeof obj !== 'object' || obj === null)
|
|
25
|
+
return false;
|
|
26
|
+
const p = obj;
|
|
27
|
+
const valid = (typeof p.id === 'string' &&
|
|
28
|
+
typeof p.name === 'string' &&
|
|
29
|
+
typeof p.directory === 'string' &&
|
|
30
|
+
typeof p.deployTarget === 'string' &&
|
|
31
|
+
typeof p.deployUrl === 'string' &&
|
|
32
|
+
typeof p.sshHost === 'string' &&
|
|
33
|
+
typeof p.framework === 'string' &&
|
|
34
|
+
typeof p.database === 'string' &&
|
|
35
|
+
typeof p.createdAt === 'string' &&
|
|
36
|
+
typeof p.lastBuildPhase === 'number' &&
|
|
37
|
+
typeof p.lastDeployAt === 'string' &&
|
|
38
|
+
typeof p.healthCheckUrl === 'string' &&
|
|
39
|
+
typeof p.monthlyCost === 'number' &&
|
|
40
|
+
typeof p.healthStatus === 'string' &&
|
|
41
|
+
VALID_HEALTH_STATUSES.has(p.healthStatus) &&
|
|
42
|
+
typeof p.healthCheckedAt === 'string');
|
|
43
|
+
if (!valid)
|
|
44
|
+
return false;
|
|
45
|
+
// Migrate legacy projects without owner/access/linkedProjects fields
|
|
46
|
+
if (typeof p.owner !== 'string')
|
|
47
|
+
p.owner = '';
|
|
48
|
+
if (!Array.isArray(p.access))
|
|
49
|
+
p.access = [];
|
|
50
|
+
if (!Array.isArray(p.linkedProjects))
|
|
51
|
+
p.linkedProjects = [];
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// ── Path normalization ─────────────────────────────
|
|
55
|
+
function normalizePath(dir) {
|
|
56
|
+
return resolve(dir);
|
|
57
|
+
}
|
|
58
|
+
// ── File I/O (atomic writes, following vault.ts) ───
|
|
59
|
+
/** Read the full registry. Returns empty array if file doesn't exist. */
|
|
60
|
+
export async function readRegistry() {
|
|
61
|
+
try {
|
|
62
|
+
const raw = await readFile(REGISTRY_PATH, 'utf-8');
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (!Array.isArray(parsed))
|
|
65
|
+
return [];
|
|
66
|
+
// Validate each entry, filter out invalid ones
|
|
67
|
+
return parsed.filter(isValidProject);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
// File not found is expected — return empty registry
|
|
71
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
// JSON parse error or permission denied — throw so callers don't overwrite data
|
|
75
|
+
throw new Error(`Registry corrupted or unreadable: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Atomic write: backup → temp file → fsync → rename (from vault.ts). */
|
|
79
|
+
async function writeRegistry(projects) {
|
|
80
|
+
await mkdir(VOIDFORGE_DIR, { recursive: true });
|
|
81
|
+
// Backup current file before overwriting (data loss prevention)
|
|
82
|
+
try {
|
|
83
|
+
await copyFile(REGISTRY_PATH, REGISTRY_PATH + '.bak');
|
|
84
|
+
await chmod(REGISTRY_PATH + '.bak', 0o600);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// ENOENT = no file to back up (expected on first write)
|
|
88
|
+
// Other errors (disk full, permissions) = log but don't block the write
|
|
89
|
+
if (!(err instanceof Error && 'code' in err && err.code === 'ENOENT')) {
|
|
90
|
+
console.error('Registry backup failed:', err instanceof Error ? err.message : 'Unknown error');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const data = JSON.stringify(projects, null, 2);
|
|
94
|
+
const tmpPath = REGISTRY_PATH + '.tmp';
|
|
95
|
+
const fh = await open(tmpPath, 'w', 0o600);
|
|
96
|
+
try {
|
|
97
|
+
await fh.writeFile(data);
|
|
98
|
+
await fh.sync();
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await fh.close();
|
|
102
|
+
}
|
|
103
|
+
await rename(tmpPath, REGISTRY_PATH);
|
|
104
|
+
}
|
|
105
|
+
// ── Public API (all mutating ops are serialized) ───
|
|
106
|
+
/**
|
|
107
|
+
* Add a new project to the registry.
|
|
108
|
+
* @throws Error if a project with the same directory is already registered.
|
|
109
|
+
*/
|
|
110
|
+
export function addProject(input) {
|
|
111
|
+
return serialized(async () => {
|
|
112
|
+
const projects = await readRegistry();
|
|
113
|
+
const normalized = normalizePath(input.directory);
|
|
114
|
+
const exists = projects.some((p) => normalizePath(p.directory) === normalized);
|
|
115
|
+
if (exists) {
|
|
116
|
+
throw new Error(`Project already registered at ${input.directory}`);
|
|
117
|
+
}
|
|
118
|
+
const project = {
|
|
119
|
+
...input,
|
|
120
|
+
directory: normalized,
|
|
121
|
+
id: randomUUID(),
|
|
122
|
+
healthStatus: 'unchecked',
|
|
123
|
+
healthCheckedAt: '',
|
|
124
|
+
owner: input.owner ?? '',
|
|
125
|
+
access: input.access ?? [],
|
|
126
|
+
linkedProjects: input.linkedProjects ?? [],
|
|
127
|
+
};
|
|
128
|
+
projects.push(project);
|
|
129
|
+
await writeRegistry(projects);
|
|
130
|
+
return project;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Get a project by ID. Returns null if not found. */
|
|
134
|
+
export async function getProject(id) {
|
|
135
|
+
const projects = await readRegistry();
|
|
136
|
+
return projects.find((p) => p.id === id) ?? null;
|
|
137
|
+
}
|
|
138
|
+
/** Find a project by directory path. Returns null if not found. */
|
|
139
|
+
export async function findByDirectory(directory) {
|
|
140
|
+
const projects = await readRegistry();
|
|
141
|
+
const normalized = normalizePath(directory);
|
|
142
|
+
return projects.find((p) => normalizePath(p.directory) === normalized) ?? null;
|
|
143
|
+
}
|
|
144
|
+
/** Mutable fields — prevents callers from injecting arbitrary keys via spread. */
|
|
145
|
+
const MUTABLE_FIELDS = new Set([
|
|
146
|
+
'name', 'deployTarget', 'deployUrl', 'sshHost', 'framework', 'database',
|
|
147
|
+
'lastBuildPhase', 'lastDeployAt', 'healthCheckUrl', 'monthlyCost',
|
|
148
|
+
'healthStatus', 'healthCheckedAt',
|
|
149
|
+
]);
|
|
150
|
+
/** Update a project by ID. Merges only known mutable fields. Returns null if not found. */
|
|
151
|
+
export function updateProject(id, updates) {
|
|
152
|
+
return serialized(async () => {
|
|
153
|
+
const projects = await readRegistry();
|
|
154
|
+
const idx = projects.findIndex((p) => p.id === id);
|
|
155
|
+
if (idx === -1)
|
|
156
|
+
return null;
|
|
157
|
+
// Pick only known mutable fields from updates, validate enum fields
|
|
158
|
+
const safe = {};
|
|
159
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
160
|
+
if (!MUTABLE_FIELDS.has(key))
|
|
161
|
+
continue;
|
|
162
|
+
// Validate healthStatus against allowed values
|
|
163
|
+
if (key === 'healthStatus' && !VALID_HEALTH_STATUSES.has(value))
|
|
164
|
+
continue;
|
|
165
|
+
safe[key] = value;
|
|
166
|
+
}
|
|
167
|
+
projects[idx] = { ...projects[idx], ...safe };
|
|
168
|
+
await writeRegistry(projects);
|
|
169
|
+
return projects[idx];
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/** Remove a project by ID. Cleans up linked references in other projects. */
|
|
173
|
+
export function removeProject(id) {
|
|
174
|
+
return serialized(async () => {
|
|
175
|
+
const projects = await readRegistry();
|
|
176
|
+
const idx = projects.findIndex((p) => p.id === id);
|
|
177
|
+
if (idx === -1)
|
|
178
|
+
return false;
|
|
179
|
+
projects.splice(idx, 1);
|
|
180
|
+
// Clean up linked references in remaining projects
|
|
181
|
+
for (const project of projects) {
|
|
182
|
+
project.linkedProjects = project.linkedProjects.filter((lid) => lid !== id);
|
|
183
|
+
}
|
|
184
|
+
await writeRegistry(projects);
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** Update health status for a project. */
|
|
189
|
+
export function updateHealthStatus(id, status) {
|
|
190
|
+
return serialized(async () => {
|
|
191
|
+
const projects = await readRegistry();
|
|
192
|
+
const idx = projects.findIndex((p) => p.id === id);
|
|
193
|
+
if (idx === -1)
|
|
194
|
+
return;
|
|
195
|
+
projects[idx] = {
|
|
196
|
+
...projects[idx],
|
|
197
|
+
healthStatus: status,
|
|
198
|
+
healthCheckedAt: new Date().toISOString(),
|
|
199
|
+
};
|
|
200
|
+
await writeRegistry(projects);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** LOKI-004: Batch update health status — single read-write cycle for N projects. */
|
|
204
|
+
export function batchUpdateHealthStatus(updates) {
|
|
205
|
+
return serialized(async () => {
|
|
206
|
+
const projects = await readRegistry();
|
|
207
|
+
const now = new Date().toISOString();
|
|
208
|
+
for (const { id, status } of updates) {
|
|
209
|
+
const idx = projects.findIndex((p) => p.id === id);
|
|
210
|
+
if (idx === -1)
|
|
211
|
+
continue;
|
|
212
|
+
projects[idx] = { ...projects[idx], healthStatus: status, healthCheckedAt: now };
|
|
213
|
+
}
|
|
214
|
+
await writeRegistry(projects);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// ── Per-project access control ──────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Get projects visible to a user.
|
|
220
|
+
* Admins see all. Others see owned + explicitly shared.
|
|
221
|
+
*/
|
|
222
|
+
export async function getProjectsForUser(username, globalRole) {
|
|
223
|
+
const projects = await readRegistry();
|
|
224
|
+
if (globalRole === 'admin')
|
|
225
|
+
return projects;
|
|
226
|
+
return projects.filter((p) => p.owner === username || p.access.some((a) => a.username === username));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if a user can access a project at the given role level.
|
|
230
|
+
* Returns the effective role or null if no access.
|
|
231
|
+
*/
|
|
232
|
+
export async function checkProjectAccess(projectId, username, globalRole) {
|
|
233
|
+
const project = await getProject(projectId);
|
|
234
|
+
if (!project)
|
|
235
|
+
return null;
|
|
236
|
+
// Global admins have full access
|
|
237
|
+
if (globalRole === 'admin')
|
|
238
|
+
return 'admin';
|
|
239
|
+
// Project owner has full access
|
|
240
|
+
if (project.owner === username)
|
|
241
|
+
return 'admin';
|
|
242
|
+
// Check access list
|
|
243
|
+
const entry = project.access.find((a) => a.username === username);
|
|
244
|
+
return entry?.role ?? null;
|
|
245
|
+
}
|
|
246
|
+
/** Grant access to a project for a user. Overwrites existing entry for that user. */
|
|
247
|
+
export function grantAccess(projectId, username, role) {
|
|
248
|
+
return serialized(async () => {
|
|
249
|
+
const projects = await readRegistry();
|
|
250
|
+
const idx = projects.findIndex((p) => p.id === projectId);
|
|
251
|
+
if (idx === -1)
|
|
252
|
+
throw new Error('Project not found');
|
|
253
|
+
const project = projects[idx];
|
|
254
|
+
// Remove existing entry for this user (if any)
|
|
255
|
+
project.access = project.access.filter((a) => a.username !== username);
|
|
256
|
+
project.access.push({ username, role });
|
|
257
|
+
await writeRegistry(projects);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** Revoke access from a project for a user. */
|
|
261
|
+
export function revokeAccess(projectId, username) {
|
|
262
|
+
return serialized(async () => {
|
|
263
|
+
const projects = await readRegistry();
|
|
264
|
+
const idx = projects.findIndex((p) => p.id === projectId);
|
|
265
|
+
if (idx === -1)
|
|
266
|
+
throw new Error('Project not found');
|
|
267
|
+
const project = projects[idx];
|
|
268
|
+
const before = project.access.length;
|
|
269
|
+
project.access = project.access.filter((a) => a.username !== username);
|
|
270
|
+
if (project.access.length === before)
|
|
271
|
+
throw new Error('User has no access to revoke');
|
|
272
|
+
await writeRegistry(projects);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Remove a user from all project access lists and clear ownership (cleanup on user deletion). */
|
|
276
|
+
export function removeUserFromAllProjects(username) {
|
|
277
|
+
return serialized(async () => {
|
|
278
|
+
const projects = await readRegistry();
|
|
279
|
+
let changedCount = 0;
|
|
280
|
+
for (const project of projects) {
|
|
281
|
+
const before = project.access.length;
|
|
282
|
+
project.access = project.access.filter((a) => a.username !== username);
|
|
283
|
+
if (project.access.length < before)
|
|
284
|
+
changedCount++;
|
|
285
|
+
// Clear ownership to prevent privilege escalation via username reuse
|
|
286
|
+
if (project.owner === username) {
|
|
287
|
+
project.owner = '';
|
|
288
|
+
changedCount++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (changedCount > 0)
|
|
292
|
+
await writeRegistry(projects);
|
|
293
|
+
return changedCount;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/** Get access list for a project. */
|
|
297
|
+
export async function getProjectAccess(projectId) {
|
|
298
|
+
const project = await getProject(projectId);
|
|
299
|
+
if (!project)
|
|
300
|
+
return null;
|
|
301
|
+
return { owner: project.owner, access: project.access };
|
|
302
|
+
}
|
|
303
|
+
// ── Linked services ─────────────────────────────────
|
|
304
|
+
/** Link two projects bidirectionally. */
|
|
305
|
+
export function linkProjects(projectIdA, projectIdB) {
|
|
306
|
+
return serialized(async () => {
|
|
307
|
+
if (projectIdA === projectIdB)
|
|
308
|
+
throw new Error('Cannot link a project to itself');
|
|
309
|
+
const projects = await readRegistry();
|
|
310
|
+
const a = projects.find((p) => p.id === projectIdA);
|
|
311
|
+
const b = projects.find((p) => p.id === projectIdB);
|
|
312
|
+
if (!a || !b)
|
|
313
|
+
throw new Error('Project not found');
|
|
314
|
+
if (!a.linkedProjects.includes(projectIdB))
|
|
315
|
+
a.linkedProjects.push(projectIdB);
|
|
316
|
+
if (!b.linkedProjects.includes(projectIdA))
|
|
317
|
+
b.linkedProjects.push(projectIdA);
|
|
318
|
+
await writeRegistry(projects);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** Unlink two projects bidirectionally. */
|
|
322
|
+
export function unlinkProjects(projectIdA, projectIdB) {
|
|
323
|
+
return serialized(async () => {
|
|
324
|
+
const projects = await readRegistry();
|
|
325
|
+
const a = projects.find((p) => p.id === projectIdA);
|
|
326
|
+
const b = projects.find((p) => p.id === projectIdB);
|
|
327
|
+
if (!a || !b)
|
|
328
|
+
throw new Error('Project not found');
|
|
329
|
+
a.linkedProjects = a.linkedProjects.filter((id) => id !== projectIdB);
|
|
330
|
+
b.linkedProjects = b.linkedProjects.filter((id) => id !== projectIdA);
|
|
331
|
+
await writeRegistry(projects);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/** Get all projects in the linked group (BFS traversal with cycle detection). */
|
|
335
|
+
export async function getLinkedGroup(projectId) {
|
|
336
|
+
const projects = await readRegistry();
|
|
337
|
+
const start = projects.find((p) => p.id === projectId);
|
|
338
|
+
if (!start)
|
|
339
|
+
return [];
|
|
340
|
+
// BFS to resolve transitive links: A→B→C means all three are in the group
|
|
341
|
+
const visited = new Set();
|
|
342
|
+
const queue = [start];
|
|
343
|
+
const group = [];
|
|
344
|
+
while (queue.length > 0) {
|
|
345
|
+
const current = queue.shift();
|
|
346
|
+
if (visited.has(current.id))
|
|
347
|
+
continue;
|
|
348
|
+
visited.add(current.id);
|
|
349
|
+
group.push(current);
|
|
350
|
+
for (const linkedId of current.linkedProjects) {
|
|
351
|
+
if (!visited.has(linkedId)) {
|
|
352
|
+
const linked = projects.find((p) => p.id === linkedId);
|
|
353
|
+
if (linked)
|
|
354
|
+
queue.push(linked);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return group;
|
|
359
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provision manifest — persists resource state to disk for crash recovery.
|
|
3
|
+
*
|
|
4
|
+
* Before each AWS resource creation, the intended action is recorded.
|
|
5
|
+
* After creation, the resource ID is written. On cleanup, the manifest
|
|
6
|
+
* is read and resources deleted in reverse. On wizard startup, incomplete
|
|
7
|
+
* manifests can be detected and cleaned up.
|
|
8
|
+
*
|
|
9
|
+
* Stored at ~/.voidforge/runs/<runId>.json
|
|
10
|
+
*/
|
|
11
|
+
import type { CreatedResource } from './provisioners/types.js';
|
|
12
|
+
export interface ManifestResource {
|
|
13
|
+
type: string;
|
|
14
|
+
id: string;
|
|
15
|
+
region: string;
|
|
16
|
+
status: 'pending' | 'created' | 'cleaned' | 'failed';
|
|
17
|
+
}
|
|
18
|
+
export interface ProvisionManifest {
|
|
19
|
+
runId: string;
|
|
20
|
+
startedAt: string;
|
|
21
|
+
target: string;
|
|
22
|
+
region: string;
|
|
23
|
+
projectName: string;
|
|
24
|
+
status: 'in-progress' | 'complete' | 'failed' | 'cleaned';
|
|
25
|
+
resources: ManifestResource[];
|
|
26
|
+
}
|
|
27
|
+
/** Create a new manifest for a provisioning run. */
|
|
28
|
+
export declare function createManifest(runId: string, target: string, region: string, projectName: string): Promise<ProvisionManifest>;
|
|
29
|
+
/** Record that a resource is about to be created (write-ahead). */
|
|
30
|
+
export declare function recordResourcePending(runId: string, type: string, id: string, region: string): Promise<void>;
|
|
31
|
+
/** Record that a resource was successfully created. */
|
|
32
|
+
export declare function recordResourceCreated(runId: string, type: string, id: string, region: string): Promise<void>;
|
|
33
|
+
/** Mark the overall run status. */
|
|
34
|
+
export declare function updateManifestStatus(runId: string, status: ProvisionManifest['status']): Promise<void>;
|
|
35
|
+
/** Mark a resource as cleaned up. */
|
|
36
|
+
export declare function recordResourceCleaned(runId: string, type: string, id: string): Promise<void>;
|
|
37
|
+
/** Read a manifest by run ID. Returns null if not found. */
|
|
38
|
+
export declare function readManifest(runId: string): Promise<ProvisionManifest | null>;
|
|
39
|
+
/** Delete a manifest file (after successful cleanup). */
|
|
40
|
+
export declare function deleteManifest(runId: string): Promise<void>;
|
|
41
|
+
/** List all incomplete manifests (for recovery on startup). */
|
|
42
|
+
export declare function listIncompleteRuns(): Promise<ProvisionManifest[]>;
|
|
43
|
+
/** Convert manifest resources to the CreatedResource[] format used by provisioners. */
|
|
44
|
+
export declare function manifestToCreatedResources(manifest: ProvisionManifest): CreatedResource[];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provision manifest — persists resource state to disk for crash recovery.
|
|
3
|
+
*
|
|
4
|
+
* Before each AWS resource creation, the intended action is recorded.
|
|
5
|
+
* After creation, the resource ID is written. On cleanup, the manifest
|
|
6
|
+
* is read and resources deleted in reverse. On wizard startup, incomplete
|
|
7
|
+
* manifests can be detected and cleaned up.
|
|
8
|
+
*
|
|
9
|
+
* Stored at ~/.voidforge/runs/<runId>.json
|
|
10
|
+
*/
|
|
11
|
+
import { readFile, readdir, unlink, mkdir, open, rename } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
const RUNS_DIR = join(homedir(), '.voidforge', 'runs');
|
|
16
|
+
/** QA-R2-004: Write queue to serialize manifest mutations and prevent race conditions */
|
|
17
|
+
let writeQueue = Promise.resolve();
|
|
18
|
+
function serialized(fn) {
|
|
19
|
+
const result = writeQueue.then(fn, () => fn());
|
|
20
|
+
writeQueue = result.then(() => { }, () => { });
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
// IG-R4 LOKI-003: Atomic write with fsync — crash-recovery manifests must survive crashes
|
|
24
|
+
async function atomicWriteManifest(runId, manifest) {
|
|
25
|
+
const filePath = manifestPath(runId);
|
|
26
|
+
const tmpPath = filePath + '.tmp';
|
|
27
|
+
const fh = await open(tmpPath, 'w', 0o600);
|
|
28
|
+
try {
|
|
29
|
+
await fh.writeFile(JSON.stringify(manifest, null, 2));
|
|
30
|
+
await fh.sync();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await fh.close();
|
|
34
|
+
}
|
|
35
|
+
await rename(tmpPath, filePath);
|
|
36
|
+
}
|
|
37
|
+
async function ensureDir() {
|
|
38
|
+
await mkdir(RUNS_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
// SEC-R3-013: Validate runId is a UUID to prevent path traversal
|
|
41
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
42
|
+
function manifestPath(runId) {
|
|
43
|
+
if (!UUID_RE.test(runId))
|
|
44
|
+
throw new Error('Invalid runId format');
|
|
45
|
+
return join(RUNS_DIR, `${runId}.json`);
|
|
46
|
+
}
|
|
47
|
+
/** Create a new manifest for a provisioning run. */
|
|
48
|
+
export function createManifest(runId, target, region, projectName) {
|
|
49
|
+
// QA-R3-004: Wrap in serialized() for consistency with other mutation functions
|
|
50
|
+
return serialized(async () => {
|
|
51
|
+
await ensureDir();
|
|
52
|
+
const manifest = {
|
|
53
|
+
runId,
|
|
54
|
+
startedAt: new Date().toISOString(),
|
|
55
|
+
target,
|
|
56
|
+
region,
|
|
57
|
+
projectName,
|
|
58
|
+
status: 'in-progress',
|
|
59
|
+
resources: [],
|
|
60
|
+
};
|
|
61
|
+
await atomicWriteManifest(runId, manifest);
|
|
62
|
+
return manifest;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Record that a resource is about to be created (write-ahead). */
|
|
66
|
+
export function recordResourcePending(runId, type, id, region) {
|
|
67
|
+
return serialized(async () => {
|
|
68
|
+
const manifest = await readManifest(runId);
|
|
69
|
+
if (!manifest)
|
|
70
|
+
return;
|
|
71
|
+
manifest.resources.push({ type, id, region, status: 'pending' });
|
|
72
|
+
await atomicWriteManifest(runId, manifest);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Record that a resource was successfully created. */
|
|
76
|
+
export function recordResourceCreated(runId, type, id, region) {
|
|
77
|
+
return serialized(async () => {
|
|
78
|
+
const manifest = await readManifest(runId);
|
|
79
|
+
if (!manifest)
|
|
80
|
+
return;
|
|
81
|
+
const existing = manifest.resources.find((r) => r.type === type && r.id === id);
|
|
82
|
+
if (existing) {
|
|
83
|
+
existing.status = 'created';
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
manifest.resources.push({ type, id, region, status: 'created' });
|
|
87
|
+
}
|
|
88
|
+
await atomicWriteManifest(runId, manifest);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Mark the overall run status. */
|
|
92
|
+
export function updateManifestStatus(runId, status) {
|
|
93
|
+
return serialized(async () => {
|
|
94
|
+
const manifest = await readManifest(runId);
|
|
95
|
+
if (!manifest)
|
|
96
|
+
return;
|
|
97
|
+
manifest.status = status;
|
|
98
|
+
await atomicWriteManifest(runId, manifest);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Mark a resource as cleaned up. */
|
|
102
|
+
export function recordResourceCleaned(runId, type, id) {
|
|
103
|
+
return serialized(async () => {
|
|
104
|
+
const manifest = await readManifest(runId);
|
|
105
|
+
if (!manifest)
|
|
106
|
+
return;
|
|
107
|
+
const resource = manifest.resources.find((r) => r.type === type && r.id === id);
|
|
108
|
+
if (resource)
|
|
109
|
+
resource.status = 'cleaned';
|
|
110
|
+
await atomicWriteManifest(runId, manifest);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Read a manifest by run ID. Returns null if not found. */
|
|
114
|
+
export async function readManifest(runId) {
|
|
115
|
+
const path = manifestPath(runId);
|
|
116
|
+
if (!existsSync(path))
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
const raw = await readFile(path, 'utf-8');
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Delete a manifest file (after successful cleanup). */
|
|
127
|
+
export async function deleteManifest(runId) {
|
|
128
|
+
const path = manifestPath(runId);
|
|
129
|
+
try {
|
|
130
|
+
await unlink(path);
|
|
131
|
+
}
|
|
132
|
+
catch { /* already gone */ }
|
|
133
|
+
}
|
|
134
|
+
/** List all incomplete manifests (for recovery on startup). */
|
|
135
|
+
export async function listIncompleteRuns() {
|
|
136
|
+
await ensureDir();
|
|
137
|
+
const incomplete = [];
|
|
138
|
+
try {
|
|
139
|
+
const files = await readdir(RUNS_DIR);
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
if (!file.endsWith('.json'))
|
|
142
|
+
continue;
|
|
143
|
+
try {
|
|
144
|
+
const raw = await readFile(join(RUNS_DIR, file), 'utf-8');
|
|
145
|
+
const manifest = JSON.parse(raw);
|
|
146
|
+
if (manifest.status === 'in-progress' || manifest.status === 'failed') {
|
|
147
|
+
const hasCreatedResources = manifest.resources.some((r) => r.status === 'created');
|
|
148
|
+
if (hasCreatedResources) {
|
|
149
|
+
incomplete.push(manifest);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* skip corrupt files */ }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch { /* directory might not exist */ }
|
|
157
|
+
return incomplete;
|
|
158
|
+
}
|
|
159
|
+
/** Convert manifest resources to the CreatedResource[] format used by provisioners. */
|
|
160
|
+
export function manifestToCreatedResources(manifest) {
|
|
161
|
+
return manifest.resources
|
|
162
|
+
.filter((r) => r.status === 'created')
|
|
163
|
+
.map((r) => ({ type: r.type, id: r.id, region: r.region }));
|
|
164
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provisioner Registry — single source of truth for provisioner map,
|
|
3
|
+
* credential scoping, and GitHub-linked target constants.
|
|
4
|
+
*
|
|
5
|
+
* ARCH-R2-002: Extracted from provision.ts + headless-deploy.ts to prevent drift.
|
|
6
|
+
*/
|
|
7
|
+
import type { Provisioner } from './provisioners/types.js';
|
|
8
|
+
/** All available provisioners keyed by deploy target name. */
|
|
9
|
+
export declare const provisioners: Record<string, Provisioner>;
|
|
10
|
+
/** Credential scoping — each provisioner only receives vault keys it needs (ADR-020). */
|
|
11
|
+
export declare const provisionKeys: Record<string, string[]>;
|
|
12
|
+
/** Deploy targets that benefit from GitHub repo linking (ADR-015). */
|
|
13
|
+
export declare const GITHUB_LINKED_TARGETS: string[];
|
|
14
|
+
/** Deploy targets where GitHub push is optional (deploy via SSH/SDK instead). */
|
|
15
|
+
export declare const GITHUB_OPTIONAL_TARGETS: string[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provisioner Registry — single source of truth for provisioner map,
|
|
3
|
+
* credential scoping, and GitHub-linked target constants.
|
|
4
|
+
*
|
|
5
|
+
* ARCH-R2-002: Extracted from provision.ts + headless-deploy.ts to prevent drift.
|
|
6
|
+
*/
|
|
7
|
+
import { dockerProvisioner } from './provisioners/docker.js';
|
|
8
|
+
import { awsVpsProvisioner } from './provisioners/aws-vps.js';
|
|
9
|
+
import { vercelProvisioner } from './provisioners/vercel.js';
|
|
10
|
+
import { railwayProvisioner } from './provisioners/railway.js';
|
|
11
|
+
import { cloudflareProvisioner } from './provisioners/cloudflare.js';
|
|
12
|
+
import { staticS3Provisioner } from './provisioners/static-s3.js';
|
|
13
|
+
/** All available provisioners keyed by deploy target name. */
|
|
14
|
+
export const provisioners = {
|
|
15
|
+
docker: dockerProvisioner,
|
|
16
|
+
vps: awsVpsProvisioner,
|
|
17
|
+
vercel: vercelProvisioner,
|
|
18
|
+
railway: railwayProvisioner,
|
|
19
|
+
cloudflare: cloudflareProvisioner,
|
|
20
|
+
static: staticS3Provisioner,
|
|
21
|
+
};
|
|
22
|
+
/** Credential scoping — each provisioner only receives vault keys it needs (ADR-020). */
|
|
23
|
+
export const provisionKeys = {
|
|
24
|
+
vps: ['aws-access-key-id', 'aws-secret-access-key', 'aws-region'],
|
|
25
|
+
static: ['aws-access-key-id', 'aws-secret-access-key', 'aws-region'],
|
|
26
|
+
vercel: ['vercel-token'],
|
|
27
|
+
railway: ['railway-token'],
|
|
28
|
+
cloudflare: ['cloudflare-api-token', 'cloudflare-account-id'],
|
|
29
|
+
docker: [],
|
|
30
|
+
};
|
|
31
|
+
/** Deploy targets that benefit from GitHub repo linking (ADR-015). */
|
|
32
|
+
export const GITHUB_LINKED_TARGETS = ['vercel', 'cloudflare', 'railway'];
|
|
33
|
+
/** Deploy targets where GitHub push is optional (deploy via SSH/SDK instead). */
|
|
34
|
+
export const GITHUB_OPTIONAL_TARGETS = ['vps', 'static'];
|