@treeseed/cli 0.1.1 → 0.4.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/README.md +27 -26
- package/dist/cli/handlers/auth-login.d.ts +2 -0
- package/dist/cli/handlers/auth-login.js +67 -0
- package/dist/cli/handlers/auth-logout.d.ts +2 -0
- package/dist/cli/handlers/auth-logout.js +20 -0
- package/dist/cli/handlers/auth-whoami.d.ts +2 -0
- package/dist/cli/handlers/auth-whoami.js +24 -0
- package/dist/cli/handlers/close.js +19 -53
- package/dist/cli/handlers/config.js +33 -53
- package/dist/cli/handlers/destroy.js +34 -79
- package/dist/{src/cli/handlers/ship.d.ts → cli/handlers/dev.d.ts} +1 -1
- package/dist/cli/handlers/dev.js +19 -0
- package/dist/cli/handlers/doctor.js +13 -6
- package/dist/cli/handlers/init.js +32 -8
- package/dist/cli/handlers/release.js +21 -53
- package/dist/cli/handlers/rollback.js +8 -8
- package/dist/cli/handlers/save.js +21 -79
- package/dist/cli/handlers/stage.d.ts +2 -0
- package/dist/cli/handlers/stage.js +28 -0
- package/dist/cli/handlers/status.js +35 -26
- package/dist/{src/cli/handlers/deploy.d.ts → cli/handlers/switch.d.ts} +1 -1
- package/dist/cli/handlers/switch.js +29 -0
- package/dist/{src/cli/handlers/next.d.ts → cli/handlers/sync.d.ts} +1 -1
- package/dist/cli/handlers/sync.js +26 -0
- package/dist/cli/handlers/tasks.d.ts +2 -0
- package/dist/cli/handlers/tasks.js +31 -0
- package/dist/cli/handlers/template.d.ts +2 -0
- package/dist/cli/handlers/template.js +27 -0
- package/dist/cli/handlers/workflow.d.ts +6 -0
- package/dist/cli/handlers/workflow.js +71 -0
- package/dist/{src/cli → cli}/help.d.ts +2 -2
- package/dist/cli/help.js +36 -24
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +14 -19
- package/dist/cli/operations-help.d.ts +1 -0
- package/dist/cli/operations-help.js +1 -0
- package/dist/cli/operations-parser.d.ts +1 -0
- package/dist/cli/operations-parser.js +1 -0
- package/dist/cli/operations-registry.d.ts +5 -0
- package/dist/cli/operations-registry.js +260 -0
- package/dist/cli/operations-types.d.ts +72 -0
- package/dist/cli/parser.d.ts +3 -0
- package/dist/cli/parser.js +1 -6
- package/dist/cli/registry.d.ts +25 -0
- package/dist/cli/registry.js +28 -416
- package/dist/cli/repair.js +6 -4
- package/dist/cli/runtime.d.ts +31 -0
- package/dist/cli/runtime.js +240 -111
- package/dist/cli/types.d.ts +1 -0
- package/dist/{src/cli → cli}/workflow-state.d.ts +9 -0
- package/dist/cli/workflow-state.js +45 -21
- package/package.json +13 -13
- package/dist/cli/handlers/continue.js +0 -23
- package/dist/cli/handlers/deploy.js +0 -139
- package/dist/cli/handlers/next.js +0 -27
- package/dist/cli/handlers/prepare.js +0 -8
- package/dist/cli/handlers/promote.js +0 -8
- package/dist/cli/handlers/publish.js +0 -8
- package/dist/cli/handlers/setup.js +0 -48
- package/dist/cli/handlers/ship.js +0 -49
- package/dist/cli/handlers/start.js +0 -97
- package/dist/cli/handlers/teardown.js +0 -50
- package/dist/cli/handlers/work.js +0 -85
- package/dist/scripts/aggregate-book.d.ts +0 -1
- package/dist/scripts/aggregate-book.js +0 -121
- package/dist/scripts/assert-release-tag-version.d.ts +0 -1
- package/dist/scripts/assert-release-tag-version.js +0 -21
- package/dist/scripts/build-dist.d.ts +0 -1
- package/dist/scripts/build-dist.js +0 -108
- package/dist/scripts/build-tenant-worker.d.ts +0 -1
- package/dist/scripts/build-tenant-worker.js +0 -36
- package/dist/scripts/cleanup-markdown.d.ts +0 -2
- package/dist/scripts/cleanup-markdown.js +0 -373
- package/dist/scripts/config-runtime-lib.d.ts +0 -122
- package/dist/scripts/config-runtime-lib.js +0 -505
- package/dist/scripts/config-treeseed.d.ts +0 -2
- package/dist/scripts/config-treeseed.js +0 -81
- package/dist/scripts/d1-migration-lib.d.ts +0 -6
- package/dist/scripts/d1-migration-lib.js +0 -90
- package/dist/scripts/deploy-lib.d.ts +0 -127
- package/dist/scripts/deploy-lib.js +0 -841
- package/dist/scripts/ensure-mailpit.d.ts +0 -1
- package/dist/scripts/ensure-mailpit.js +0 -29
- package/dist/scripts/git-workflow-lib.d.ts +0 -25
- package/dist/scripts/git-workflow-lib.js +0 -136
- package/dist/scripts/github-automation-lib.d.ts +0 -156
- package/dist/scripts/github-automation-lib.js +0 -242
- package/dist/scripts/local-dev-lib.d.ts +0 -9
- package/dist/scripts/local-dev-lib.js +0 -84
- package/dist/scripts/local-dev.d.ts +0 -1
- package/dist/scripts/local-dev.js +0 -129
- package/dist/scripts/logs-mailpit.d.ts +0 -1
- package/dist/scripts/logs-mailpit.js +0 -2
- package/dist/scripts/mailpit-runtime.d.ts +0 -4
- package/dist/scripts/mailpit-runtime.js +0 -57
- package/dist/scripts/package-tools.d.ts +0 -22
- package/dist/scripts/package-tools.js +0 -255
- package/dist/scripts/patch-starlight-content-path.d.ts +0 -1
- package/dist/scripts/patch-starlight-content-path.js +0 -172
- package/dist/scripts/paths.d.ts +0 -17
- package/dist/scripts/paths.js +0 -26
- package/dist/scripts/publish-package.d.ts +0 -1
- package/dist/scripts/publish-package.js +0 -19
- package/dist/scripts/release-verify.d.ts +0 -1
- package/dist/scripts/release-verify.js +0 -136
- package/dist/scripts/run-fixture-astro-command.d.ts +0 -1
- package/dist/scripts/run-fixture-astro-command.js +0 -18
- package/dist/scripts/save-deploy-preflight-lib.d.ts +0 -34
- package/dist/scripts/save-deploy-preflight-lib.js +0 -69
- package/dist/scripts/scaffold-site.d.ts +0 -2
- package/dist/scripts/scaffold-site.js +0 -92
- package/dist/scripts/stop-mailpit.d.ts +0 -1
- package/dist/scripts/stop-mailpit.js +0 -5
- package/dist/scripts/sync-dev-vars.d.ts +0 -1
- package/dist/scripts/sync-dev-vars.js +0 -6
- package/dist/scripts/template-registry-lib.d.ts +0 -47
- package/dist/scripts/template-registry-lib.js +0 -137
- package/dist/scripts/tenant-astro-command.d.ts +0 -1
- package/dist/scripts/tenant-astro-command.js +0 -3
- package/dist/scripts/tenant-build.d.ts +0 -1
- package/dist/scripts/tenant-build.js +0 -16
- package/dist/scripts/tenant-check.d.ts +0 -1
- package/dist/scripts/tenant-check.js +0 -7
- package/dist/scripts/tenant-d1-migrate-local.d.ts +0 -1
- package/dist/scripts/tenant-d1-migrate-local.js +0 -11
- package/dist/scripts/tenant-deploy.d.ts +0 -2
- package/dist/scripts/tenant-deploy.js +0 -180
- package/dist/scripts/tenant-destroy.d.ts +0 -2
- package/dist/scripts/tenant-destroy.js +0 -104
- package/dist/scripts/tenant-dev.d.ts +0 -1
- package/dist/scripts/tenant-dev.js +0 -171
- package/dist/scripts/tenant-lint.d.ts +0 -1
- package/dist/scripts/tenant-lint.js +0 -4
- package/dist/scripts/tenant-test.d.ts +0 -1
- package/dist/scripts/tenant-test.js +0 -4
- package/dist/scripts/test-cloudflare-local.d.ts +0 -1
- package/dist/scripts/test-cloudflare-local.js +0 -212
- package/dist/scripts/test-scaffold.d.ts +0 -2
- package/dist/scripts/test-scaffold.js +0 -297
- package/dist/scripts/treeseed.d.ts +0 -2
- package/dist/scripts/treeseed.js +0 -4
- package/dist/scripts/validate-templates.d.ts +0 -2
- package/dist/scripts/validate-templates.js +0 -4
- package/dist/scripts/watch-dev-lib.d.ts +0 -21
- package/dist/scripts/watch-dev-lib.js +0 -277
- package/dist/scripts/workspace-close.d.ts +0 -2
- package/dist/scripts/workspace-close.js +0 -24
- package/dist/scripts/workspace-command-e2e.d.ts +0 -2
- package/dist/scripts/workspace-command-e2e.js +0 -718
- package/dist/scripts/workspace-lint.d.ts +0 -1
- package/dist/scripts/workspace-lint.js +0 -9
- package/dist/scripts/workspace-preflight-lib.d.ts +0 -36
- package/dist/scripts/workspace-preflight-lib.js +0 -179
- package/dist/scripts/workspace-preflight.d.ts +0 -2
- package/dist/scripts/workspace-preflight.js +0 -22
- package/dist/scripts/workspace-publish-changed-packages.d.ts +0 -1
- package/dist/scripts/workspace-publish-changed-packages.js +0 -16
- package/dist/scripts/workspace-release-verify.d.ts +0 -1
- package/dist/scripts/workspace-release-verify.js +0 -81
- package/dist/scripts/workspace-release.d.ts +0 -2
- package/dist/scripts/workspace-release.js +0 -42
- package/dist/scripts/workspace-save-lib.d.ts +0 -42
- package/dist/scripts/workspace-save-lib.js +0 -220
- package/dist/scripts/workspace-save.d.ts +0 -2
- package/dist/scripts/workspace-save.js +0 -124
- package/dist/scripts/workspace-start-warning.js +0 -3
- package/dist/scripts/workspace-start.d.ts +0 -2
- package/dist/scripts/workspace-start.js +0 -71
- package/dist/scripts/workspace-test-unit.d.ts +0 -1
- package/dist/scripts/workspace-test-unit.js +0 -4
- package/dist/scripts/workspace-test.d.ts +0 -1
- package/dist/scripts/workspace-test.js +0 -11
- package/dist/scripts/workspace-tools.d.ts +0 -13
- package/dist/scripts/workspace-tools.js +0 -226
- package/dist/src/cli/handlers/continue.d.ts +0 -2
- package/dist/src/cli/handlers/prepare.d.ts +0 -2
- package/dist/src/cli/handlers/promote.d.ts +0 -2
- package/dist/src/cli/handlers/publish.d.ts +0 -2
- package/dist/src/cli/handlers/setup.d.ts +0 -2
- package/dist/src/cli/handlers/start.d.ts +0 -3
- package/dist/src/cli/handlers/teardown.d.ts +0 -2
- package/dist/src/cli/handlers/work.d.ts +0 -2
- package/dist/src/cli/main.d.ts +0 -6
- package/dist/src/cli/parser.d.ts +0 -3
- package/dist/src/cli/registry.d.ts +0 -27
- package/dist/src/cli/runtime.d.ts +0 -4
- package/dist/src/cli/types.d.ts +0 -71
- /package/dist/{src/cli → cli}/handlers/close.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/config.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/destroy.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/doctor.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/init.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/release.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/rollback.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/save.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/status.d.ts +0 -0
- /package/dist/{src/cli → cli}/handlers/utils.d.ts +0 -0
- /package/dist/{scripts/workspace-start-warning.d.ts → cli/operations-types.js} +0 -0
- /package/dist/{src/cli → cli}/repair.d.ts +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
|
@@ -1,841 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, relative, resolve } from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { createInterface } from 'node:readline/promises';
|
|
6
|
-
import { deriveCloudflareWorkerName } from '@treeseed/core/deploy/config';
|
|
7
|
-
import { loadCliDeployConfig, resolveWranglerBin } from './package-tools.js';
|
|
8
|
-
const DEFAULT_COMPATIBILITY_DATE = '2026-04-05';
|
|
9
|
-
const DEFAULT_COMPATIBILITY_FLAGS = ['nodejs_compat'];
|
|
10
|
-
const GENERATED_ROOT = '.treeseed/generated';
|
|
11
|
-
const STATE_ROOT = '.treeseed/state';
|
|
12
|
-
const PERSISTENT_SCOPES = new Set(['local', 'staging', 'prod']);
|
|
13
|
-
const TRESEED_ENVELOPE_SCHEMA_GENERATION = 'runtime-envelopes-v1';
|
|
14
|
-
const TRESEED_MIGRATION_WAVE_ID = '0005_runtime_envelopes';
|
|
15
|
-
const TRESEED_SUPPORTED_PAYLOAD_RANGE = { min: 1, max: 1 };
|
|
16
|
-
function ensureParent(filePath) {
|
|
17
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
function stableHash(value) {
|
|
20
|
-
return createHash('sha256').update(value).digest('hex');
|
|
21
|
-
}
|
|
22
|
-
function readJson(filePath, fallback) {
|
|
23
|
-
if (!existsSync(filePath)) {
|
|
24
|
-
return fallback;
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return fallback;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function writeJson(filePath, value) {
|
|
34
|
-
ensureParent(filePath);
|
|
35
|
-
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
36
|
-
}
|
|
37
|
-
function renderTomlString(value) {
|
|
38
|
-
return JSON.stringify(String(value));
|
|
39
|
-
}
|
|
40
|
-
function envOrNull(key) {
|
|
41
|
-
const value = process.env[key];
|
|
42
|
-
return typeof value === 'string' && value.length ? value : null;
|
|
43
|
-
}
|
|
44
|
-
function loadTenantDeployConfig(tenantRoot) {
|
|
45
|
-
return loadCliDeployConfig(tenantRoot);
|
|
46
|
-
}
|
|
47
|
-
function sanitizeSegment(value) {
|
|
48
|
-
return String(value)
|
|
49
|
-
.trim()
|
|
50
|
-
.toLowerCase()
|
|
51
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
52
|
-
.replace(/^-+|-+$/g, '')
|
|
53
|
-
.replace(/-{2,}/g, '-')
|
|
54
|
-
.slice(0, 36) || 'default';
|
|
55
|
-
}
|
|
56
|
-
export function normalizePersistentScope(scope = 'prod') {
|
|
57
|
-
if (!PERSISTENT_SCOPES.has(scope)) {
|
|
58
|
-
throw new Error(`Unsupported Treeseed environment "${scope}". Expected one of local, staging, prod.`);
|
|
59
|
-
}
|
|
60
|
-
return scope;
|
|
61
|
-
}
|
|
62
|
-
export function createPersistentDeployTarget(scope = 'prod') {
|
|
63
|
-
return {
|
|
64
|
-
kind: 'persistent',
|
|
65
|
-
scope: normalizePersistentScope(scope),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
export function createBranchPreviewDeployTarget(branchName) {
|
|
69
|
-
const normalized = String(branchName ?? '').trim();
|
|
70
|
-
if (!normalized) {
|
|
71
|
-
throw new Error('Branch preview target requires a branch name.');
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
kind: 'branch',
|
|
75
|
-
branchName: normalized,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function normalizeTarget(scopeOrTarget = 'prod') {
|
|
79
|
-
if (!scopeOrTarget || typeof scopeOrTarget === 'string') {
|
|
80
|
-
return createPersistentDeployTarget(scopeOrTarget ?? 'prod');
|
|
81
|
-
}
|
|
82
|
-
if (scopeOrTarget.kind === 'persistent') {
|
|
83
|
-
return createPersistentDeployTarget(scopeOrTarget.scope);
|
|
84
|
-
}
|
|
85
|
-
if (scopeOrTarget.kind === 'branch') {
|
|
86
|
-
return createBranchPreviewDeployTarget(scopeOrTarget.branchName);
|
|
87
|
-
}
|
|
88
|
-
throw new Error('Unsupported Treeseed deployment target.');
|
|
89
|
-
}
|
|
90
|
-
export function scopeFromTarget(target) {
|
|
91
|
-
return target.kind === 'persistent'
|
|
92
|
-
? target.scope
|
|
93
|
-
: 'staging';
|
|
94
|
-
}
|
|
95
|
-
function targetDirectoryParts(target) {
|
|
96
|
-
if (target.kind === 'persistent') {
|
|
97
|
-
return ['environments', target.scope];
|
|
98
|
-
}
|
|
99
|
-
return ['branches', sanitizeSegment(target.branchName)];
|
|
100
|
-
}
|
|
101
|
-
function targetKey(target) {
|
|
102
|
-
return target.kind === 'persistent'
|
|
103
|
-
? target.scope
|
|
104
|
-
: `branch:${target.branchName}`;
|
|
105
|
-
}
|
|
106
|
-
function resolveTargetPaths(tenantRoot, scopeOrTarget = 'prod') {
|
|
107
|
-
const target = normalizeTarget(scopeOrTarget);
|
|
108
|
-
const pathParts = targetDirectoryParts(target);
|
|
109
|
-
const generatedRoot = resolve(tenantRoot, GENERATED_ROOT, ...pathParts);
|
|
110
|
-
const statePath = resolve(tenantRoot, STATE_ROOT, ...pathParts, 'deploy.json');
|
|
111
|
-
return {
|
|
112
|
-
target,
|
|
113
|
-
generatedRoot,
|
|
114
|
-
wranglerPath: resolve(generatedRoot, 'wrangler.toml'),
|
|
115
|
-
workerEntryPath: resolve(generatedRoot, 'worker/index.js'),
|
|
116
|
-
statePath,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
export function deployTargetLabel(scopeOrTarget = 'prod') {
|
|
120
|
-
const target = normalizeTarget(scopeOrTarget);
|
|
121
|
-
return target.kind === 'persistent' ? target.scope : `branch:${target.branchName}`;
|
|
122
|
-
}
|
|
123
|
-
function targetWorkerName(deployConfig, target) {
|
|
124
|
-
const baseName = deriveCloudflareWorkerName(deployConfig);
|
|
125
|
-
if (target.kind === 'persistent') {
|
|
126
|
-
if (target.scope === 'prod') {
|
|
127
|
-
return baseName;
|
|
128
|
-
}
|
|
129
|
-
return `${baseName}-${target.scope}`;
|
|
130
|
-
}
|
|
131
|
-
return `${baseName}-${sanitizeSegment(target.branchName)}`;
|
|
132
|
-
}
|
|
133
|
-
function targetWorkersDevUrl(workerName) {
|
|
134
|
-
return `https://${workerName}.workers.dev`;
|
|
135
|
-
}
|
|
136
|
-
function relativeFromGeneratedRoot(targetPath, generatedRoot) {
|
|
137
|
-
return relative(generatedRoot, targetPath).replaceAll('\\', '/');
|
|
138
|
-
}
|
|
139
|
-
function buildPublicVars(deployConfig) {
|
|
140
|
-
return {
|
|
141
|
-
TREESEED_AGENT_EXECUTION_PROVIDER: deployConfig.providers?.agents?.execution ?? 'stub',
|
|
142
|
-
TREESEED_AGENT_REPOSITORY_PROVIDER: deployConfig.providers?.agents?.repository ?? 'stub',
|
|
143
|
-
TREESEED_AGENT_VERIFICATION_PROVIDER: deployConfig.providers?.agents?.verification ?? 'stub',
|
|
144
|
-
TREESEED_PUBLIC_TURNSTILE_SITE_KEY: envOrNull('TREESEED_PUBLIC_TURNSTILE_SITE_KEY') ?? '',
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function buildSecretMap(deployConfig, state) {
|
|
148
|
-
const generatedSecret = state.generatedSecrets?.TREESEED_FORM_TOKEN_SECRET ?? randomBytes(24).toString('hex');
|
|
149
|
-
return {
|
|
150
|
-
TREESEED_FORM_TOKEN_SECRET: envOrNull('TREESEED_FORM_TOKEN_SECRET') ?? generatedSecret,
|
|
151
|
-
TREESEED_TURNSTILE_SECRET_KEY: envOrNull('TREESEED_TURNSTILE_SECRET_KEY'),
|
|
152
|
-
TREESEED_SMTP_HOST: deployConfig.smtp?.enabled ? envOrNull('TREESEED_SMTP_HOST') : null,
|
|
153
|
-
TREESEED_SMTP_PORT: deployConfig.smtp?.enabled ? envOrNull('TREESEED_SMTP_PORT') : null,
|
|
154
|
-
TREESEED_SMTP_USERNAME: deployConfig.smtp?.enabled ? envOrNull('TREESEED_SMTP_USERNAME') : null,
|
|
155
|
-
TREESEED_SMTP_PASSWORD: deployConfig.smtp?.enabled ? envOrNull('TREESEED_SMTP_PASSWORD') : null,
|
|
156
|
-
TREESEED_SMTP_FROM: deployConfig.smtp?.enabled ? envOrNull('TREESEED_SMTP_FROM') : null,
|
|
157
|
-
TREESEED_SMTP_REPLY_TO: deployConfig.smtp?.enabled ? envOrNull('TREESEED_SMTP_REPLY_TO') : null,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
function defaultStateFromConfig(deployConfig, target) {
|
|
161
|
-
const workerName = targetWorkerName(deployConfig, target);
|
|
162
|
-
const suffix = target.kind === 'persistent' ? target.scope : sanitizeSegment(target.branchName);
|
|
163
|
-
return {
|
|
164
|
-
version: 2,
|
|
165
|
-
target,
|
|
166
|
-
previewEnabled: target.kind === 'branch',
|
|
167
|
-
workerName,
|
|
168
|
-
kvNamespaces: {
|
|
169
|
-
FORM_GUARD_KV: {
|
|
170
|
-
name: `${workerName}-form-guard`,
|
|
171
|
-
id: `dryrun-${suffix}-form-guard`,
|
|
172
|
-
previewId: `dryrun-${suffix}-form-guard-preview`,
|
|
173
|
-
},
|
|
174
|
-
SESSION: {
|
|
175
|
-
name: `${workerName}-session`,
|
|
176
|
-
id: `dryrun-${suffix}-session`,
|
|
177
|
-
previewId: `dryrun-${suffix}-session-preview`,
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
d1Databases: {
|
|
181
|
-
SITE_DATA_DB: {
|
|
182
|
-
databaseName: `${workerName}-site-data`,
|
|
183
|
-
databaseId: `dryrun-${suffix}-site-data`,
|
|
184
|
-
previewDatabaseId: `dryrun-${suffix}-site-data-preview`,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
generatedSecrets: {},
|
|
188
|
-
readiness: {
|
|
189
|
-
initialized: false,
|
|
190
|
-
initializedAt: null,
|
|
191
|
-
lastValidatedAt: null,
|
|
192
|
-
lastConfigFingerprint: null,
|
|
193
|
-
},
|
|
194
|
-
lastDeployedUrl: target.kind === 'branch' ? targetWorkersDevUrl(workerName) : null,
|
|
195
|
-
lastManifestFingerprint: null,
|
|
196
|
-
lastDeploymentTimestamp: null,
|
|
197
|
-
lastDeployedCommit: null,
|
|
198
|
-
runtimeCompatibility: {
|
|
199
|
-
envelopeSchemaGeneration: TRESEED_ENVELOPE_SCHEMA_GENERATION,
|
|
200
|
-
migrationWaveId: TRESEED_MIGRATION_WAVE_ID,
|
|
201
|
-
supportedPayloadVersionRange: TRESEED_SUPPORTED_PAYLOAD_RANGE,
|
|
202
|
-
},
|
|
203
|
-
deploymentHistory: [],
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
export function loadDeployState(tenantRoot, deployConfig, options = {}) {
|
|
207
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
208
|
-
const defaults = defaultStateFromConfig(deployConfig, target);
|
|
209
|
-
const { statePath } = resolveTargetPaths(tenantRoot, target);
|
|
210
|
-
const persisted = readJson(statePath, {});
|
|
211
|
-
const persistedSiteDataDb = persisted.d1Databases?.SITE_DATA_DB ?? persisted.d1Databases?.SUBSCRIBERS_DB ?? {};
|
|
212
|
-
const merged = {
|
|
213
|
-
...defaults,
|
|
214
|
-
...persisted,
|
|
215
|
-
target,
|
|
216
|
-
previewEnabled: persisted.previewEnabled ?? defaults.previewEnabled,
|
|
217
|
-
workerName: defaults.workerName,
|
|
218
|
-
kvNamespaces: {
|
|
219
|
-
...defaults.kvNamespaces,
|
|
220
|
-
...(persisted.kvNamespaces ?? {}),
|
|
221
|
-
FORM_GUARD_KV: {
|
|
222
|
-
...defaults.kvNamespaces.FORM_GUARD_KV,
|
|
223
|
-
...(persisted.kvNamespaces?.FORM_GUARD_KV ?? {}),
|
|
224
|
-
name: defaults.kvNamespaces.FORM_GUARD_KV.name,
|
|
225
|
-
},
|
|
226
|
-
SESSION: {
|
|
227
|
-
...defaults.kvNamespaces.SESSION,
|
|
228
|
-
...(persisted.kvNamespaces?.SESSION ?? {}),
|
|
229
|
-
name: defaults.kvNamespaces.SESSION.name,
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
d1Databases: {
|
|
233
|
-
...defaults.d1Databases,
|
|
234
|
-
...(persisted.d1Databases ?? {}),
|
|
235
|
-
SITE_DATA_DB: {
|
|
236
|
-
...defaults.d1Databases.SITE_DATA_DB,
|
|
237
|
-
...persistedSiteDataDb,
|
|
238
|
-
databaseName: defaults.d1Databases.SITE_DATA_DB.databaseName,
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
generatedSecrets: {
|
|
242
|
-
...(defaults.generatedSecrets ?? {}),
|
|
243
|
-
...(persisted.generatedSecrets ?? {}),
|
|
244
|
-
},
|
|
245
|
-
readiness: {
|
|
246
|
-
...defaults.readiness,
|
|
247
|
-
...(persisted.readiness ?? {}),
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
if (target.kind === 'branch' && !merged.lastDeployedUrl) {
|
|
251
|
-
merged.lastDeployedUrl = targetWorkersDevUrl(merged.workerName);
|
|
252
|
-
}
|
|
253
|
-
return merged;
|
|
254
|
-
}
|
|
255
|
-
export function writeDeployState(tenantRoot, state, options = {}) {
|
|
256
|
-
const target = normalizeTarget(options.scope ?? options.target ?? state.target ?? 'prod');
|
|
257
|
-
const { statePath } = resolveTargetPaths(tenantRoot, target);
|
|
258
|
-
writeJson(statePath, {
|
|
259
|
-
...state,
|
|
260
|
-
target,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
export function resolveGeneratedWranglerPath(tenantRoot, options = {}) {
|
|
264
|
-
return resolveTargetPaths(tenantRoot, options.scope ?? options.target ?? 'prod').wranglerPath;
|
|
265
|
-
}
|
|
266
|
-
export function buildWranglerConfigContents(tenantRoot, deployConfig, state, options = {}) {
|
|
267
|
-
const target = normalizeTarget(options.scope ?? options.target ?? state.target ?? 'prod');
|
|
268
|
-
const { generatedRoot } = resolveTargetPaths(tenantRoot, target);
|
|
269
|
-
const workerName = state.workerName ?? targetWorkerName(deployConfig, target);
|
|
270
|
-
const mainPath = relativeFromGeneratedRoot(resolve(generatedRoot, 'worker/index.js'), generatedRoot);
|
|
271
|
-
const assetsDirectory = relativeFromGeneratedRoot(resolve(tenantRoot, 'dist'), generatedRoot);
|
|
272
|
-
const migrationsDir = relativeFromGeneratedRoot(resolve(tenantRoot, 'migrations'), generatedRoot);
|
|
273
|
-
const vars = buildPublicVars(deployConfig);
|
|
274
|
-
return [
|
|
275
|
-
`name = ${renderTomlString(workerName)}`,
|
|
276
|
-
`compatibility_date = ${renderTomlString(DEFAULT_COMPATIBILITY_DATE)}`,
|
|
277
|
-
`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((flag) => renderTomlString(flag)).join(', ')}]`,
|
|
278
|
-
`main = ${renderTomlString(mainPath)}`,
|
|
279
|
-
'workers_dev = true',
|
|
280
|
-
'preview_urls = true',
|
|
281
|
-
'',
|
|
282
|
-
'[assets]',
|
|
283
|
-
`directory = ${renderTomlString(assetsDirectory)}`,
|
|
284
|
-
'',
|
|
285
|
-
'[vars]',
|
|
286
|
-
...Object.entries(vars).map(([key, value]) => `${key} = ${renderTomlString(value)}`),
|
|
287
|
-
'',
|
|
288
|
-
'[[kv_namespaces]]',
|
|
289
|
-
'binding = "FORM_GUARD_KV"',
|
|
290
|
-
`id = ${renderTomlString(state.kvNamespaces.FORM_GUARD_KV.id)}`,
|
|
291
|
-
`preview_id = ${renderTomlString(state.kvNamespaces.FORM_GUARD_KV.previewId ?? state.kvNamespaces.FORM_GUARD_KV.id)}`,
|
|
292
|
-
'',
|
|
293
|
-
'[[kv_namespaces]]',
|
|
294
|
-
'binding = "SESSION"',
|
|
295
|
-
`id = ${renderTomlString(state.kvNamespaces.SESSION.id)}`,
|
|
296
|
-
`preview_id = ${renderTomlString(state.kvNamespaces.SESSION.previewId ?? state.kvNamespaces.SESSION.id)}`,
|
|
297
|
-
'',
|
|
298
|
-
'[[d1_databases]]',
|
|
299
|
-
'binding = "SITE_DATA_DB"',
|
|
300
|
-
`database_name = ${renderTomlString(state.d1Databases.SITE_DATA_DB.databaseName)}`,
|
|
301
|
-
`database_id = ${renderTomlString(state.d1Databases.SITE_DATA_DB.databaseId)}`,
|
|
302
|
-
`preview_database_id = ${renderTomlString(state.d1Databases.SITE_DATA_DB.previewDatabaseId ?? state.d1Databases.SITE_DATA_DB.databaseId)}`,
|
|
303
|
-
`migrations_dir = ${renderTomlString(migrationsDir)}`,
|
|
304
|
-
'',
|
|
305
|
-
].join('\n');
|
|
306
|
-
}
|
|
307
|
-
export function ensureGeneratedWranglerConfig(tenantRoot, options = {}) {
|
|
308
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
309
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
310
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
311
|
-
const { wranglerPath } = resolveTargetPaths(tenantRoot, target);
|
|
312
|
-
const manifestFingerprint = stableHash(JSON.stringify({ deployConfig, targetKey: targetKey(target) }));
|
|
313
|
-
const contents = buildWranglerConfigContents(tenantRoot, deployConfig, state, { target });
|
|
314
|
-
ensureParent(wranglerPath);
|
|
315
|
-
writeFileSync(wranglerPath, contents, 'utf8');
|
|
316
|
-
state.lastManifestFingerprint = manifestFingerprint;
|
|
317
|
-
state.readiness.lastConfigFingerprint = manifestFingerprint;
|
|
318
|
-
if (!state.generatedSecrets) {
|
|
319
|
-
state.generatedSecrets = {};
|
|
320
|
-
}
|
|
321
|
-
const secretMap = buildSecretMap(deployConfig, state);
|
|
322
|
-
state.generatedSecrets.TREESEED_FORM_TOKEN_SECRET = secretMap.TREESEED_FORM_TOKEN_SECRET;
|
|
323
|
-
writeDeployState(tenantRoot, state, { target });
|
|
324
|
-
return { wranglerPath, deployConfig, state, manifestFingerprint, target };
|
|
325
|
-
}
|
|
326
|
-
function runWrangler(args, { cwd, allowFailure = false, json = false, capture = false, env = {}, input } = {}) {
|
|
327
|
-
const result = spawnSync(process.execPath, [resolveWranglerBin(), ...args], {
|
|
328
|
-
stdio: json || capture || input !== undefined ? ['pipe', 'pipe', 'pipe'] : 'inherit',
|
|
329
|
-
cwd,
|
|
330
|
-
env: { ...process.env, ...env },
|
|
331
|
-
encoding: 'utf8',
|
|
332
|
-
input,
|
|
333
|
-
});
|
|
334
|
-
if (result.status !== 0 && !allowFailure) {
|
|
335
|
-
const stderr = result.stderr?.trim();
|
|
336
|
-
throw new Error(stderr || `Wrangler command failed: ${args.join(' ')}`);
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
339
|
-
}
|
|
340
|
-
function parseWranglerJsonOutput(result, label) {
|
|
341
|
-
const source = `${result.stdout ?? ''}`.trim();
|
|
342
|
-
if (!source) {
|
|
343
|
-
throw new Error(`Expected JSON output from ${label}.`);
|
|
344
|
-
}
|
|
345
|
-
return JSON.parse(source);
|
|
346
|
-
}
|
|
347
|
-
function listKvNamespaces(tenantRoot, env) {
|
|
348
|
-
const result = runWrangler(['kv', 'namespace', 'list'], {
|
|
349
|
-
cwd: tenantRoot,
|
|
350
|
-
capture: true,
|
|
351
|
-
env,
|
|
352
|
-
});
|
|
353
|
-
return parseWranglerJsonOutput(result, 'KV namespace list');
|
|
354
|
-
}
|
|
355
|
-
function listD1Databases(tenantRoot, env) {
|
|
356
|
-
const result = runWrangler(['d1', 'list', '--json'], {
|
|
357
|
-
cwd: tenantRoot,
|
|
358
|
-
capture: true,
|
|
359
|
-
env,
|
|
360
|
-
});
|
|
361
|
-
return parseWranglerJsonOutput(result, 'D1 list');
|
|
362
|
-
}
|
|
363
|
-
function isPlaceholderResourceId(value) {
|
|
364
|
-
if (!value || typeof value !== 'string') {
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
return (value.startsWith('local-')
|
|
368
|
-
|| value.startsWith('dryrun-')
|
|
369
|
-
|| value.endsWith('-id')
|
|
370
|
-
|| value.endsWith('-preview-id'));
|
|
371
|
-
}
|
|
372
|
-
function buildProvisioningSummary(deployConfig, state, target) {
|
|
373
|
-
return {
|
|
374
|
-
target: deployTargetLabel(target),
|
|
375
|
-
workerName: state.workerName ?? targetWorkerName(deployConfig, target),
|
|
376
|
-
siteUrl: target.kind === 'branch' ? targetWorkersDevUrl(state.workerName) : deployConfig.siteUrl,
|
|
377
|
-
accountId: deployConfig.cloudflare.accountId,
|
|
378
|
-
formGuardKv: state.kvNamespaces.FORM_GUARD_KV,
|
|
379
|
-
sessionKv: state.kvNamespaces.SESSION,
|
|
380
|
-
siteDataDb: state.d1Databases.SITE_DATA_DB,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
function buildDestroySummary(deployConfig, state, target) {
|
|
384
|
-
return buildProvisioningSummary(deployConfig, state, target);
|
|
385
|
-
}
|
|
386
|
-
function isPlaceholderAccountId(value) {
|
|
387
|
-
return !value || value === 'replace-with-cloudflare-account-id';
|
|
388
|
-
}
|
|
389
|
-
function missingTurnstileRequirements() {
|
|
390
|
-
const issues = [];
|
|
391
|
-
if (!envOrNull('TREESEED_PUBLIC_TURNSTILE_SITE_KEY')) {
|
|
392
|
-
issues.push('Set TREESEED_PUBLIC_TURNSTILE_SITE_KEY before deploying.');
|
|
393
|
-
}
|
|
394
|
-
if (!envOrNull('TREESEED_TURNSTILE_SECRET_KEY')) {
|
|
395
|
-
issues.push('Set TREESEED_TURNSTILE_SECRET_KEY before deploying.');
|
|
396
|
-
}
|
|
397
|
-
return issues;
|
|
398
|
-
}
|
|
399
|
-
export function collectMissingDeployInputs(tenantRoot) {
|
|
400
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
401
|
-
const missing = [];
|
|
402
|
-
if (isPlaceholderAccountId(deployConfig.cloudflare.accountId)) {
|
|
403
|
-
missing.push({
|
|
404
|
-
key: 'CLOUDFLARE_ACCOUNT_ID',
|
|
405
|
-
label: 'Cloudflare account ID',
|
|
406
|
-
message: `Cloudflare account ID is missing. Add it to ${relative(tenantRoot, deployConfig.__configPath ?? resolve(tenantRoot, 'treeseed.site.yaml'))} or provide it now.`,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
if (!envOrNull('TREESEED_PUBLIC_TURNSTILE_SITE_KEY')) {
|
|
410
|
-
missing.push({
|
|
411
|
-
key: 'TREESEED_PUBLIC_TURNSTILE_SITE_KEY',
|
|
412
|
-
label: 'Turnstile public site key',
|
|
413
|
-
message: 'Turnstile public site key is missing for deploy.',
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
if (!envOrNull('TREESEED_TURNSTILE_SECRET_KEY')) {
|
|
417
|
-
missing.push({
|
|
418
|
-
key: 'TREESEED_TURNSTILE_SECRET_KEY',
|
|
419
|
-
label: 'Turnstile secret key',
|
|
420
|
-
message: 'Turnstile secret key is missing for deploy.',
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
return missing;
|
|
424
|
-
}
|
|
425
|
-
export async function promptForMissingDeployInputs(tenantRoot) {
|
|
426
|
-
const missing = collectMissingDeployInputs(tenantRoot);
|
|
427
|
-
if (!missing.length || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
428
|
-
return { prompted: false, provided: [] };
|
|
429
|
-
}
|
|
430
|
-
const rl = createInterface({
|
|
431
|
-
input: process.stdin,
|
|
432
|
-
output: process.stdout,
|
|
433
|
-
});
|
|
434
|
-
const provided = [];
|
|
435
|
-
try {
|
|
436
|
-
console.log('Treeseed deploy needs a few missing values before it can continue.');
|
|
437
|
-
console.log('These values will be used for this deploy process only. Persist them in your env files or CI secrets afterward.');
|
|
438
|
-
for (const item of missing) {
|
|
439
|
-
console.log(`- ${item.message}`);
|
|
440
|
-
const answer = (await rl.question(`${item.label}: `)).trim();
|
|
441
|
-
if (!answer) {
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
process.env[item.key] = answer;
|
|
445
|
-
provided.push(item.key);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
finally {
|
|
449
|
-
rl.close();
|
|
450
|
-
}
|
|
451
|
-
return { prompted: true, provided };
|
|
452
|
-
}
|
|
453
|
-
export function validateDeployPrerequisites(tenantRoot, { requireRemote = true } = {}) {
|
|
454
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
455
|
-
const issues = [];
|
|
456
|
-
if (isPlaceholderAccountId(deployConfig.cloudflare.accountId)) {
|
|
457
|
-
issues.push(`Set cloudflare.accountId in ${relative(tenantRoot, deployConfig.__configPath ?? resolve(tenantRoot, 'treeseed.site.yaml'))} or export CLOUDFLARE_ACCOUNT_ID.`);
|
|
458
|
-
}
|
|
459
|
-
if (requireRemote) {
|
|
460
|
-
issues.push(...missingTurnstileRequirements());
|
|
461
|
-
const result = runWrangler(['whoami'], {
|
|
462
|
-
cwd: tenantRoot,
|
|
463
|
-
allowFailure: true,
|
|
464
|
-
capture: true,
|
|
465
|
-
});
|
|
466
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
467
|
-
if (/You are not authenticated/i.test(output) || /wrangler login/i.test(output)) {
|
|
468
|
-
issues.push('Authenticate Wrangler first with `wrangler login`.');
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (issues.length > 0) {
|
|
472
|
-
throw new Error(`Treeseed deploy prerequisites are not satisfied:\n- ${issues.join('\n- ')}`);
|
|
473
|
-
}
|
|
474
|
-
return deployConfig;
|
|
475
|
-
}
|
|
476
|
-
export function validateDestroyPrerequisites(tenantRoot, { requireRemote = true } = {}) {
|
|
477
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
478
|
-
const issues = [];
|
|
479
|
-
if (requireRemote && isPlaceholderAccountId(deployConfig.cloudflare.accountId)) {
|
|
480
|
-
issues.push(`Set cloudflare.accountId in ${relative(tenantRoot, deployConfig.__configPath ?? resolve(tenantRoot, 'treeseed.site.yaml'))} or export CLOUDFLARE_ACCOUNT_ID.`);
|
|
481
|
-
}
|
|
482
|
-
if (requireRemote) {
|
|
483
|
-
const result = runWrangler(['whoami'], {
|
|
484
|
-
cwd: tenantRoot,
|
|
485
|
-
allowFailure: true,
|
|
486
|
-
capture: true,
|
|
487
|
-
});
|
|
488
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
489
|
-
if (/You are not authenticated/i.test(output) || /wrangler login/i.test(output)) {
|
|
490
|
-
issues.push('Authenticate Wrangler first with `wrangler login`.');
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (issues.length > 0) {
|
|
494
|
-
throw new Error(`Treeseed destroy prerequisites are not satisfied:\n- ${issues.join('\n- ')}`);
|
|
495
|
-
}
|
|
496
|
-
return deployConfig;
|
|
497
|
-
}
|
|
498
|
-
function resolveExistingKvIdByName(kvNamespaces, expectedName, fallbackId) {
|
|
499
|
-
if (fallbackId && !isPlaceholderResourceId(fallbackId)) {
|
|
500
|
-
return fallbackId;
|
|
501
|
-
}
|
|
502
|
-
return kvNamespaces.find((entry) => entry?.title === expectedName)?.id ?? null;
|
|
503
|
-
}
|
|
504
|
-
function resolveExistingD1ByName(d1Databases, expectedName, current) {
|
|
505
|
-
if (current?.databaseId && !isPlaceholderResourceId(current.databaseId)) {
|
|
506
|
-
return current;
|
|
507
|
-
}
|
|
508
|
-
const existing = d1Databases.find((entry) => entry?.name === expectedName);
|
|
509
|
-
if (!existing?.uuid) {
|
|
510
|
-
return current;
|
|
511
|
-
}
|
|
512
|
-
return {
|
|
513
|
-
...current,
|
|
514
|
-
databaseId: existing.uuid,
|
|
515
|
-
previewDatabaseId: existing.previewDatabaseUuid ?? existing.uuid,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
function looksLikeMissingResource(output) {
|
|
519
|
-
return /not found|does not exist|could not find|unknown/i.test(output);
|
|
520
|
-
}
|
|
521
|
-
function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = false }) {
|
|
522
|
-
if (!namespaceId || isPlaceholderResourceId(namespaceId)) {
|
|
523
|
-
return { status: 'missing', id: namespaceId };
|
|
524
|
-
}
|
|
525
|
-
if (dryRun) {
|
|
526
|
-
return { status: 'planned', id: namespaceId, preview };
|
|
527
|
-
}
|
|
528
|
-
const args = ['kv', 'namespace', 'delete', '--namespace-id', namespaceId, '--skip-confirmation'];
|
|
529
|
-
if (preview) {
|
|
530
|
-
args.push('--preview');
|
|
531
|
-
}
|
|
532
|
-
const result = runWrangler(args, {
|
|
533
|
-
cwd: tenantRoot,
|
|
534
|
-
allowFailure: true,
|
|
535
|
-
capture: true,
|
|
536
|
-
env,
|
|
537
|
-
});
|
|
538
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
539
|
-
if (result.status !== 0 && !looksLikeMissingResource(output)) {
|
|
540
|
-
throw new Error(output.trim() || `Failed to delete KV namespace ${namespaceId}.`);
|
|
541
|
-
}
|
|
542
|
-
return { status: result.status === 0 ? 'deleted' : 'missing', id: namespaceId, preview };
|
|
543
|
-
}
|
|
544
|
-
function deleteD1Database(tenantRoot, databaseName, { env, dryRun }) {
|
|
545
|
-
if (!databaseName) {
|
|
546
|
-
return { status: 'missing', name: databaseName };
|
|
547
|
-
}
|
|
548
|
-
if (dryRun) {
|
|
549
|
-
return { status: 'planned', name: databaseName };
|
|
550
|
-
}
|
|
551
|
-
const result = runWrangler(['d1', 'delete', databaseName, '--skip-confirmation'], {
|
|
552
|
-
cwd: tenantRoot,
|
|
553
|
-
allowFailure: true,
|
|
554
|
-
capture: true,
|
|
555
|
-
env,
|
|
556
|
-
});
|
|
557
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
558
|
-
if (result.status !== 0 && !looksLikeMissingResource(output)) {
|
|
559
|
-
throw new Error(output.trim() || `Failed to delete D1 database ${databaseName}.`);
|
|
560
|
-
}
|
|
561
|
-
return { status: result.status === 0 ? 'deleted' : 'missing', name: databaseName };
|
|
562
|
-
}
|
|
563
|
-
function deleteWorker(tenantRoot, workerName, { env, dryRun, force = false }) {
|
|
564
|
-
if (!workerName) {
|
|
565
|
-
return { status: 'missing', name: workerName };
|
|
566
|
-
}
|
|
567
|
-
if (dryRun) {
|
|
568
|
-
return { status: 'planned', name: workerName };
|
|
569
|
-
}
|
|
570
|
-
const args = ['delete', workerName];
|
|
571
|
-
if (force) {
|
|
572
|
-
args.push('--force');
|
|
573
|
-
}
|
|
574
|
-
const result = runWrangler(args, {
|
|
575
|
-
cwd: tenantRoot,
|
|
576
|
-
allowFailure: true,
|
|
577
|
-
capture: true,
|
|
578
|
-
env,
|
|
579
|
-
input: 'y\n',
|
|
580
|
-
});
|
|
581
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
582
|
-
if (result.status !== 0 && !looksLikeMissingResource(output)) {
|
|
583
|
-
throw new Error(output.trim() || `Failed to delete Worker ${workerName}.`);
|
|
584
|
-
}
|
|
585
|
-
return { status: result.status === 0 ? 'deleted' : 'missing', name: workerName };
|
|
586
|
-
}
|
|
587
|
-
export function destroyCloudflareResources(tenantRoot, options = {}) {
|
|
588
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
589
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
590
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
591
|
-
state.workerName = targetWorkerName(deployConfig, target);
|
|
592
|
-
const env = {
|
|
593
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig.cloudflare.accountId,
|
|
594
|
-
};
|
|
595
|
-
const dryRun = options.dryRun ?? false;
|
|
596
|
-
const force = options.force ?? false;
|
|
597
|
-
const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
|
|
598
|
-
const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
|
|
599
|
-
state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(kvNamespaces, state.kvNamespaces.FORM_GUARD_KV.name, state.kvNamespaces.FORM_GUARD_KV.id);
|
|
600
|
-
state.kvNamespaces.SESSION.id = resolveExistingKvIdByName(kvNamespaces, state.kvNamespaces.SESSION.name, state.kvNamespaces.SESSION.id);
|
|
601
|
-
state.d1Databases.SITE_DATA_DB = resolveExistingD1ByName(d1Databases, state.d1Databases.SITE_DATA_DB.databaseName, state.d1Databases.SITE_DATA_DB);
|
|
602
|
-
const worker = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
|
|
603
|
-
const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
|
|
604
|
-
const formGuardPreview = state.kvNamespaces.FORM_GUARD_KV.previewId
|
|
605
|
-
&& state.kvNamespaces.FORM_GUARD_KV.previewId !== state.kvNamespaces.FORM_GUARD_KV.id
|
|
606
|
-
? deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.previewId, { env, dryRun, preview: true })
|
|
607
|
-
: null;
|
|
608
|
-
const session = deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.id, { env, dryRun });
|
|
609
|
-
const sessionPreview = state.kvNamespaces.SESSION.previewId
|
|
610
|
-
&& state.kvNamespaces.SESSION.previewId !== state.kvNamespaces.SESSION.id
|
|
611
|
-
? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.previewId, { env, dryRun, preview: true })
|
|
612
|
-
: null;
|
|
613
|
-
const database = deleteD1Database(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun });
|
|
614
|
-
return {
|
|
615
|
-
target,
|
|
616
|
-
summary: buildDestroySummary(deployConfig, state, target),
|
|
617
|
-
operations: {
|
|
618
|
-
worker,
|
|
619
|
-
formGuard,
|
|
620
|
-
formGuardPreview,
|
|
621
|
-
session,
|
|
622
|
-
sessionPreview,
|
|
623
|
-
database,
|
|
624
|
-
},
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
export function cleanupDestroyedState(tenantRoot, options = {}) {
|
|
628
|
-
const target = options.scope || options.target ? normalizeTarget(options.scope ?? options.target) : null;
|
|
629
|
-
if (target) {
|
|
630
|
-
const { statePath, generatedRoot } = resolveTargetPaths(tenantRoot, target);
|
|
631
|
-
rmSync(statePath, { force: true });
|
|
632
|
-
rmSync(generatedRoot, { recursive: true, force: true });
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
rmSync(resolve(tenantRoot, STATE_ROOT), { recursive: true, force: true });
|
|
636
|
-
rmSync(resolve(tenantRoot, GENERATED_ROOT), { recursive: true, force: true });
|
|
637
|
-
if (options.removeBuildArtifacts) {
|
|
638
|
-
rmSync(resolve(tenantRoot, 'dist'), { recursive: true, force: true });
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
export function provisionCloudflareResources(tenantRoot, options = {}) {
|
|
642
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
643
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
644
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
645
|
-
state.workerName = targetWorkerName(deployConfig, target);
|
|
646
|
-
const env = {
|
|
647
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig.cloudflare.accountId,
|
|
648
|
-
};
|
|
649
|
-
const dryRun = options.dryRun ?? false;
|
|
650
|
-
const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
|
|
651
|
-
const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
|
|
652
|
-
const ensureKv = (binding) => {
|
|
653
|
-
const current = state.kvNamespaces[binding];
|
|
654
|
-
if (current?.id && !isPlaceholderResourceId(current.id)) {
|
|
655
|
-
state.kvNamespaces[binding].previewId = current.previewId ?? current.id;
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
const existing = kvNamespaces.find((entry) => entry?.title === current.name);
|
|
659
|
-
if (existing?.id) {
|
|
660
|
-
state.kvNamespaces[binding].id = existing.id;
|
|
661
|
-
state.kvNamespaces[binding].previewId = existing.id;
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
if (dryRun) {
|
|
665
|
-
state.kvNamespaces[binding].id = `dryrun-${current.name}`;
|
|
666
|
-
state.kvNamespaces[binding].previewId = `dryrun-${current.name}-preview`;
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
runWrangler(['kv', 'namespace', 'create', current.name], { cwd: tenantRoot, capture: true, env });
|
|
670
|
-
const refreshed = listKvNamespaces(tenantRoot, env);
|
|
671
|
-
const created = refreshed.find((entry) => entry?.title === current.name);
|
|
672
|
-
if (!created?.id) {
|
|
673
|
-
throw new Error(`Unable to resolve created KV namespace id for ${current.name}.`);
|
|
674
|
-
}
|
|
675
|
-
state.kvNamespaces[binding].id = created.id;
|
|
676
|
-
state.kvNamespaces[binding].previewId = created.id;
|
|
677
|
-
};
|
|
678
|
-
const ensureD1 = () => {
|
|
679
|
-
const current = state.d1Databases.SITE_DATA_DB;
|
|
680
|
-
if (current?.databaseId && !isPlaceholderResourceId(current.databaseId)) {
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
const existing = d1Databases.find((entry) => entry?.name === current.databaseName);
|
|
684
|
-
if (existing?.uuid) {
|
|
685
|
-
current.databaseId = existing.uuid;
|
|
686
|
-
current.previewDatabaseId = existing.previewDatabaseUuid ?? existing.uuid;
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
if (dryRun) {
|
|
690
|
-
current.databaseId = `dryrun-${current.databaseName}`;
|
|
691
|
-
current.previewDatabaseId = `dryrun-${current.databaseName}-preview`;
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
runWrangler(['d1', 'create', current.databaseName], {
|
|
695
|
-
cwd: tenantRoot,
|
|
696
|
-
capture: true,
|
|
697
|
-
env,
|
|
698
|
-
});
|
|
699
|
-
const refreshed = listD1Databases(tenantRoot, env);
|
|
700
|
-
const created = refreshed.find((entry) => entry?.name === current.databaseName);
|
|
701
|
-
if (!created?.uuid) {
|
|
702
|
-
throw new Error(`Unable to resolve created D1 database id for ${current.databaseName}.`);
|
|
703
|
-
}
|
|
704
|
-
current.databaseId = created.uuid;
|
|
705
|
-
current.previewDatabaseId = created.previewDatabaseUuid ?? created.uuid;
|
|
706
|
-
};
|
|
707
|
-
ensureKv('FORM_GUARD_KV');
|
|
708
|
-
ensureKv('SESSION');
|
|
709
|
-
ensureD1();
|
|
710
|
-
state.readiness.initialized = true;
|
|
711
|
-
state.readiness.initializedAt = new Date().toISOString();
|
|
712
|
-
state.readiness.lastValidatedAt = state.readiness.initializedAt;
|
|
713
|
-
writeDeployState(tenantRoot, state, { target });
|
|
714
|
-
return buildProvisioningSummary(deployConfig, state, target);
|
|
715
|
-
}
|
|
716
|
-
export function syncCloudflareSecrets(tenantRoot, options = {}) {
|
|
717
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
718
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
719
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
720
|
-
const env = {
|
|
721
|
-
CLOUDFLARE_ACCOUNT_ID: deployConfig.cloudflare.accountId,
|
|
722
|
-
};
|
|
723
|
-
const secrets = buildSecretMap(deployConfig, state);
|
|
724
|
-
const synced = [];
|
|
725
|
-
const dryRun = options.dryRun ?? false;
|
|
726
|
-
for (const [key, value] of Object.entries(secrets)) {
|
|
727
|
-
if (!value) {
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
synced.push(key);
|
|
731
|
-
if (dryRun) {
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
const result = spawnSync(process.execPath, [resolveWranglerBin(), 'secret', 'put', key, '--config', resolveGeneratedWranglerPath(tenantRoot, { target })], {
|
|
735
|
-
cwd: tenantRoot,
|
|
736
|
-
input: `${value}\n`,
|
|
737
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
738
|
-
env: { ...process.env, ...env },
|
|
739
|
-
encoding: 'utf8',
|
|
740
|
-
});
|
|
741
|
-
if (result.status !== 0) {
|
|
742
|
-
throw new Error(`Failed to sync secret ${key}.`);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
state.generatedSecrets = {
|
|
746
|
-
...(state.generatedSecrets ?? {}),
|
|
747
|
-
TREESEED_FORM_TOKEN_SECRET: secrets.TREESEED_FORM_TOKEN_SECRET ?? state.generatedSecrets?.TREESEED_FORM_TOKEN_SECRET,
|
|
748
|
-
};
|
|
749
|
-
writeDeployState(tenantRoot, state, { target });
|
|
750
|
-
return synced;
|
|
751
|
-
}
|
|
752
|
-
export function runRemoteD1Migrations(tenantRoot, options = {}) {
|
|
753
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
754
|
-
const { wranglerPath, deployConfig, state } = ensureGeneratedWranglerConfig(tenantRoot, { target });
|
|
755
|
-
if (options.dryRun) {
|
|
756
|
-
return { databaseName: state.d1Databases.SITE_DATA_DB.databaseName, dryRun: true };
|
|
757
|
-
}
|
|
758
|
-
runWrangler(['d1', 'migrations', 'apply', state.d1Databases.SITE_DATA_DB.databaseName, '--remote', '--config', wranglerPath], {
|
|
759
|
-
cwd: tenantRoot,
|
|
760
|
-
env: { CLOUDFLARE_ACCOUNT_ID: deployConfig.cloudflare.accountId },
|
|
761
|
-
});
|
|
762
|
-
return { databaseName: state.d1Databases.SITE_DATA_DB.databaseName, dryRun: false };
|
|
763
|
-
}
|
|
764
|
-
export function markDeploymentInitialized(tenantRoot, options = {}) {
|
|
765
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
766
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
767
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
768
|
-
const timestamp = new Date().toISOString();
|
|
769
|
-
state.readiness.initialized = true;
|
|
770
|
-
state.readiness.initializedAt = state.readiness.initializedAt ?? timestamp;
|
|
771
|
-
state.readiness.lastValidatedAt = timestamp;
|
|
772
|
-
state.readiness.lastConfigFingerprint = state.lastManifestFingerprint ?? state.readiness.lastConfigFingerprint;
|
|
773
|
-
writeDeployState(tenantRoot, state, { target });
|
|
774
|
-
return state;
|
|
775
|
-
}
|
|
776
|
-
export function assertDeploymentInitialized(tenantRoot, options = {}) {
|
|
777
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
778
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
779
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
780
|
-
if (state.readiness?.initialized) {
|
|
781
|
-
return state;
|
|
782
|
-
}
|
|
783
|
-
throw new Error(`Treeseed environment ${deployTargetLabel(target)} has not been initialized. Run \`treeseed config --environment ${scopeFromTarget(target)}\` first.`);
|
|
784
|
-
}
|
|
785
|
-
export function finalizeDeploymentState(tenantRoot, options = {}) {
|
|
786
|
-
const target = normalizeTarget(options.scope ?? options.target ?? 'prod');
|
|
787
|
-
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
788
|
-
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
789
|
-
state.lastManifestFingerprint = stableHash(JSON.stringify({ deployConfig, targetKey: targetKey(target) }));
|
|
790
|
-
state.lastDeployedUrl = target.kind === 'branch' ? targetWorkersDevUrl(state.workerName) : deployConfig.siteUrl;
|
|
791
|
-
state.lastDeploymentTimestamp = new Date().toISOString();
|
|
792
|
-
state.lastDeployedCommit = envOrNull('GITHUB_SHA') ?? envOrNull('TREESEED_DEPLOY_COMMIT') ?? null;
|
|
793
|
-
state.runtimeCompatibility = {
|
|
794
|
-
envelopeSchemaGeneration: TRESEED_ENVELOPE_SCHEMA_GENERATION,
|
|
795
|
-
migrationWaveId: TRESEED_MIGRATION_WAVE_ID,
|
|
796
|
-
supportedPayloadVersionRange: TRESEED_SUPPORTED_PAYLOAD_RANGE,
|
|
797
|
-
};
|
|
798
|
-
const nextHistoryEntry = {
|
|
799
|
-
commit: state.lastDeployedCommit,
|
|
800
|
-
timestamp: state.lastDeploymentTimestamp,
|
|
801
|
-
url: state.lastDeployedUrl,
|
|
802
|
-
target: deployTargetLabel(target),
|
|
803
|
-
appVersion: envOrNull('npm_package_version') ?? envOrNull('TREESEED_APP_VERSION') ?? null,
|
|
804
|
-
envelopeSchemaGeneration: TRESEED_ENVELOPE_SCHEMA_GENERATION,
|
|
805
|
-
migrationWaveId: TRESEED_MIGRATION_WAVE_ID,
|
|
806
|
-
supportedPayloadVersionRange: TRESEED_SUPPORTED_PAYLOAD_RANGE,
|
|
807
|
-
};
|
|
808
|
-
const history = Array.isArray(state.deploymentHistory) ? state.deploymentHistory : [];
|
|
809
|
-
state.deploymentHistory = [...history, nextHistoryEntry].slice(-20);
|
|
810
|
-
state.readiness.initialized = true;
|
|
811
|
-
state.readiness.lastValidatedAt = state.lastDeploymentTimestamp;
|
|
812
|
-
writeDeployState(tenantRoot, state, { target });
|
|
813
|
-
return state;
|
|
814
|
-
}
|
|
815
|
-
export function printDeploySummary(summary) {
|
|
816
|
-
console.log('Treeseed deployment summary');
|
|
817
|
-
console.log(` Target: ${summary.target}`);
|
|
818
|
-
console.log(` Worker: ${summary.workerName}`);
|
|
819
|
-
console.log(` Site URL: ${summary.siteUrl}`);
|
|
820
|
-
console.log(` Account ID: ${summary.accountId}`);
|
|
821
|
-
console.log(` D1: ${summary.siteDataDb.databaseName} (${summary.siteDataDb.databaseId})`);
|
|
822
|
-
console.log(` KV FORM_GUARD_KV: ${summary.formGuardKv.id}`);
|
|
823
|
-
console.log(` KV SESSION: ${summary.sessionKv.id}`);
|
|
824
|
-
}
|
|
825
|
-
export function printDestroySummary(result) {
|
|
826
|
-
const { summary, operations } = result;
|
|
827
|
-
console.log('Treeseed destroy summary');
|
|
828
|
-
console.log(` Target: ${summary.target}`);
|
|
829
|
-
console.log(` Worker: ${summary.workerName} -> ${operations.worker.status}`);
|
|
830
|
-
console.log(` Site URL: ${summary.siteUrl}`);
|
|
831
|
-
console.log(` Account ID: ${summary.accountId}`);
|
|
832
|
-
console.log(` D1: ${summary.siteDataDb.databaseName} -> ${operations.database.status}`);
|
|
833
|
-
console.log(` KV FORM_GUARD_KV: ${summary.formGuardKv.name} -> ${operations.formGuard.status}`);
|
|
834
|
-
if (operations.formGuardPreview) {
|
|
835
|
-
console.log(` KV FORM_GUARD_KV preview -> ${operations.formGuardPreview.status}`);
|
|
836
|
-
}
|
|
837
|
-
console.log(` KV SESSION: ${summary.sessionKv.name} -> ${operations.session.status}`);
|
|
838
|
-
if (operations.sessionPreview) {
|
|
839
|
-
console.log(` KV SESSION preview -> ${operations.sessionPreview.status}`);
|
|
840
|
-
}
|
|
841
|
-
}
|