@treeseed/cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +115 -0
  2. package/dist/cli/handlers/close.js +60 -0
  3. package/dist/cli/handlers/config.js +76 -0
  4. package/dist/cli/handlers/continue.js +23 -0
  5. package/dist/cli/handlers/deploy.js +139 -0
  6. package/dist/cli/handlers/destroy.js +94 -0
  7. package/dist/cli/handlers/doctor.js +48 -0
  8. package/dist/cli/handlers/init.js +30 -0
  9. package/dist/cli/handlers/next.js +27 -0
  10. package/dist/cli/handlers/prepare.js +8 -0
  11. package/dist/cli/handlers/promote.js +8 -0
  12. package/dist/cli/handlers/publish.js +8 -0
  13. package/dist/cli/handlers/release.js +60 -0
  14. package/dist/cli/handlers/rollback.js +163 -0
  15. package/dist/cli/handlers/save.js +87 -0
  16. package/dist/cli/handlers/setup.js +48 -0
  17. package/dist/cli/handlers/ship.js +49 -0
  18. package/dist/cli/handlers/start.js +97 -0
  19. package/dist/cli/handlers/status.js +31 -0
  20. package/dist/cli/handlers/teardown.js +50 -0
  21. package/dist/cli/handlers/utils.js +59 -0
  22. package/dist/cli/handlers/work.js +85 -0
  23. package/dist/cli/help.js +143 -0
  24. package/dist/cli/main.js +27 -0
  25. package/dist/cli/parser.js +96 -0
  26. package/dist/cli/registry.js +445 -0
  27. package/dist/cli/repair.js +89 -0
  28. package/dist/cli/runtime.js +165 -0
  29. package/dist/cli/types.js +0 -0
  30. package/dist/cli/workflow-state.js +171 -0
  31. package/dist/index.js +1 -0
  32. package/dist/scripts/aggregate-book.d.ts +1 -0
  33. package/dist/scripts/aggregate-book.js +121 -0
  34. package/dist/scripts/assert-release-tag-version.d.ts +1 -0
  35. package/dist/scripts/assert-release-tag-version.js +21 -0
  36. package/dist/scripts/build-dist.d.ts +1 -0
  37. package/dist/scripts/build-dist.js +108 -0
  38. package/dist/scripts/build-tenant-worker.d.ts +1 -0
  39. package/dist/scripts/build-tenant-worker.js +36 -0
  40. package/dist/scripts/cleanup-markdown.d.ts +2 -0
  41. package/dist/scripts/cleanup-markdown.js +373 -0
  42. package/dist/scripts/config-runtime-lib.d.ts +122 -0
  43. package/dist/scripts/config-runtime-lib.js +505 -0
  44. package/dist/scripts/config-treeseed.d.ts +2 -0
  45. package/dist/scripts/config-treeseed.js +81 -0
  46. package/dist/scripts/d1-migration-lib.d.ts +6 -0
  47. package/dist/scripts/d1-migration-lib.js +90 -0
  48. package/dist/scripts/deploy-lib.d.ts +127 -0
  49. package/dist/scripts/deploy-lib.js +841 -0
  50. package/dist/scripts/ensure-mailpit.d.ts +1 -0
  51. package/dist/scripts/ensure-mailpit.js +29 -0
  52. package/dist/scripts/git-workflow-lib.d.ts +25 -0
  53. package/dist/scripts/git-workflow-lib.js +136 -0
  54. package/dist/scripts/github-automation-lib.d.ts +156 -0
  55. package/dist/scripts/github-automation-lib.js +242 -0
  56. package/dist/scripts/local-dev-lib.d.ts +9 -0
  57. package/dist/scripts/local-dev-lib.js +84 -0
  58. package/dist/scripts/local-dev.d.ts +1 -0
  59. package/dist/scripts/local-dev.js +129 -0
  60. package/dist/scripts/logs-mailpit.d.ts +1 -0
  61. package/dist/scripts/logs-mailpit.js +2 -0
  62. package/dist/scripts/mailpit-runtime.d.ts +4 -0
  63. package/dist/scripts/mailpit-runtime.js +57 -0
  64. package/dist/scripts/package-tools.d.ts +22 -0
  65. package/dist/scripts/package-tools.js +255 -0
  66. package/dist/scripts/patch-starlight-content-path.d.ts +1 -0
  67. package/dist/scripts/patch-starlight-content-path.js +172 -0
  68. package/dist/scripts/paths.d.ts +17 -0
  69. package/dist/scripts/paths.js +26 -0
  70. package/dist/scripts/publish-package.d.ts +1 -0
  71. package/dist/scripts/publish-package.js +19 -0
  72. package/dist/scripts/release-verify.d.ts +1 -0
  73. package/dist/scripts/release-verify.js +136 -0
  74. package/dist/scripts/run-fixture-astro-command.d.ts +1 -0
  75. package/dist/scripts/run-fixture-astro-command.js +18 -0
  76. package/dist/scripts/save-deploy-preflight-lib.d.ts +34 -0
  77. package/dist/scripts/save-deploy-preflight-lib.js +69 -0
  78. package/dist/scripts/scaffold-site.d.ts +2 -0
  79. package/dist/scripts/scaffold-site.js +92 -0
  80. package/dist/scripts/stop-mailpit.d.ts +1 -0
  81. package/dist/scripts/stop-mailpit.js +5 -0
  82. package/dist/scripts/sync-dev-vars.d.ts +1 -0
  83. package/dist/scripts/sync-dev-vars.js +6 -0
  84. package/dist/scripts/template-registry-lib.d.ts +47 -0
  85. package/dist/scripts/template-registry-lib.js +137 -0
  86. package/dist/scripts/tenant-astro-command.d.ts +1 -0
  87. package/dist/scripts/tenant-astro-command.js +3 -0
  88. package/dist/scripts/tenant-build.d.ts +1 -0
  89. package/dist/scripts/tenant-build.js +16 -0
  90. package/dist/scripts/tenant-check.d.ts +1 -0
  91. package/dist/scripts/tenant-check.js +7 -0
  92. package/dist/scripts/tenant-d1-migrate-local.d.ts +1 -0
  93. package/dist/scripts/tenant-d1-migrate-local.js +11 -0
  94. package/dist/scripts/tenant-deploy.d.ts +2 -0
  95. package/dist/scripts/tenant-deploy.js +180 -0
  96. package/dist/scripts/tenant-destroy.d.ts +2 -0
  97. package/dist/scripts/tenant-destroy.js +104 -0
  98. package/dist/scripts/tenant-dev.d.ts +1 -0
  99. package/dist/scripts/tenant-dev.js +171 -0
  100. package/dist/scripts/tenant-lint.d.ts +1 -0
  101. package/dist/scripts/tenant-lint.js +4 -0
  102. package/dist/scripts/tenant-test.d.ts +1 -0
  103. package/dist/scripts/tenant-test.js +4 -0
  104. package/dist/scripts/test-cloudflare-local.d.ts +1 -0
  105. package/dist/scripts/test-cloudflare-local.js +212 -0
  106. package/dist/scripts/test-scaffold.d.ts +2 -0
  107. package/dist/scripts/test-scaffold.js +297 -0
  108. package/dist/scripts/treeseed.d.ts +2 -0
  109. package/dist/scripts/treeseed.js +4 -0
  110. package/dist/scripts/validate-templates.d.ts +2 -0
  111. package/dist/scripts/validate-templates.js +4 -0
  112. package/dist/scripts/watch-dev-lib.d.ts +21 -0
  113. package/dist/scripts/watch-dev-lib.js +277 -0
  114. package/dist/scripts/workspace-close.d.ts +2 -0
  115. package/dist/scripts/workspace-close.js +24 -0
  116. package/dist/scripts/workspace-command-e2e.d.ts +2 -0
  117. package/dist/scripts/workspace-command-e2e.js +718 -0
  118. package/dist/scripts/workspace-lint.d.ts +1 -0
  119. package/dist/scripts/workspace-lint.js +9 -0
  120. package/dist/scripts/workspace-preflight-lib.d.ts +36 -0
  121. package/dist/scripts/workspace-preflight-lib.js +179 -0
  122. package/dist/scripts/workspace-preflight.d.ts +2 -0
  123. package/dist/scripts/workspace-preflight.js +22 -0
  124. package/dist/scripts/workspace-publish-changed-packages.d.ts +1 -0
  125. package/dist/scripts/workspace-publish-changed-packages.js +16 -0
  126. package/dist/scripts/workspace-release-verify.d.ts +1 -0
  127. package/dist/scripts/workspace-release-verify.js +81 -0
  128. package/dist/scripts/workspace-release.d.ts +2 -0
  129. package/dist/scripts/workspace-release.js +42 -0
  130. package/dist/scripts/workspace-save-lib.d.ts +42 -0
  131. package/dist/scripts/workspace-save-lib.js +220 -0
  132. package/dist/scripts/workspace-save.d.ts +2 -0
  133. package/dist/scripts/workspace-save.js +124 -0
  134. package/dist/scripts/workspace-start-warning.d.ts +0 -0
  135. package/dist/scripts/workspace-start-warning.js +3 -0
  136. package/dist/scripts/workspace-start.d.ts +2 -0
  137. package/dist/scripts/workspace-start.js +71 -0
  138. package/dist/scripts/workspace-test-unit.d.ts +1 -0
  139. package/dist/scripts/workspace-test-unit.js +4 -0
  140. package/dist/scripts/workspace-test.d.ts +1 -0
  141. package/dist/scripts/workspace-test.js +11 -0
  142. package/dist/scripts/workspace-tools.d.ts +13 -0
  143. package/dist/scripts/workspace-tools.js +226 -0
  144. package/dist/src/cli/handlers/close.d.ts +2 -0
  145. package/dist/src/cli/handlers/config.d.ts +2 -0
  146. package/dist/src/cli/handlers/continue.d.ts +2 -0
  147. package/dist/src/cli/handlers/deploy.d.ts +2 -0
  148. package/dist/src/cli/handlers/destroy.d.ts +2 -0
  149. package/dist/src/cli/handlers/doctor.d.ts +2 -0
  150. package/dist/src/cli/handlers/init.d.ts +2 -0
  151. package/dist/src/cli/handlers/next.d.ts +2 -0
  152. package/dist/src/cli/handlers/prepare.d.ts +2 -0
  153. package/dist/src/cli/handlers/promote.d.ts +2 -0
  154. package/dist/src/cli/handlers/publish.d.ts +2 -0
  155. package/dist/src/cli/handlers/release.d.ts +2 -0
  156. package/dist/src/cli/handlers/rollback.d.ts +2 -0
  157. package/dist/src/cli/handlers/save.d.ts +2 -0
  158. package/dist/src/cli/handlers/setup.d.ts +2 -0
  159. package/dist/src/cli/handlers/ship.d.ts +2 -0
  160. package/dist/src/cli/handlers/start.d.ts +3 -0
  161. package/dist/src/cli/handlers/status.d.ts +2 -0
  162. package/dist/src/cli/handlers/teardown.d.ts +2 -0
  163. package/dist/src/cli/handlers/utils.d.ts +18 -0
  164. package/dist/src/cli/handlers/work.d.ts +2 -0
  165. package/dist/src/cli/help.d.ts +4 -0
  166. package/dist/src/cli/main.d.ts +6 -0
  167. package/dist/src/cli/parser.d.ts +3 -0
  168. package/dist/src/cli/registry.d.ts +27 -0
  169. package/dist/src/cli/repair.d.ts +6 -0
  170. package/dist/src/cli/runtime.d.ts +4 -0
  171. package/dist/src/cli/types.d.ts +71 -0
  172. package/dist/src/cli/workflow-state.d.ts +49 -0
  173. package/dist/src/index.d.ts +1 -0
  174. package/package.json +57 -0
