@treeseed/sdk 0.1.1 → 0.3.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.
Files changed (228) hide show
  1. package/README.md +97 -494
  2. package/dist/{src/cli-tools.d.ts → cli-tools.d.ts} +1 -1
  3. package/dist/cli-tools.js +5 -3
  4. package/dist/{src/content-store.d.ts → content-store.d.ts} +3 -2
  5. package/dist/content-store.js +52 -20
  6. package/dist/{src/d1-store.d.ts → d1-store.d.ts} +62 -1
  7. package/dist/d1-store.js +625 -65
  8. package/dist/field-aliases.d.ts +11 -0
  9. package/dist/field-aliases.js +41 -0
  10. package/dist/graph/build.d.ts +19 -0
  11. package/dist/graph/build.js +949 -0
  12. package/dist/graph/dsl.d.ts +2 -0
  13. package/dist/graph/dsl.js +243 -0
  14. package/dist/graph/query.d.ts +47 -0
  15. package/dist/graph/query.js +447 -0
  16. package/dist/graph/ranking.d.ts +3 -0
  17. package/dist/graph/ranking.js +483 -0
  18. package/dist/graph/schema.d.ts +142 -0
  19. package/dist/graph/schema.js +133 -0
  20. package/dist/graph.d.ts +52 -0
  21. package/dist/graph.js +133 -0
  22. package/dist/index.d.ts +27 -0
  23. package/dist/index.js +90 -2
  24. package/dist/model-registry.d.ts +8 -0
  25. package/dist/model-registry.js +351 -25
  26. package/dist/operations/providers/default.d.ts +10 -0
  27. package/dist/operations/providers/default.js +514 -0
  28. package/dist/operations/runtime.d.ts +7 -0
  29. package/dist/operations/runtime.js +60 -0
  30. package/dist/operations/services/config-runtime.d.ts +269 -0
  31. package/dist/operations/services/config-runtime.js +1397 -0
  32. package/dist/operations/services/d1-migration.d.ts +6 -0
  33. package/dist/operations/services/d1-migration.js +89 -0
  34. package/dist/operations/services/deploy.d.ts +371 -0
  35. package/dist/operations/services/deploy.js +981 -0
  36. package/dist/operations/services/git-workflow.d.ts +49 -0
  37. package/dist/operations/services/git-workflow.js +218 -0
  38. package/dist/operations/services/github-automation.d.ts +156 -0
  39. package/dist/operations/services/github-automation.js +256 -0
  40. package/dist/operations/services/local-dev.d.ts +9 -0
  41. package/dist/operations/services/local-dev.js +106 -0
  42. package/dist/operations/services/mailpit-runtime.d.ts +4 -0
  43. package/dist/operations/services/mailpit-runtime.js +59 -0
  44. package/dist/operations/services/railway-deploy.d.ts +53 -0
  45. package/dist/operations/services/railway-deploy.js +123 -0
  46. package/dist/operations/services/runtime-paths.d.ts +19 -0
  47. package/dist/operations/services/runtime-paths.js +54 -0
  48. package/dist/operations/services/runtime-tools.d.ts +117 -0
  49. package/dist/operations/services/runtime-tools.js +358 -0
  50. package/dist/operations/services/save-deploy-preflight.d.ts +34 -0
  51. package/dist/operations/services/save-deploy-preflight.js +76 -0
  52. package/dist/operations/services/template-registry.d.ts +88 -0
  53. package/dist/operations/services/template-registry.js +407 -0
  54. package/dist/operations/services/watch-dev.d.ts +21 -0
  55. package/dist/operations/services/watch-dev.js +284 -0
  56. package/dist/operations/services/workspace-preflight.d.ts +40 -0
  57. package/dist/operations/services/workspace-preflight.js +165 -0
  58. package/dist/operations/services/workspace-save.d.ts +42 -0
  59. package/dist/operations/services/workspace-save.js +235 -0
  60. package/dist/operations/services/workspace-tools.d.ts +16 -0
  61. package/dist/operations/services/workspace-tools.js +270 -0
  62. package/dist/operations-registry.d.ts +5 -0
  63. package/dist/operations-registry.js +68 -0
  64. package/dist/operations-types.d.ts +71 -0
  65. package/dist/operations-types.js +17 -0
  66. package/dist/operations.d.ts +6 -0
  67. package/dist/operations.js +16 -0
  68. package/dist/platform/books-data.d.ts +1 -0
  69. package/dist/platform/books-data.js +1 -0
  70. package/dist/platform/contracts.d.ts +158 -0
  71. package/dist/platform/contracts.js +0 -0
  72. package/dist/platform/deploy/config.d.ts +4 -0
  73. package/dist/platform/deploy/config.js +222 -0
  74. package/dist/platform/deploy-config.d.ts +1 -0
  75. package/dist/platform/deploy-config.js +1 -0
  76. package/dist/platform/env.yaml +394 -0
  77. package/dist/platform/environment.d.ts +130 -0
  78. package/dist/platform/environment.js +331 -0
  79. package/dist/platform/plugin.d.ts +2 -0
  80. package/dist/platform/plugin.js +4 -0
  81. package/dist/platform/plugins/constants.d.ts +22 -0
  82. package/dist/platform/plugins/constants.js +29 -0
  83. package/dist/platform/plugins/plugin.d.ts +51 -0
  84. package/dist/platform/plugins/plugin.js +6 -0
  85. package/dist/platform/plugins/runtime.d.ts +35 -0
  86. package/dist/platform/plugins/runtime.js +142 -0
  87. package/dist/platform/plugins.d.ts +5 -0
  88. package/dist/platform/plugins.js +16 -0
  89. package/dist/platform/site-config-schema.js +1 -0
  90. package/dist/platform/tenant/config.d.ts +9 -0
  91. package/dist/platform/tenant/config.js +154 -0
  92. package/dist/platform/tenant/runtime-config.d.ts +4 -0
  93. package/dist/platform/tenant/runtime-config.js +20 -0
  94. package/dist/platform/tenant-config.d.ts +1 -0
  95. package/dist/platform/tenant-config.js +1 -0
  96. package/dist/platform/utils/books-data.d.ts +29 -0
  97. package/dist/platform/utils/books-data.js +82 -0
  98. package/dist/platform/utils/site-config-schema.js +321 -0
  99. package/dist/remote.d.ts +175 -0
  100. package/dist/remote.js +202 -0
  101. package/dist/runtime.js +50 -3
  102. package/dist/scripts/aggregate-book.js +121 -0
  103. package/dist/scripts/build-dist.js +57 -13
  104. package/dist/scripts/build-tenant-worker.js +36 -0
  105. package/dist/scripts/cleanup-markdown.js +373 -0
  106. package/dist/scripts/cli-test-fixtures.js +48 -0
  107. package/dist/scripts/config-treeseed.js +95 -0
  108. package/dist/scripts/ensure-mailpit.js +29 -0
  109. package/dist/scripts/local-dev.js +129 -0
  110. package/dist/scripts/logs-mailpit.js +2 -0
  111. package/dist/scripts/patch-starlight-content-path.js +172 -0
  112. package/dist/scripts/release-verify.js +34 -5
  113. package/dist/scripts/run-fixture-astro-command.js +18 -0
  114. package/dist/scripts/scaffold-site.js +65 -0
  115. package/dist/scripts/stop-mailpit.js +5 -0
  116. package/dist/scripts/sync-dev-vars.js +6 -0
  117. package/dist/scripts/sync-template.js +20 -0
  118. package/dist/scripts/template-catalog.test.js +100 -0
  119. package/dist/scripts/template-command.js +31 -0
  120. package/dist/scripts/tenant-astro-command.js +3 -0
  121. package/dist/scripts/tenant-build.js +16 -0
  122. package/dist/scripts/tenant-check.js +7 -0
  123. package/dist/scripts/tenant-d1-migrate-local.js +11 -0
  124. package/dist/scripts/tenant-deploy.js +180 -0
  125. package/dist/scripts/tenant-destroy.js +104 -0
  126. package/dist/scripts/tenant-dev.js +171 -0
  127. package/dist/scripts/tenant-lint.js +4 -0
  128. package/dist/scripts/tenant-test.js +4 -0
  129. package/dist/scripts/test-cloudflare-local.js +212 -0
  130. package/dist/scripts/test-scaffold.js +314 -0
  131. package/dist/scripts/test-smoke.js +71 -13
  132. package/dist/scripts/treeseed-assert-release-tag-version.js +21 -0
  133. package/dist/scripts/treeseed-build-dist.js +134 -0
  134. package/dist/scripts/treeseed-publish-package.js +19 -0
  135. package/dist/scripts/treeseed-release-verify.js +131 -0
  136. package/dist/scripts/treeseed-run-ts.js +45 -0
  137. package/dist/scripts/validate-templates.js +6 -0
  138. package/dist/scripts/verify-driver.js +29 -0
  139. package/dist/scripts/workflow-commands.test.js +39 -0
  140. package/dist/scripts/workspace-close.js +24 -0
  141. package/dist/scripts/workspace-command-e2e.js +718 -0
  142. package/dist/scripts/workspace-lint.js +9 -0
  143. package/dist/scripts/workspace-preflight.js +22 -0
  144. package/dist/scripts/workspace-publish-changed-packages.js +16 -0
  145. package/dist/scripts/workspace-release-verify.js +81 -0
  146. package/dist/scripts/workspace-release.js +42 -0
  147. package/dist/scripts/workspace-save.js +124 -0
  148. package/dist/scripts/workspace-start-warning.js +3 -0
  149. package/dist/scripts/workspace-start.js +71 -0
  150. package/dist/scripts/workspace-test-unit.js +4 -0
  151. package/dist/scripts/workspace-test.js +11 -0
  152. package/dist/sdk-fields.d.ts +11 -0
  153. package/dist/sdk-fields.js +169 -0
  154. package/dist/sdk-filters.d.ts +4 -0
  155. package/dist/sdk-filters.js +12 -15
  156. package/dist/sdk-types.d.ts +796 -0
  157. package/dist/sdk-types.js +7 -1
  158. package/dist/sdk-version.d.ts +2 -0
  159. package/dist/sdk-version.js +42 -0
  160. package/dist/sdk.d.ts +215 -0
  161. package/dist/sdk.js +235 -11
  162. package/dist/stores/cursor-store.js +9 -3
  163. package/dist/stores/lease-store.js +8 -2
  164. package/dist/{src/stores → stores}/message-store.d.ts +1 -1
  165. package/dist/stores/message-store.js +27 -3
  166. package/dist/stores/operational-store.d.ts +24 -0
  167. package/dist/stores/operational-store.js +279 -0
  168. package/dist/stores/run-store.js +8 -1
  169. package/dist/stores/subscription-store.js +7 -5
  170. package/dist/template-catalog.d.ts +13 -0
  171. package/dist/template-catalog.js +141 -0
  172. package/dist/treeseed/services/compose.yml +7 -0
  173. package/dist/treeseed/template-catalog/catalog.fixture.json +55 -0
  174. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.d.ts +2 -0
  175. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.ts +3 -0
  176. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +32 -0
  177. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +40 -0
  178. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/empty/.gitkeep +1 -0
  179. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/knowledge/handbook/index.mdx +11 -0
  180. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/pages/welcome.mdx +11 -0
  181. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.d.ts +1 -0
  182. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.ts +3 -0
  183. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/env.yaml +1 -0
  184. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +19 -0
  185. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +26 -0
  186. package/dist/treeseed/template-catalog/templates/starter-basic/template/tsconfig.json +9 -0
  187. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +90 -0
  188. package/dist/verification.d.ts +20 -0
  189. package/dist/verification.js +98 -0
  190. package/dist/workflow/operations.d.ts +396 -0
  191. package/dist/workflow/operations.js +841 -0
  192. package/dist/workflow-state.d.ts +56 -0
  193. package/dist/workflow-state.js +195 -0
  194. package/dist/workflow-support.d.ts +9 -0
  195. package/dist/workflow-support.js +176 -0
  196. package/dist/workflow.d.ts +111 -0
  197. package/dist/workflow.js +97 -0
  198. package/package.json +97 -5
  199. package/scripts/verify-driver.mjs +29 -0
  200. package/dist/scripts/.ts-run-1775616845195-odh4xzphk3l.js +0 -22
  201. package/dist/scripts/.ts-run-1775616848931-9386s6kwrl.js +0 -126
  202. package/dist/scripts/assert-release-tag-version.d.ts +0 -1
  203. package/dist/scripts/build-dist.d.ts +0 -1
  204. package/dist/scripts/package-tools.d.ts +0 -15
  205. package/dist/scripts/publish-package.d.ts +0 -1
  206. package/dist/scripts/release-verify.d.ts +0 -1
  207. package/dist/scripts/test-smoke.d.ts +0 -1
  208. package/dist/src/index.d.ts +0 -6
  209. package/dist/src/model-registry.d.ts +0 -4
  210. package/dist/src/sdk-filters.d.ts +0 -4
  211. package/dist/src/sdk-types.d.ts +0 -285
  212. package/dist/src/sdk.d.ts +0 -109
  213. package/dist/test/test-fixture.d.ts +0 -1
  214. package/dist/test/utils/envelopes.test.d.ts +0 -1
  215. package/dist/test/utils/sdk.test.d.ts +0 -1
  216. package/dist/vitest.config.d.ts +0 -2
  217. /package/dist/{src/frontmatter.d.ts → frontmatter.d.ts} +0 -0
  218. /package/dist/{src/git-runtime.d.ts → git-runtime.d.ts} +0 -0
  219. /package/dist/{src/runtime.d.ts → runtime.d.ts} +0 -0
  220. /package/dist/{src/stores → stores}/cursor-store.d.ts +0 -0
  221. /package/dist/{src/stores → stores}/envelopes.d.ts +0 -0
  222. /package/dist/{src/stores → stores}/helpers.d.ts +0 -0
  223. /package/dist/{src/stores → stores}/lease-store.d.ts +0 -0
  224. /package/dist/{src/stores → stores}/run-store.d.ts +0 -0
  225. /package/dist/{src/stores → stores}/subscription-store.d.ts +0 -0
  226. /package/dist/{src/types → types}/agents.d.ts +0 -0
  227. /package/dist/{src/types → types}/cloudflare.d.ts +0 -0
  228. /package/dist/{src/wrangler-d1.d.ts → wrangler-d1.d.ts} +0 -0
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdirSync, writeFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { applyTreeseedEnvironmentToProcess } from '../operations/services/config-runtime.js';
6
+ import { assertDeploymentInitialized, createBranchPreviewDeployTarget, createPersistentDeployTarget, deployTargetLabel, ensureGeneratedWranglerConfig, finalizeDeploymentState, runRemoteD1Migrations, } from '../operations/services/deploy.js';
7
+ import { currentManagedBranch, PRODUCTION_BRANCH, STAGING_BRANCH } from '../operations/services/git-workflow.js';
8
+ import { packageScriptPath, resolveWranglerBin } from '../operations/services/runtime-tools.js';
9
+ import { runTenantDeployPreflight } from '../operations/services/save-deploy-preflight.js';
10
+ const tenantRoot = process.cwd();
11
+ const args = process.argv.slice(2);
12
+ function writeDeployReport(payload) {
13
+ const target = process.env.TREESEED_DEPLOY_REPORT_PATH;
14
+ if (!target) {
15
+ return;
16
+ }
17
+ const filePath = resolve(target);
18
+ mkdirSync(dirname(filePath), { recursive: true });
19
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
20
+ }
21
+ function parseArgs(argv) {
22
+ const parsed = {
23
+ dryRun: false,
24
+ only: null,
25
+ name: null,
26
+ environment: null,
27
+ targetBranch: null,
28
+ };
29
+ const rest = [...argv];
30
+ while (rest.length) {
31
+ const current = rest.shift();
32
+ if (!current)
33
+ continue;
34
+ if (current === '--dry-run') {
35
+ parsed.dryRun = true;
36
+ continue;
37
+ }
38
+ if (current === '--only') {
39
+ parsed.only = rest.shift() ?? null;
40
+ continue;
41
+ }
42
+ if (current === '--name') {
43
+ parsed.name = rest.shift() ?? null;
44
+ continue;
45
+ }
46
+ if (current === '--environment') {
47
+ parsed.environment = rest.shift() ?? null;
48
+ continue;
49
+ }
50
+ if (current.startsWith('--environment=')) {
51
+ parsed.environment = current.split('=', 2)[1] ?? null;
52
+ continue;
53
+ }
54
+ if (current === '--target-branch') {
55
+ parsed.targetBranch = rest.shift() ?? null;
56
+ continue;
57
+ }
58
+ if (current.startsWith('--target-branch=')) {
59
+ parsed.targetBranch = current.split('=', 2)[1] ?? null;
60
+ continue;
61
+ }
62
+ throw new Error(`Unknown deploy argument: ${current}`);
63
+ }
64
+ return parsed;
65
+ }
66
+ function inferEnvironmentFromBranch() {
67
+ const branch = currentManagedBranch(tenantRoot);
68
+ if (branch === STAGING_BRANCH) {
69
+ return 'staging';
70
+ }
71
+ if (branch === PRODUCTION_BRANCH) {
72
+ return 'prod';
73
+ }
74
+ return null;
75
+ }
76
+ function resolveTarget(options) {
77
+ if (options.targetBranch) {
78
+ return {
79
+ target: createBranchPreviewDeployTarget(options.targetBranch),
80
+ scope: 'staging',
81
+ };
82
+ }
83
+ const environment = options.environment ?? (process.env.CI ? inferEnvironmentFromBranch() : null);
84
+ if (!environment) {
85
+ throw new Error('Treeseed deploy requires `--environment local|staging|prod` outside CI.');
86
+ }
87
+ return {
88
+ target: createPersistentDeployTarget(environment),
89
+ scope: environment,
90
+ };
91
+ }
92
+ function runNodeScript(scriptPath, scriptArgs = [], env = {}) {
93
+ const result = spawnSync(process.execPath, [scriptPath, ...scriptArgs], {
94
+ stdio: 'inherit',
95
+ cwd: tenantRoot,
96
+ env: { ...process.env, ...env },
97
+ });
98
+ if (result.status !== 0) {
99
+ process.exit(result.status ?? 1);
100
+ }
101
+ }
102
+ function runWranglerDeploy(configPath) {
103
+ const result = spawnSync(process.execPath, [resolveWranglerBin(), 'deploy', '--config', configPath], {
104
+ stdio: 'inherit',
105
+ cwd: tenantRoot,
106
+ env: { ...process.env },
107
+ });
108
+ if (result.status !== 0) {
109
+ process.exit(result.status ?? 1);
110
+ }
111
+ }
112
+ async function main() {
113
+ const options = parseArgs(args);
114
+ const { target, scope } = resolveTarget(options);
115
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
116
+ const allowedSteps = new Set(['migrate', 'build', 'publish']);
117
+ if (options.only && !allowedSteps.has(options.only)) {
118
+ throw new Error(`Unsupported deploy step "${options.only}". Expected one of ${[...allowedSteps].join(', ')}.`);
119
+ }
120
+ const shouldRun = (step) => !options.only || options.only === step;
121
+ if (options.name) {
122
+ console.log(`Deploy target label: ${options.name}`);
123
+ }
124
+ if (scope === 'local') {
125
+ runTenantDeployPreflight({ cwd: tenantRoot, scope: 'local' });
126
+ runNodeScript(packageScriptPath('tenant-build'));
127
+ writeDeployReport({ ok: true, kind: 'success', scope, dryRun: options.dryRun, target: deployTargetLabel(target) });
128
+ console.log('Treeseed local deploy completed as a build-only publish target.');
129
+ return;
130
+ }
131
+ try {
132
+ assertDeploymentInitialized(tenantRoot, { target });
133
+ runTenantDeployPreflight({ cwd: tenantRoot, scope });
134
+ }
135
+ catch (error) {
136
+ writeDeployReport({
137
+ ok: false,
138
+ kind: 'preflight_failed',
139
+ target: deployTargetLabel(target),
140
+ message: error instanceof Error ? error.message : String(error),
141
+ });
142
+ throw error;
143
+ }
144
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
145
+ if (shouldRun('migrate')) {
146
+ const result = runRemoteD1Migrations(tenantRoot, { dryRun: options.dryRun, target });
147
+ console.log(`${options.dryRun ? 'Planned' : 'Applied'} remote migrations for ${result.databaseName}.`);
148
+ }
149
+ if (shouldRun('build')) {
150
+ if (options.dryRun) {
151
+ console.log('Dry run: skipped tenant build.');
152
+ }
153
+ else {
154
+ runNodeScript(packageScriptPath('tenant-build'));
155
+ }
156
+ }
157
+ if (shouldRun('publish')) {
158
+ if (options.dryRun) {
159
+ console.log(`Dry run: would deploy ${deployTargetLabel(target)} with generated Wrangler config at ${resolve(wranglerPath)}.`);
160
+ }
161
+ else {
162
+ runWranglerDeploy(wranglerPath);
163
+ finalizeDeploymentState(tenantRoot, { target });
164
+ }
165
+ }
166
+ writeDeployReport({
167
+ ok: true,
168
+ kind: 'success',
169
+ dryRun: options.dryRun,
170
+ only: options.only,
171
+ target: deployTargetLabel(target),
172
+ });
173
+ }
174
+ try {
175
+ await main();
176
+ }
177
+ catch (error) {
178
+ console.error(error instanceof Error ? error.message : String(error));
179
+ process.exit(1);
180
+ }
@@ -0,0 +1,104 @@
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 { applyTreeseedEnvironmentToProcess, assertTreeseedCommandEnvironment } from '../operations/services/config-runtime.js';
5
+ import { cleanupDestroyedState, createPersistentDeployTarget, destroyCloudflareResources, loadDeployState, printDestroySummary, validateDestroyPrerequisites, } from '../operations/services/deploy.js';
6
+ import { deriveCloudflareWorkerName } from '../platform/deploy/config.js';
7
+ const tenantRoot = process.cwd();
8
+ function parseArgs(argv) {
9
+ const parsed = {
10
+ dryRun: false,
11
+ force: false,
12
+ skipConfirmation: false,
13
+ confirm: null,
14
+ removeBuildArtifacts: false,
15
+ environment: null,
16
+ };
17
+ const rest = [...argv];
18
+ while (rest.length) {
19
+ const current = rest.shift();
20
+ if (!current)
21
+ continue;
22
+ if (current === '--dry-run') {
23
+ parsed.dryRun = true;
24
+ continue;
25
+ }
26
+ if (current === '--force') {
27
+ parsed.force = true;
28
+ continue;
29
+ }
30
+ if (current === '--skip-confirmation') {
31
+ parsed.skipConfirmation = true;
32
+ continue;
33
+ }
34
+ if (current === '--confirm') {
35
+ parsed.confirm = rest.shift() ?? null;
36
+ continue;
37
+ }
38
+ if (current === '--remove-build-artifacts') {
39
+ parsed.removeBuildArtifacts = true;
40
+ continue;
41
+ }
42
+ if (current === '--environment') {
43
+ parsed.environment = rest.shift() ?? null;
44
+ continue;
45
+ }
46
+ if (current.startsWith('--environment=')) {
47
+ parsed.environment = current.split('=', 2)[1] ?? null;
48
+ continue;
49
+ }
50
+ throw new Error(`Unknown destroy argument: ${current}`);
51
+ }
52
+ return parsed;
53
+ }
54
+ function getExpectedConfirmation(deployConfig) {
55
+ return deployConfig.slug;
56
+ }
57
+ function printDangerMessage(deployConfig, state, expectedConfirmation) {
58
+ console.error('DANGER: treeseed destroy will permanently delete this site and its Cloudflare resources.');
59
+ console.error(` Site: ${deployConfig.name}`);
60
+ console.error(` Slug: ${deployConfig.slug}`);
61
+ console.error(` Worker: ${state.workerName ?? deriveCloudflareWorkerName(deployConfig)}`);
62
+ console.error(` D1: ${state.d1Databases.SITE_DATA_DB.databaseName}`);
63
+ console.error(` KV FORM_GUARD_KV: ${state.kvNamespaces.FORM_GUARD_KV.name}`);
64
+ console.error(` KV SESSION: ${state.kvNamespaces.SESSION.name}`);
65
+ console.error(' This action is irreversible.');
66
+ console.error(` To continue, type exactly: ${expectedConfirmation}`);
67
+ }
68
+ async function readConfirmation(expectedConfirmation) {
69
+ const rl = readline.createInterface({ input, output });
70
+ try {
71
+ return (await rl.question('Confirmation: ')).trim() === expectedConfirmation;
72
+ }
73
+ finally {
74
+ rl.close();
75
+ }
76
+ }
77
+ const options = parseArgs(process.argv.slice(2));
78
+ const scope = options.environment ?? 'prod';
79
+ const target = createPersistentDeployTarget(scope);
80
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
81
+ assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose: 'destroy' });
82
+ const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote: !options.dryRun });
83
+ const state = loadDeployState(tenantRoot, deployConfig, { target });
84
+ const expectedConfirmation = getExpectedConfirmation(deployConfig);
85
+ if (!options.skipConfirmation) {
86
+ printDangerMessage(deployConfig, state, expectedConfirmation);
87
+ const confirmed = options.confirm !== null ? options.confirm === expectedConfirmation : await readConfirmation(expectedConfirmation);
88
+ if (!confirmed) {
89
+ console.error('Destroy aborted: confirmation text did not match.');
90
+ process.exit(1);
91
+ }
92
+ }
93
+ const result = destroyCloudflareResources(tenantRoot, {
94
+ dryRun: options.dryRun,
95
+ force: options.force,
96
+ target,
97
+ });
98
+ printDestroySummary(result);
99
+ if (options.dryRun) {
100
+ console.log('Dry run: no remote resources were deleted.');
101
+ process.exit(0);
102
+ }
103
+ cleanupDestroyedState(tenantRoot, { target, removeBuildArtifacts: options.removeBuildArtifacts });
104
+ console.log('Treeseed destroy completed and local deployment state was removed.');
@@ -0,0 +1,171 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { corePackageRoot, packageScriptPath, spawnNodeBinary, resolveWranglerBin } from '../operations/services/runtime-tools.js';
3
+ import { applyTreeseedEnvironmentToProcess, assertTreeseedCommandEnvironment } from '../operations/services/config-runtime.js';
4
+ import { ensureGeneratedWranglerConfig } from '../operations/services/deploy.js';
5
+ import { loadTreeseedDeployConfig } from '../platform/deploy/config.js';
6
+ import { createTenantWatchEntries, isEditablePackageWorkspace, startPollingWatch, stopManagedProcess, writeDevReloadStamp, workspaceSdkRoot, } from '../operations/services/watch-dev.js';
7
+ const tenantRoot = process.cwd();
8
+ const cliArgs = process.argv.slice(2);
9
+ const watchMode = cliArgs.includes('--watch');
10
+ const wranglerArgs = cliArgs.filter((arg) => arg !== '--watch');
11
+ let wranglerChild = null;
12
+ let stopWatching = null;
13
+ let isStoppingForRebuild = false;
14
+ let shuttingDown = false;
15
+ function shouldEnsureMailpit() {
16
+ if (process.env.TREESEED_DEV_FORCE_MAILPIT === '1') {
17
+ return true;
18
+ }
19
+ return loadTreeseedDeployConfig().smtp?.enabled === true;
20
+ }
21
+ function runStep(command, args, { cwd = tenantRoot, env = {}, fatal = true } = {}) {
22
+ const result = spawnSync(command, args, {
23
+ stdio: 'inherit',
24
+ cwd,
25
+ env: { ...process.env, ...env },
26
+ });
27
+ if (result.status !== 0 && fatal) {
28
+ process.exit(result.status ?? 1);
29
+ }
30
+ return result.status === 0;
31
+ }
32
+ function runNodeScript(scriptPath, args = [], options = {}) {
33
+ return runStep(process.execPath, [scriptPath, ...args], options);
34
+ }
35
+ function runTenantBuildCycle({ includePackageBuild = false, includeSdkBuild = false, fatal = true } = {}) {
36
+ const envOverrides = ['TREESEED_LOCAL_DEV_MODE=cloudflare'];
37
+ if (watchMode) {
38
+ envOverrides.push('TREESEED_PUBLIC_DEV_WATCH_RELOAD=true');
39
+ }
40
+ if (includeSdkBuild && isEditablePackageWorkspace()) {
41
+ const sdkRoot = workspaceSdkRoot();
42
+ if (sdkRoot) {
43
+ const sdkBuilt = runStep('npm', ['run', 'build:dist'], {
44
+ cwd: sdkRoot,
45
+ fatal,
46
+ });
47
+ if (!sdkBuilt) {
48
+ return false;
49
+ }
50
+ }
51
+ }
52
+ if (includePackageBuild && isEditablePackageWorkspace()) {
53
+ const distBuilt = runStep('npm', ['run', 'build:dist'], {
54
+ cwd: corePackageRoot,
55
+ fatal,
56
+ });
57
+ if (!distBuilt) {
58
+ return false;
59
+ }
60
+ }
61
+ const buildScripts = [
62
+ ['patch-starlight-content-path', []],
63
+ ['aggregate-book', []],
64
+ ['sync-dev-vars', envOverrides],
65
+ ['tenant-d1-migrate-local', []],
66
+ ];
67
+ if (shouldEnsureMailpit()) {
68
+ buildScripts.splice(2, 0, ['ensure-mailpit', []]);
69
+ }
70
+ for (const [scriptName, args] of buildScripts) {
71
+ const ok = runNodeScript(packageScriptPath(scriptName), args, { fatal });
72
+ if (!ok) {
73
+ return false;
74
+ }
75
+ }
76
+ ensureGeneratedWranglerConfig(tenantRoot);
77
+ if (watchMode) {
78
+ writeDevReloadStamp(tenantRoot);
79
+ }
80
+ const built = runNodeScript(packageScriptPath('tenant-build'), [], {
81
+ fatal,
82
+ env: watchMode ? { TREESEED_PUBLIC_DEV_WATCH_RELOAD: 'true' } : {},
83
+ });
84
+ if (!built) {
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ function startWrangler() {
90
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot);
91
+ const child = spawnNodeBinary(resolveWranglerBin(), ['dev', '--local', '--config', wranglerPath, ...wranglerArgs], {
92
+ cwd: tenantRoot,
93
+ env: watchMode ? { TREESEED_PUBLIC_DEV_WATCH_RELOAD: 'true' } : {},
94
+ detached: process.platform !== 'win32',
95
+ });
96
+ wranglerChild = child;
97
+ child.on('exit', (code, signal) => {
98
+ if (child !== wranglerChild) {
99
+ return;
100
+ }
101
+ wranglerChild = null;
102
+ if (isStoppingForRebuild || shuttingDown) {
103
+ return;
104
+ }
105
+ if (stopWatching) {
106
+ stopWatching();
107
+ }
108
+ if (signal) {
109
+ process.kill(process.pid, signal);
110
+ return;
111
+ }
112
+ process.exit(code ?? 0);
113
+ });
114
+ }
115
+ async function restartWranglerAfterBuild() {
116
+ if (wranglerChild) {
117
+ isStoppingForRebuild = true;
118
+ await stopManagedProcess(wranglerChild);
119
+ isStoppingForRebuild = false;
120
+ }
121
+ if (!shuttingDown) {
122
+ startWrangler();
123
+ }
124
+ }
125
+ async function shutdownAndExit(code = 0) {
126
+ shuttingDown = true;
127
+ if (stopWatching) {
128
+ stopWatching();
129
+ }
130
+ await stopManagedProcess(wranglerChild);
131
+ process.exit(code);
132
+ }
133
+ process.on('SIGINT', () => {
134
+ void shutdownAndExit(130);
135
+ });
136
+ process.on('SIGTERM', () => {
137
+ void shutdownAndExit(143);
138
+ });
139
+ process.env.TREESEED_LOCAL_DEV_MODE = process.env.TREESEED_LOCAL_DEV_MODE ?? 'cloudflare';
140
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope: 'local', override: true });
141
+ assertTreeseedCommandEnvironment({ tenantRoot, scope: 'local', purpose: 'dev' });
142
+ runTenantBuildCycle({
143
+ includeSdkBuild: isEditablePackageWorkspace(),
144
+ includePackageBuild: isEditablePackageWorkspace(),
145
+ fatal: true,
146
+ });
147
+ startWrangler();
148
+ if (watchMode) {
149
+ console.log('Starting unified Wrangler watch mode. Changes will rebuild the app and refresh the browser.');
150
+ stopWatching = startPollingWatch({
151
+ watchEntries: createTenantWatchEntries(tenantRoot),
152
+ onChange: async ({ changedPaths, packageChanged, sdkChanged }) => {
153
+ console.log(`Detected ${changedPaths.length} change${changedPaths.length === 1 ? '' : 's'}; rebuilding ${sdkChanged ? 'sdk, core, and tenant' : packageChanged ? 'core and tenant' : 'tenant'} output...`);
154
+ isStoppingForRebuild = true;
155
+ await stopManagedProcess(wranglerChild);
156
+ isStoppingForRebuild = false;
157
+ const ok = runTenantBuildCycle({
158
+ includeSdkBuild: sdkChanged,
159
+ includePackageBuild: packageChanged || sdkChanged,
160
+ fatal: false,
161
+ });
162
+ if (ok) {
163
+ startWrangler();
164
+ console.log('Rebuild complete. Wrangler restarted with the updated output.');
165
+ }
166
+ else {
167
+ console.error('Rebuild failed. Wrangler remains stopped until the next successful save.');
168
+ }
169
+ },
170
+ });
171
+ }
@@ -0,0 +1,4 @@
1
+ import { packageScriptPath, runNodeScript } from '../operations/services/runtime-tools.js';
2
+ runNodeScript(packageScriptPath('cleanup-markdown'), ['--check'], {
3
+ cwd: process.cwd(),
4
+ });
@@ -0,0 +1,4 @@
1
+ import { packageScriptPath, runNodeScript } from '../operations/services/runtime-tools.js';
2
+ runNodeScript(packageScriptPath('tenant-check'), [], {
3
+ cwd: process.cwd(),
4
+ });
@@ -0,0 +1,212 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { prepareCloudflareLocalRuntime, spawnProcess, startWranglerDev } from '../operations/services/local-dev.js';
6
+ import { fixtureRoot, fixtureWranglerConfig } from '../operations/services/runtime-paths.js';
7
+ const TEST_PORT = 8791;
8
+ const BASE_URL = `http://127.0.0.1:${TEST_PORT}`;
9
+ const RUN_ID = `${Date.now()}-${process.pid}`;
10
+ const ipSeed = Date.now() + process.pid;
11
+ const TEST_IP = `198.51.${(ipSeed >>> 8) % 250}.${ipSeed % 250}`;
12
+ const TEST_EMAIL = `integration-subscriber-${RUN_ID}@localhost.test`;
13
+ const PERSIST_TO = resolve(process.cwd(), '.local', 'cloudflare-integration', RUN_ID);
14
+ const workerLogs = [];
15
+ mkdirSync(PERSIST_TO, { recursive: true });
16
+ function captureLogs(stream, label) {
17
+ if (!stream) {
18
+ return;
19
+ }
20
+ stream.setEncoding('utf8');
21
+ stream.on('data', (chunk) => {
22
+ workerLogs.push(`[${label}] ${chunk}`);
23
+ if (workerLogs.length > 40) {
24
+ workerLogs.shift();
25
+ }
26
+ });
27
+ }
28
+ function readBody(message) {
29
+ return new Promise((resolve) => {
30
+ let body = '';
31
+ message.setEncoding('utf8');
32
+ message.on('data', (chunk) => {
33
+ body += chunk;
34
+ });
35
+ message.on('end', () => resolve(body));
36
+ });
37
+ }
38
+ function extractSetCookie(response) {
39
+ if (typeof response.headers.getSetCookie === 'function') {
40
+ return response.headers.getSetCookie();
41
+ }
42
+ const cookie = response.headers.get('set-cookie');
43
+ return cookie ? [cookie] : [];
44
+ }
45
+ async function waitForWorker(worker) {
46
+ for (let attempt = 0; attempt < 60; attempt += 1) {
47
+ if (worker.exitCode !== null) {
48
+ throw new Error(`Wrangler exited before the worker became ready (exit ${worker.exitCode}).\n${workerLogs.join('')}`);
49
+ }
50
+ try {
51
+ const response = await fetch(`${BASE_URL}/api/form/submit?formType=subscribe`, {
52
+ signal: AbortSignal.timeout(5000),
53
+ headers: {
54
+ 'x-forwarded-for': TEST_IP,
55
+ },
56
+ });
57
+ if (response.ok) {
58
+ return;
59
+ }
60
+ }
61
+ catch {
62
+ // Keep polling until the local worker is ready.
63
+ }
64
+ await delay(1000);
65
+ }
66
+ throw new Error(`Timed out waiting for local Wrangler dev to start.\n${workerLogs.join('')}`);
67
+ }
68
+ async function issueToken() {
69
+ const response = await fetch(`${BASE_URL}/api/form/submit?formType=subscribe`, {
70
+ signal: AbortSignal.timeout(10000),
71
+ headers: {
72
+ 'x-forwarded-for': TEST_IP,
73
+ },
74
+ });
75
+ assert.equal(response.status, 200, 'token endpoint should return HTTP 200');
76
+ const payload = await response.json();
77
+ const cookies = extractSetCookie(response);
78
+ assert.equal(payload.ok, true, 'token payload should be ok');
79
+ assert.ok(payload.formToken, 'token endpoint should return a form token');
80
+ assert.ok(payload.sessionId, 'token endpoint should return a session id');
81
+ assert.ok(cookies.length > 0, 'token endpoint should set a session cookie');
82
+ return {
83
+ formToken: payload.formToken,
84
+ sessionId: payload.sessionId,
85
+ cookieHeader: cookies.map((cookie) => cookie.split(';', 1)[0]).join('; '),
86
+ };
87
+ }
88
+ async function submitSubscribeForm({ formToken, sessionId, cookieHeader }, email = TEST_EMAIL) {
89
+ const form = new FormData();
90
+ form.set('formType', 'subscribe');
91
+ form.set('name', 'Integration Test');
92
+ form.set('email', email);
93
+ form.set('formToken', formToken);
94
+ form.set('formSession', sessionId);
95
+ form.set('redirectTo', '/');
96
+ return fetch(`${BASE_URL}/api/form/submit`, {
97
+ method: 'POST',
98
+ redirect: 'manual',
99
+ signal: AbortSignal.timeout(10000),
100
+ headers: {
101
+ cookie: cookieHeader,
102
+ origin: BASE_URL,
103
+ referer: `${BASE_URL}/`,
104
+ 'x-forwarded-for': TEST_IP,
105
+ },
106
+ body: form,
107
+ });
108
+ }
109
+ async function querySubscribers() {
110
+ const query = `SELECT lookup_key AS email, status, json_extract(payload_json, '$.source') AS source FROM runtime_records WHERE record_type = 'subscription' AND lookup_key = '${TEST_EMAIL}'`;
111
+ const child = spawnProcess('wrangler', [
112
+ 'd1',
113
+ 'execute',
114
+ 'karyon-docs-site-data',
115
+ '--local',
116
+ '--config',
117
+ fixtureWranglerConfig,
118
+ '--persist-to',
119
+ PERSIST_TO,
120
+ '--json',
121
+ '--command',
122
+ query,
123
+ ], { stdio: ['ignore', 'pipe', 'inherit'], cwd: fixtureRoot });
124
+ const stdout = await readBody(child.stdout);
125
+ const exitCode = await new Promise((resolve) => {
126
+ child.on('exit', resolve);
127
+ });
128
+ assert.equal(exitCode, 0, 'local D1 query should succeed');
129
+ const parsed = JSON.parse(stdout);
130
+ const results = Array.isArray(parsed) ? parsed : [parsed];
131
+ const rows = results.flatMap((entry) => entry.results ?? []);
132
+ return rows;
133
+ }
134
+ async function main() {
135
+ prepareCloudflareLocalRuntime({
136
+ persistTo: PERSIST_TO,
137
+ envOverrides: {
138
+ TREESEED_LOCAL_DEV_MODE: 'cloudflare',
139
+ TREESEED_FORMS_LOCAL_BYPASS_TURNSTILE: 'true',
140
+ TREESEED_PUBLIC_FORMS_LOCAL_BYPASS_TURNSTILE: 'true',
141
+ TREESEED_FORMS_LOCAL_BYPASS_CLOUDFLARE_GUARDS: 'false',
142
+ TREESEED_FORMS_LOCAL_USE_MAILPIT: 'true',
143
+ TREESEED_MAILPIT_SMTP_HOST: '127.0.0.1',
144
+ TREESEED_MAILPIT_SMTP_PORT: '1025',
145
+ },
146
+ });
147
+ const worker = startWranglerDev(['--port', String(TEST_PORT), '--persist-to', PERSIST_TO], {
148
+ stdio: ['ignore', 'pipe', 'pipe'],
149
+ });
150
+ captureLogs(worker.stdout, 'stdout');
151
+ captureLogs(worker.stderr, 'stderr');
152
+ const teardown = async () => {
153
+ if (worker.exitCode === null && !worker.killed) {
154
+ worker.kill('SIGTERM');
155
+ await Promise.race([
156
+ new Promise((resolve) => worker.once('exit', resolve)),
157
+ delay(5000),
158
+ ]);
159
+ if (worker.exitCode === null && !worker.killed) {
160
+ worker.kill('SIGKILL');
161
+ await Promise.race([
162
+ new Promise((resolve) => worker.once('exit', resolve)),
163
+ delay(5000),
164
+ ]);
165
+ }
166
+ }
167
+ };
168
+ process.on('SIGINT', async () => {
169
+ await teardown();
170
+ process.exit(130);
171
+ });
172
+ process.on('SIGTERM', async () => {
173
+ await teardown();
174
+ process.exit(143);
175
+ });
176
+ let shouldQuerySubscribers = false;
177
+ try {
178
+ await waitForWorker(worker);
179
+ const firstToken = await issueToken();
180
+ const firstResponse = await submitSubscribeForm(firstToken);
181
+ assert.equal(firstResponse.status, 303, 'subscribe submit should redirect');
182
+ assert.match(firstResponse.headers.get('location') ?? '', /\/\?formStatus=success&formCode=success#site-subscribe$/, 'subscribe success redirect should include success markers');
183
+ shouldQuerySubscribers = true;
184
+ const replayResponse = await submitSubscribeForm(firstToken);
185
+ assert.equal(replayResponse.status, 303, 'replayed submit should still redirect');
186
+ assert.match(replayResponse.headers.get('location') ?? '', /\/\?formStatus=error&formCode=token_replayed#site-subscribe$/, 'replayed token should be rejected by KV-backed nonce storage');
187
+ for (let attempt = 0; attempt < 2; attempt += 1) {
188
+ const token = await issueToken();
189
+ const response = await submitSubscribeForm(token);
190
+ assert.equal(response.status, 303, 'pre-limit submissions should redirect');
191
+ assert.match(response.headers.get('location') ?? '', /\/\?formStatus=success&formCode=success#site-subscribe$/, 'pre-limit submissions should succeed');
192
+ }
193
+ const limitedToken = await issueToken();
194
+ const limitedResponse = await submitSubscribeForm(limitedToken);
195
+ assert.equal(limitedResponse.status, 303, 'rate-limited submit should redirect');
196
+ assert.match(limitedResponse.headers.get('location') ?? '', /\/\?formStatus=error&formCode=rate_limited#site-subscribe$/, 'local KV-backed rate limiting should reject the fourth submission');
197
+ }
198
+ finally {
199
+ await teardown();
200
+ }
201
+ if (shouldQuerySubscribers) {
202
+ const rows = await querySubscribers();
203
+ assert.equal(rows.length, 1, 'subscriber should be written to local D1');
204
+ assert.equal(rows[0].email, TEST_EMAIL, 'subscriber email should match the submitted address');
205
+ assert.equal(rows[0].status, 'active', 'subscriber should be active');
206
+ assert.equal(rows[0].source, 'footer', 'subscriber source should be tracked');
207
+ }
208
+ }
209
+ main().catch((error) => {
210
+ console.error(error);
211
+ process.exit(1);
212
+ });