@@ -0,0 +1,505 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
+ import { getTreeseedEnvironmentSuggestedValues, resolveTreeseedEnvironmentRegistry, TREESEED_ENVIRONMENT_SCOPES, validateTreeseedEnvironmentValues, } from '@treeseed/core/environment';
7
+ import { loadTreeseedManifest } from '@treeseed/core/tenant-config';
8
+ import { createPersistentDeployTarget, ensureGeneratedWranglerConfig, markDeploymentInitialized, provisionCloudflareResources, syncCloudflareSecrets, } from './deploy-lib.js';
9
+ import { maybeResolveGitHubRepositorySlug } from './github-automation-lib.js';
10
+ import { loadCliDeployConfig, withProcessCwd } from './package-tools.js';
11
+ const MACHINE_CONFIG_RELATIVE_PATH = '.treeseed/config/machine.yaml';
12
+ const MACHINE_KEY_RELATIVE_PATH = '.treeseed/config/machine.key';
13
+ const TENANT_ENVIRONMENT_OVERLAY_PATH = 'src/env.yaml';
14
+ function ensureParent(filePath) {
15
+ mkdirSync(dirname(filePath), { recursive: true });
16
+ }
17
+ function parseEnvFile(contents) {
18
+ return contents
19
+ .split(/\r?\n/)
20
+ .map((line) => line.trim())
21
+ .filter((line) => line && !line.startsWith('#'))
22
+ .reduce((acc, line) => {
23
+ const separatorIndex = line.indexOf('=');
24
+ if (separatorIndex === -1) {
25
+ return acc;
26
+ }
27
+ acc[line.slice(0, separatorIndex).trim()] = line.slice(separatorIndex + 1);
28
+ return acc;
29
+ }, {});
30
+ }
31
+ function readEnvFileIfPresent(filePath) {
32
+ if (!existsSync(filePath)) {
33
+ return {};
34
+ }
35
+ return parseEnvFile(readFileSync(filePath, 'utf8'));
36
+ }
37
+ function maskValue(value) {
38
+ if (!value) {
39
+ return '(unset)';
40
+ }
41
+ if (value.length <= 8) {
42
+ return '********';
43
+ }
44
+ return `${value.slice(0, 3)}...${value.slice(-3)}`;
45
+ }
46
+ function writeDeploySummary(write, summary) {
47
+ write('Treeseed deployment summary');
48
+ write(` Target: ${summary.target}`);
49
+ write(` Worker: ${summary.workerName}`);
50
+ write(` Site URL: ${summary.siteUrl}`);
51
+ write(` Account ID: ${summary.accountId}`);
52
+ write(` D1: ${summary.siteDataDb.databaseName} (${summary.siteDataDb.databaseId})`);
53
+ write(` KV FORM_GUARD_KV: ${summary.formGuardKv.id}`);
54
+ write(` KV SESSION: ${summary.sessionKv.id}`);
55
+ }
56
+ function loadTenantDeployConfig(tenantRoot) {
57
+ return loadCliDeployConfig(tenantRoot);
58
+ }
59
+ function loadOptionalTenantManifest(tenantRoot) {
60
+ try {
61
+ return withProcessCwd(tenantRoot, () => loadTreeseedManifest());
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ export function getTreeseedMachineConfigPaths(tenantRoot) {
68
+ return {
69
+ configPath: resolve(tenantRoot, MACHINE_CONFIG_RELATIVE_PATH),
70
+ keyPath: resolve(tenantRoot, MACHINE_KEY_RELATIVE_PATH),
71
+ };
72
+ }
73
+ export function createDefaultTreeseedMachineConfig({ tenantRoot, deployConfig, tenantConfig }) {
74
+ return {
75
+ version: 1,
76
+ project: {
77
+ tenantRoot,
78
+ tenantId: tenantConfig?.id ?? deployConfig.slug,
79
+ slug: deployConfig.slug,
80
+ name: deployConfig.name,
81
+ siteUrl: deployConfig.siteUrl,
82
+ overlayPath: resolve(tenantRoot, TENANT_ENVIRONMENT_OVERLAY_PATH),
83
+ },
84
+ settings: {
85
+ sync: {
86
+ github: true,
87
+ cloudflare: true,
88
+ },
89
+ },
90
+ environments: Object.fromEntries(TREESEED_ENVIRONMENT_SCOPES.map((scope) => [
91
+ scope,
92
+ {
93
+ values: {},
94
+ secrets: {},
95
+ },
96
+ ])),
97
+ };
98
+ }
99
+ function loadMachineKey(tenantRoot) {
100
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
101
+ if (existsSync(keyPath)) {
102
+ return Buffer.from(readFileSync(keyPath, 'utf8').trim(), 'base64');
103
+ }
104
+ const key = randomBytes(32);
105
+ ensureParent(keyPath);
106
+ writeFileSync(keyPath, `${key.toString('base64')}\n`, { mode: 0o600 });
107
+ return key;
108
+ }
109
+ function encryptValue(value, key) {
110
+ const iv = randomBytes(12);
111
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
112
+ const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
113
+ const tag = cipher.getAuthTag();
114
+ return {
115
+ algorithm: 'aes-256-gcm',
116
+ iv: iv.toString('base64'),
117
+ tag: tag.toString('base64'),
118
+ ciphertext: ciphertext.toString('base64'),
119
+ };
120
+ }
121
+ function decryptValue(payload, key) {
122
+ if (!payload || typeof payload !== 'object') {
123
+ return '';
124
+ }
125
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(String(payload.iv ?? ''), 'base64'));
126
+ decipher.setAuthTag(Buffer.from(String(payload.tag ?? ''), 'base64'));
127
+ const decrypted = Buffer.concat([
128
+ decipher.update(Buffer.from(String(payload.ciphertext ?? ''), 'base64')),
129
+ decipher.final(),
130
+ ]);
131
+ return decrypted.toString('utf8');
132
+ }
133
+ export function loadTreeseedMachineConfig(tenantRoot) {
134
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
135
+ const tenantConfig = loadOptionalTenantManifest(tenantRoot);
136
+ const defaults = createDefaultTreeseedMachineConfig({ tenantRoot, deployConfig, tenantConfig });
137
+ const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
138
+ if (!existsSync(configPath)) {
139
+ return defaults;
140
+ }
141
+ const raw = parseYaml(readFileSync(configPath, 'utf8')) ?? {};
142
+ const parsed = raw && typeof raw === 'object' ? raw : {};
143
+ return {
144
+ ...defaults,
145
+ ...parsed,
146
+ project: {
147
+ ...defaults.project,
148
+ ...(parsed.project ?? {}),
149
+ },
150
+ settings: {
151
+ ...defaults.settings,
152
+ ...(parsed.settings ?? {}),
153
+ sync: {
154
+ ...defaults.settings.sync,
155
+ ...(parsed.settings?.sync ?? {}),
156
+ },
157
+ },
158
+ environments: Object.fromEntries(TREESEED_ENVIRONMENT_SCOPES.map((scope) => [
159
+ scope,
160
+ {
161
+ values: {
162
+ ...(defaults.environments?.[scope]?.values ?? {}),
163
+ ...(parsed.environments?.[scope]?.values ?? {}),
164
+ },
165
+ secrets: {
166
+ ...(defaults.environments?.[scope]?.secrets ?? {}),
167
+ ...(parsed.environments?.[scope]?.secrets ?? {}),
168
+ },
169
+ },
170
+ ])),
171
+ };
172
+ }
173
+ export function writeTreeseedMachineConfig(tenantRoot, config) {
174
+ const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
175
+ ensureParent(configPath);
176
+ writeFileSync(configPath, stringifyYaml(config), 'utf8');
177
+ }
178
+ export function ensureTreeseedGitignoreEntries(tenantRoot) {
179
+ const gitignorePath = resolve(tenantRoot, '.gitignore');
180
+ const requiredEntries = ['.env.local', '.dev.vars', '.treeseed/'];
181
+ const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
182
+ const lines = current.split(/\r?\n/);
183
+ let changed = false;
184
+ for (const entry of requiredEntries) {
185
+ if (!lines.includes(entry)) {
186
+ lines.push(entry);
187
+ changed = true;
188
+ }
189
+ }
190
+ if (changed || !existsSync(gitignorePath)) {
191
+ writeFileSync(gitignorePath, `${lines.filter(Boolean).join('\n')}\n`, 'utf8');
192
+ }
193
+ return gitignorePath;
194
+ }
195
+ export function resolveTreeseedMachineEnvironmentValues(tenantRoot, scope) {
196
+ const key = loadMachineKey(tenantRoot);
197
+ const config = loadTreeseedMachineConfig(tenantRoot);
198
+ const values = {
199
+ ...(config.environments?.[scope]?.values ?? {}),
200
+ };
201
+ for (const [entryId, payload] of Object.entries(config.environments?.[scope]?.secrets ?? {})) {
202
+ values[entryId] = decryptValue(payload, key);
203
+ }
204
+ return values;
205
+ }
206
+ export function setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, value) {
207
+ const key = loadMachineKey(tenantRoot);
208
+ const config = loadTreeseedMachineConfig(tenantRoot);
209
+ const scoped = config.environments[scope];
210
+ if (entry.sensitivity === 'secret') {
211
+ delete scoped.values[entry.id];
212
+ if (value) {
213
+ scoped.secrets[entry.id] = encryptValue(value, key);
214
+ }
215
+ else {
216
+ delete scoped.secrets[entry.id];
217
+ }
218
+ }
219
+ else {
220
+ delete scoped.secrets[entry.id];
221
+ if (value) {
222
+ scoped.values[entry.id] = value;
223
+ }
224
+ else {
225
+ delete scoped.values[entry.id];
226
+ }
227
+ }
228
+ writeTreeseedMachineConfig(tenantRoot, config);
229
+ return config;
230
+ }
231
+ export function collectTreeseedEnvironmentContext(tenantRoot) {
232
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
233
+ const tenantConfig = loadOptionalTenantManifest(tenantRoot);
234
+ return resolveTreeseedEnvironmentRegistry({
235
+ deployConfig,
236
+ tenantConfig,
237
+ });
238
+ }
239
+ export function collectTreeseedConfigSeedValues(tenantRoot, scope) {
240
+ return {
241
+ ...readEnvFileIfPresent(resolve(tenantRoot, '.env.local')),
242
+ ...readEnvFileIfPresent(resolve(tenantRoot, '.dev.vars')),
243
+ ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
244
+ };
245
+ }
246
+ export function applyTreeseedEnvironmentToProcess({ tenantRoot, scope }) {
247
+ const resolvedValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
248
+ for (const [key, value] of Object.entries(resolvedValues)) {
249
+ if ((process.env[key] ?? '').length === 0 && typeof value === 'string' && value.length > 0) {
250
+ process.env[key] = value;
251
+ }
252
+ }
253
+ return resolvedValues;
254
+ }
255
+ export function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
256
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
257
+ const machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
258
+ const values = {
259
+ ...machineValues,
260
+ ...Object.fromEntries(Object.entries(process.env).map(([key, value]) => [key, value ?? undefined])),
261
+ };
262
+ const validation = validateTreeseedEnvironmentValues({
263
+ values,
264
+ scope,
265
+ purpose,
266
+ deployConfig: registry.context.deployConfig,
267
+ tenantConfig: registry.context.tenantConfig,
268
+ plugins: registry.context.plugins,
269
+ });
270
+ return {
271
+ registry,
272
+ values,
273
+ validation,
274
+ };
275
+ }
276
+ export function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
277
+ const report = validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose });
278
+ if (report.validation.ok) {
279
+ return report;
280
+ }
281
+ const lines = [
282
+ `Treeseed environment is not ready for ${purpose} (${scope}).`,
283
+ 'Run `treeseed config` to fill in the missing values, or export them in the current shell.',
284
+ ];
285
+ for (const problem of [...report.validation.missing, ...report.validation.invalid]) {
286
+ lines.push(`- ${problem.message}`);
287
+ }
288
+ const error = new Error(lines.join('\n'));
289
+ error.kind = report.validation.missing.length > 0 ? 'missing_config' : 'invalid_config';
290
+ error.details = report.validation;
291
+ throw error;
292
+ }
293
+ function renderEnvEntries(entries, values) {
294
+ return entries
295
+ .map((entry) => [entry.id, values[entry.id]])
296
+ .filter(([, value]) => typeof value === 'string' && value.length > 0)
297
+ .map(([key, value]) => `${key}=${value}`)
298
+ .join('\n');
299
+ }
300
+ export function writeTreeseedLocalEnvironmentFiles(tenantRoot) {
301
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
302
+ const scope = 'local';
303
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
304
+ const envEntries = registry.entries.filter((entry) => entry.scopes.includes(scope)
305
+ && entry.targets.includes('local-file'));
306
+ const devVarsEntries = registry.entries.filter((entry) => entry.scopes.includes(scope)
307
+ && entry.targets.includes('wrangler-dev-vars'));
308
+ writeFileSync(resolve(tenantRoot, '.env.local'), `${renderEnvEntries(envEntries, values)}\n`, 'utf8');
309
+ writeFileSync(resolve(tenantRoot, '.dev.vars'), `${renderEnvEntries(devVarsEntries, values)}\n`, 'utf8');
310
+ return {
311
+ envLocalPath: resolve(tenantRoot, '.env.local'),
312
+ devVarsPath: resolve(tenantRoot, '.dev.vars'),
313
+ };
314
+ }
315
+ function runGh(args, { cwd, dryRun = false, input } = {}) {
316
+ if (dryRun) {
317
+ return { status: 0, stdout: '', stderr: '' };
318
+ }
319
+ const result = spawnSync('gh', args, {
320
+ cwd,
321
+ stdio: input !== undefined ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
322
+ encoding: 'utf8',
323
+ input,
324
+ });
325
+ if (result.status !== 0) {
326
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(' ')} failed`);
327
+ }
328
+ return result;
329
+ }
330
+ function listGitHubNames(command, repository, tenantRoot) {
331
+ const result = runGh([command, 'list', '--repo', repository, '--json', 'name'], { cwd: tenantRoot });
332
+ return new Set((JSON.parse(result.stdout || '[]')).map((entry) => entry?.name).filter(Boolean));
333
+ }
334
+ export function syncTreeseedGitHubEnvironment({ tenantRoot, scope = 'prod', dryRun = false } = {}) {
335
+ const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
336
+ if (!repository) {
337
+ throw new Error('Unable to determine the GitHub repository from the origin remote.');
338
+ }
339
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
340
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
341
+ const relevant = registry.entries.filter((entry) => entry.scopes.includes(scope));
342
+ const secretNames = listGitHubNames('secret', repository, tenantRoot);
343
+ const variableNames = listGitHubNames('variable', repository, tenantRoot);
344
+ const synced = {
345
+ secrets: [],
346
+ variables: [],
347
+ };
348
+ for (const entry of relevant) {
349
+ const value = values[entry.id];
350
+ if (!value) {
351
+ continue;
352
+ }
353
+ if (entry.targets.includes('github-secret')) {
354
+ runGh(['secret', 'set', entry.id, '--repo', repository, '--body', value], { cwd: tenantRoot, dryRun });
355
+ synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
356
+ }
357
+ if (entry.targets.includes('github-variable')) {
358
+ runGh(['variable', 'set', entry.id, '--repo', repository, '--body', value], { cwd: tenantRoot, dryRun });
359
+ synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
360
+ }
361
+ }
362
+ return {
363
+ repository,
364
+ scope,
365
+ ...synced,
366
+ };
367
+ }
368
+ export function syncTreeseedCloudflareEnvironment({ tenantRoot, scope = 'prod', dryRun = false } = {}) {
369
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
370
+ const target = createPersistentDeployTarget(scope);
371
+ for (const [key, value] of Object.entries(values)) {
372
+ if (typeof value === 'string' && value.length > 0) {
373
+ process.env[key] = value;
374
+ }
375
+ }
376
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
377
+ const syncedSecrets = syncCloudflareSecrets(tenantRoot, { dryRun, target });
378
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
379
+ const cloudflareVars = registry.entries
380
+ .filter((entry) => entry.scopes.includes(scope) && entry.targets.includes('cloudflare-var'))
381
+ .map((entry) => entry.id)
382
+ .filter((key) => typeof values[key] === 'string' && values[key].length > 0);
383
+ return {
384
+ scope,
385
+ target,
386
+ wranglerPath,
387
+ secrets: syncedSecrets,
388
+ varsManagedByWranglerConfig: cloudflareVars,
389
+ };
390
+ }
391
+ export function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = 'prod', dryRun = false } = {}) {
392
+ const normalizedScope = scope === 'prod' ? 'prod' : scope;
393
+ const target = createPersistentDeployTarget(normalizedScope);
394
+ const summary = provisionCloudflareResources(tenantRoot, { dryRun, target });
395
+ ensureGeneratedWranglerConfig(tenantRoot, { target });
396
+ const syncedSecrets = syncCloudflareSecrets(tenantRoot, { dryRun, target });
397
+ if (!dryRun) {
398
+ markDeploymentInitialized(tenantRoot, { target });
399
+ }
400
+ return {
401
+ scope: normalizedScope,
402
+ target,
403
+ summary,
404
+ secrets: syncedSecrets,
405
+ };
406
+ }
407
+ export async function runTreeseedConfigWizard({ tenantRoot, scopes = ['local', 'staging', 'prod'], sync = 'none', prompt, authStatus, write = console.log, }) {
408
+ ensureTreeseedGitignoreEntries(tenantRoot);
409
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
410
+ const groups = ['local-development', 'forms', 'smtp', 'cloudflare'];
411
+ const summary = {
412
+ scopes,
413
+ updated: [],
414
+ synced: {},
415
+ initialized: [],
416
+ };
417
+ for (const scope of scopes) {
418
+ const existingValues = collectTreeseedConfigSeedValues(tenantRoot, scope);
419
+ const suggested = getTreeseedEnvironmentSuggestedValues({
420
+ scope,
421
+ deployConfig: registry.context.deployConfig,
422
+ tenantConfig: registry.context.tenantConfig,
423
+ plugins: registry.context.plugins,
424
+ });
425
+ write(`\nTreeseed configuration for ${scope}`);
426
+ write(`Tenant: ${registry.context.deployConfig.name} (${registry.context.deployConfig.slug})`);
427
+ if (authStatus) {
428
+ write(`GitHub auth: ${authStatus.gh?.authenticated ? 'ready' : 'not ready'}`);
429
+ write(`Wrangler auth: ${authStatus.wrangler?.authenticated ? 'ready' : 'not ready'}`);
430
+ }
431
+ for (const group of groups) {
432
+ const groupEntries = registry.entries.filter((entry) => entry.group === group
433
+ && entry.scopes.includes(scope)
434
+ && (!entry.isRelevant || entry.isRelevant(registry.context, scope, 'config')));
435
+ if (groupEntries.length === 0) {
436
+ continue;
437
+ }
438
+ write(`\n[${group}]`);
439
+ for (const entry of groupEntries) {
440
+ const currentValue = existingValues[entry.id];
441
+ const suggestedValue = suggested[entry.id];
442
+ const displayValue = currentValue ?? suggestedValue ?? '';
443
+ write(`\n${entry.label} (${entry.id})`);
444
+ write(`Why: ${entry.description}`);
445
+ write(`How to get it: ${entry.howToGet}`);
446
+ write(`Used for: ${entry.purposes.join(', ')}`);
447
+ write(`Targets: ${entry.targets.join(', ')}`);
448
+ write(`Current: ${entry.sensitivity === 'secret' ? maskValue(currentValue) : currentValue ?? '(unset)'}`);
449
+ const answer = (await prompt(`${entry.id}${displayValue ? ` [${entry.sensitivity === 'secret' ? 'keep current' : displayValue}]` : ''}: `)).trim();
450
+ if (answer === '' && displayValue) {
451
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, displayValue);
452
+ summary.updated.push({ scope, id: entry.id, reused: true });
453
+ continue;
454
+ }
455
+ if (answer === '' && !displayValue) {
456
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, '');
457
+ continue;
458
+ }
459
+ if (answer === '-') {
460
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, '');
461
+ summary.updated.push({ scope, id: entry.id, cleared: true });
462
+ continue;
463
+ }
464
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, answer);
465
+ summary.updated.push({ scope, id: entry.id, reused: false });
466
+ }
467
+ }
468
+ const validation = validateTreeseedEnvironmentValues({
469
+ values: resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
470
+ scope,
471
+ purpose: 'config',
472
+ deployConfig: registry.context.deployConfig,
473
+ tenantConfig: registry.context.tenantConfig,
474
+ plugins: registry.context.plugins,
475
+ });
476
+ if (!validation.ok) {
477
+ const details = [...validation.missing, ...validation.invalid]
478
+ .map((problem) => `- ${problem.message}`);
479
+ join('\n');
480
+ throw new Error(`Treeseed config validation failed for ${scope}:\n${details}`);
481
+ }
482
+ }
483
+ writeTreeseedLocalEnvironmentFiles(tenantRoot);
484
+ for (const scope of scopes) {
485
+ if (scope === 'local') {
486
+ continue;
487
+ }
488
+ const initialized = initializeTreeseedPersistentEnvironment({ tenantRoot, scope });
489
+ if (write) {
490
+ writeDeploySummary(write, initialized.summary);
491
+ }
492
+ summary.initialized.push({
493
+ scope,
494
+ secrets: initialized.secrets.length,
495
+ target: initialized.summary.target,
496
+ });
497
+ }
498
+ if (sync === 'github' || sync === 'all') {
499
+ summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? 'prod' });
500
+ }
501
+ if (sync === 'cloudflare' || sync === 'all') {
502
+ summary.synced.cloudflare = syncTreeseedCloudflareEnvironment({ tenantRoot, scope: scopes.at(-1) ?? 'prod' });
503
+ }
504
+ return summary;
505
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import readline from 'node:readline/promises';
3
+ import { stdin as input, stdout as output } from 'node:process';
4
+ import { collectCliPreflight } from './workspace-preflight-lib.js';
5
+ import { applyTreeseedEnvironmentToProcess, ensureTreeseedGitignoreEntries, getTreeseedMachineConfigPaths, runTreeseedConfigWizard, writeTreeseedLocalEnvironmentFiles, } from './config-runtime-lib.js';
6
+ const tenantRoot = process.cwd();
7
+ function parseArgs(argv) {
8
+ const parsed = {
9
+ scopes: [],
10
+ sync: 'none',
11
+ };
12
+ const rest = [...argv];
13
+ while (rest.length) {
14
+ const current = rest.shift();
15
+ if (!current) {
16
+ continue;
17
+ }
18
+ if (current === '--environment') {
19
+ parsed.scopes.push(rest.shift() ?? '');
20
+ continue;
21
+ }
22
+ if (current.startsWith('--environment=')) {
23
+ parsed.scopes.push(current.split('=', 2)[1] ?? '');
24
+ continue;
25
+ }
26
+ if (current === '--sync') {
27
+ parsed.sync = rest.shift() ?? 'none';
28
+ continue;
29
+ }
30
+ if (current.startsWith('--sync=')) {
31
+ parsed.sync = current.split('=', 2)[1] ?? 'none';
32
+ continue;
33
+ }
34
+ throw new Error(`Unknown config argument: ${current}`);
35
+ }
36
+ return parsed;
37
+ }
38
+ const options = parseArgs(process.argv.slice(2));
39
+ const scopes = options.scopes.length > 0 ? options.scopes : ['local', 'staging', 'prod'];
40
+ ensureTreeseedGitignoreEntries(tenantRoot);
41
+ const preflight = collectCliPreflight({ cwd: tenantRoot, requireAuth: false });
42
+ const rl = readline.createInterface({ input, output });
43
+ try {
44
+ console.log('Treeseed configuration wizard');
45
+ console.log('This command writes a local machine config, generates .env.local and .dev.vars, and can sync GitHub or Cloudflare settings.');
46
+ console.log('Enter a value to set it, press Enter to keep the current/default value, or enter "-" to clear a value.\n');
47
+ const result = await runTreeseedConfigWizard({
48
+ tenantRoot,
49
+ scopes,
50
+ sync: options.sync,
51
+ authStatus: preflight.checks.auth,
52
+ prompt: async (message) => {
53
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
54
+ return '';
55
+ }
56
+ try {
57
+ return await rl.question(message);
58
+ }
59
+ catch {
60
+ return '';
61
+ }
62
+ },
63
+ });
64
+ writeTreeseedLocalEnvironmentFiles(tenantRoot);
65
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope: 'local' });
66
+ const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
67
+ console.log('\nTreeseed config completed.');
68
+ console.log(`Machine config: ${configPath}`);
69
+ console.log(`Machine key: ${keyPath}`);
70
+ console.log(`Updated values: ${result.updated.length}`);
71
+ console.log(`Initialized environments: ${result.initialized.length}`);
72
+ if (result.synced.github) {
73
+ console.log(`GitHub sync: ${result.synced.github.secrets.length} secrets, ${result.synced.github.variables.length} variables (${result.synced.github.repository})`);
74
+ }
75
+ if (result.synced.cloudflare) {
76
+ console.log(`Cloudflare sync: ${result.synced.cloudflare.secrets.length} secrets, ${result.synced.cloudflare.varsManagedByWranglerConfig.length} vars via Wrangler config`);
77
+ }
78
+ }
79
+ finally {
80
+ rl.close();
81
+ }
@@ -0,0 +1,6 @@
1
+ export declare function runLocalD1Migrations({ cwd, wranglerConfig, migrationsRoot, persistTo }: {
2
+ cwd: any;
3
+ wranglerConfig: any;
4
+ migrationsRoot: any;
5
+ persistTo: any;
6
+ }): void;
@@ -0,0 +1,90 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { resolveWranglerBin } from './package-tools.js';
5
+ const DATABASE_BINDING = 'SITE_DATA_DB';
6
+ function runWrangler(args, { cwd, capture = false } = {}) {
7
+ return spawnSync(process.execPath, [resolveWranglerBin(), ...args], {
8
+ cwd,
9
+ env: { ...process.env },
10
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
11
+ encoding: capture ? 'utf8' : undefined,
12
+ });
13
+ }
14
+ function executeSqlFile({ cwd, wranglerConfig, filePath, persistTo }) {
15
+ const args = ['d1', 'execute', DATABASE_BINDING, '--local', '--config', wranglerConfig, '--file', filePath];
16
+ if (persistTo) {
17
+ args.push('--persist-to', persistTo);
18
+ }
19
+ const result = runWrangler(args, { cwd });
20
+ if (result.status !== 0) {
21
+ process.exit(result.status ?? 1);
22
+ }
23
+ }
24
+ function executeSqlCommand({ cwd, wranglerConfig, command, persistTo, capture = false }) {
25
+ const args = ['d1', 'execute', DATABASE_BINDING, '--local', '--config', wranglerConfig, '--command', command];
26
+ if (persistTo) {
27
+ args.push('--persist-to', persistTo);
28
+ }
29
+ const result = runWrangler(args, { cwd, capture });
30
+ if (result.status !== 0) {
31
+ if (capture) {
32
+ if (result.stdout)
33
+ process.stdout.write(result.stdout);
34
+ if (result.stderr)
35
+ process.stderr.write(result.stderr);
36
+ }
37
+ process.exit(result.status ?? 1);
38
+ }
39
+ return result;
40
+ }
41
+ function ensureSchemaMigrationsTable({ cwd, wranglerConfig, persistTo }) {
42
+ executeSqlCommand({
43
+ cwd,
44
+ wranglerConfig,
45
+ persistTo,
46
+ command: `CREATE TABLE IF NOT EXISTS treeseed_schema_migrations (
47
+ name TEXT PRIMARY KEY,
48
+ applied_at TEXT NOT NULL
49
+ );`,
50
+ });
51
+ }
52
+ function loadAppliedMigrations({ cwd, wranglerConfig, persistTo }) {
53
+ const result = executeSqlCommand({
54
+ cwd,
55
+ wranglerConfig,
56
+ persistTo,
57
+ capture: true,
58
+ command: 'SELECT name FROM treeseed_schema_migrations ORDER BY name ASC;',
59
+ });
60
+ const parsed = JSON.parse(result.stdout);
61
+ const rows = (Array.isArray(parsed) ? parsed : [parsed]).flatMap((entry) => entry.results ?? []);
62
+ return new Set(rows.map((row) => row.name).filter(Boolean));
63
+ }
64
+ function markMigrationApplied({ cwd, wranglerConfig, persistTo, migration }) {
65
+ executeSqlCommand({
66
+ cwd,
67
+ wranglerConfig,
68
+ persistTo,
69
+ command: `INSERT OR REPLACE INTO treeseed_schema_migrations (name, applied_at) VALUES ('${migration.replace(/'/g, "''")}', datetime('now'));`,
70
+ });
71
+ }
72
+ export function runLocalD1Migrations({ cwd, wranglerConfig, migrationsRoot, persistTo }) {
73
+ ensureSchemaMigrationsTable({ cwd, wranglerConfig, persistTo });
74
+ const appliedMigrations = loadAppliedMigrations({ cwd, wranglerConfig, persistTo });
75
+ const migrations = readdirSync(migrationsRoot)
76
+ .filter((entry) => /^\d+.*\.sql$/i.test(entry))
77
+ .sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
78
+ for (const migration of migrations) {
79
+ if (appliedMigrations.has(migration)) {
80
+ continue;
81
+ }
82
+ const filePath = resolve(migrationsRoot, migration);
83
+ if (!existsSync(filePath)) {
84
+ console.error(`Unable to find migration file at ${filePath}.`);
85
+ process.exit(1);
86
+ }
87
+ executeSqlFile({ cwd, wranglerConfig, filePath, persistTo });
88
+ markMigrationApplied({ cwd, wranglerConfig, persistTo, migration });
89
+ }
90
+ }