anhedral 0.8.6 → 0.10.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 (123) hide show
  1. package/README.md +137 -38
  2. package/assets/anhedral-cli-init-technical.svg +15 -15
  3. package/assets/anhedral-cli-init.svg +16 -16
  4. package/dist/access.d.ts +10 -0
  5. package/dist/access.js +93 -0
  6. package/dist/auth-schema-snapshot.d.ts +346 -0
  7. package/dist/auth-schema-snapshot.js +347 -0
  8. package/dist/auth-schema.d.ts +1 -0
  9. package/dist/auth-schema.js +57 -0
  10. package/dist/bin.js +349 -39
  11. package/dist/bounded-file.d.ts +2 -0
  12. package/dist/bounded-file.js +30 -0
  13. package/dist/capabilities.d.ts +24 -42
  14. package/dist/capabilities.js +73 -75
  15. package/dist/cloudflare.d.ts +14 -0
  16. package/dist/cloudflare.js +66 -0
  17. package/dist/command.js +12 -3
  18. package/dist/delivery.d.ts +118 -0
  19. package/dist/delivery.js +611 -0
  20. package/dist/dependencies.d.ts +24 -3
  21. package/dist/dependencies.js +112 -74
  22. package/dist/dependency-order.d.ts +2 -0
  23. package/dist/dependency-order.js +24 -0
  24. package/dist/index.d.ts +16 -3
  25. package/dist/index.js +11 -2
  26. package/dist/managed-hosting.d.ts +49 -0
  27. package/dist/managed-hosting.js +373 -0
  28. package/dist/management.d.ts +3 -0
  29. package/dist/management.js +47 -0
  30. package/dist/platforms/desktop.d.ts +2 -2
  31. package/dist/platforms/desktop.js +25 -15
  32. package/dist/platforms/extension.d.ts +2 -2
  33. package/dist/platforms/extension.js +104 -66
  34. package/dist/platforms/mobile.d.ts +2 -2
  35. package/dist/platforms/mobile.js +13 -7
  36. package/dist/pnpm-policy.d.ts +0 -2
  37. package/dist/pnpm-policy.js +18 -13
  38. package/dist/project-state.d.ts +28 -0
  39. package/dist/project-state.js +226 -0
  40. package/dist/provider-readiness.d.ts +30 -0
  41. package/dist/provider-readiness.js +87 -0
  42. package/dist/provision.d.ts +46 -0
  43. package/dist/provision.js +197 -0
  44. package/dist/readiness.d.ts +4 -2
  45. package/dist/readiness.js +64 -14
  46. package/dist/registry/add.d.ts +18 -0
  47. package/dist/registry/add.js +163 -0
  48. package/dist/registry/bootstrap.d.ts +2 -0
  49. package/dist/registry/bootstrap.js +14 -0
  50. package/dist/registry/commands.d.ts +2 -0
  51. package/dist/registry/commands.js +48 -0
  52. package/dist/registry/context.d.ts +3 -0
  53. package/dist/registry/context.js +21 -0
  54. package/dist/registry/options.d.ts +2 -0
  55. package/dist/registry/options.js +65 -0
  56. package/dist/registry/receipt.d.ts +2 -0
  57. package/dist/registry/receipt.js +52 -0
  58. package/dist/registry/resolve.d.ts +10 -0
  59. package/dist/registry/resolve.js +98 -0
  60. package/dist/registry/types.d.ts +64 -0
  61. package/dist/registry/usage.d.ts +1 -0
  62. package/dist/registry/usage.js +37 -0
  63. package/dist/render.d.ts +0 -4
  64. package/dist/render.js +3 -16
  65. package/dist/repository.d.ts +24 -0
  66. package/dist/repository.js +96 -0
  67. package/dist/scaffold/api.d.ts +2 -0
  68. package/dist/scaffold/api.js +13 -0
  69. package/dist/scaffold/auth.d.ts +3 -0
  70. package/dist/scaffold/auth.js +95 -0
  71. package/dist/scaffold/bootstrap.d.ts +3 -0
  72. package/dist/scaffold/bootstrap.js +39 -0
  73. package/dist/scaffold/compose.d.ts +4 -0
  74. package/dist/scaffold/compose.js +34 -0
  75. package/dist/scaffold/database.d.ts +2 -0
  76. package/dist/scaffold/database.js +43 -0
  77. package/dist/scaffold/documentation.d.ts +3 -0
  78. package/dist/scaffold/documentation.js +91 -0
  79. package/dist/scaffold/environment.d.ts +2 -0
  80. package/dist/scaffold/environment.js +5 -0
  81. package/dist/scaffold/files.d.ts +25 -0
  82. package/dist/scaffold/files.js +108 -0
  83. package/dist/scaffold/integrations.d.ts +3 -0
  84. package/dist/scaffold/integrations.js +7 -0
  85. package/dist/scaffold/services.d.ts +5 -0
  86. package/dist/scaffold/services.js +84 -0
  87. package/dist/scaffold/web.d.ts +2 -0
  88. package/dist/scaffold/web.js +102 -0
  89. package/dist/scaffold/workspace.d.ts +3 -0
  90. package/dist/scaffold/workspace.js +150 -0
  91. package/dist/security-backports.js +7 -2
  92. package/dist/stack-registry.d.ts +80 -0
  93. package/dist/stack-registry.js +424 -0
  94. package/dist/standard.d.ts +6 -19
  95. package/dist/standard.js +45 -658
  96. package/dist/transaction.js +47 -56
  97. package/dist/util.js +12 -2
  98. package/dist/vercel.d.ts +98 -0
  99. package/dist/vercel.js +796 -0
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/vinext.d.ts +1 -0
  103. package/dist/vinext.js +83 -0
  104. package/dist/workspace-graph.d.ts +7 -0
  105. package/dist/workspace-graph.js +73 -0
  106. package/docs/application-stack-standard.md +76 -91
  107. package/docs/delivery-workflow.md +83 -0
  108. package/docs/managed-hosting-architecture.md +87 -0
  109. package/docs/sponsorships.md +25 -0
  110. package/package.json +23 -29
  111. package/dist/evidence-validation.d.ts +0 -2
  112. package/dist/evidence-validation.js +0 -18
  113. package/dist/foundation.d.ts +0 -6
  114. package/dist/foundation.js +0 -49
  115. package/dist/progress-view.d.ts +0 -57
  116. package/dist/progress-view.js +0 -69
  117. package/dist/progress.d.ts +0 -95
  118. package/dist/progress.js +0 -327
  119. package/dist/project.d.ts +0 -5
  120. package/dist/standard-products.d.ts +0 -2
  121. package/dist/standard-products.js +0 -6
  122. /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
  123. /package/dist/{project.js → registry/types.js} +0 -0
@@ -3,24 +3,28 @@ import { writeReactLintConfig } from './lint.js';
3
3
  import { writeFile } from '../util.js';
4
4
  import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
5
5
  import { childPackageName, htmlText, jsString, markdownHeading } from '../render.js';
6
- export async function scaffoldExtension(root, options) {
7
- const { projectName, displayName, extensionSurface = 'sidepanel' } = options;
6
+ export async function scaffoldWxt({ root, options }) {
7
+ const projectName = options.name;
8
+ const displayName = options.name;
8
9
  const dir = path.join(root, 'apps/extension');
9
10
  writePackageJson(dir, projectName);
10
- writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({ extends: './.wxt/tsconfig.json', compilerOptions: { strict: true, jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } } }, null, 2) + '\n');
11
+ writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
12
+ extends: './.wxt/tsconfig.json',
13
+ compilerOptions: { strict: true, jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } },
14
+ }, null, 2) + '\n');
11
15
  writeReactLintConfig(dir);
12
- writeWxtConfig(dir, displayName, extensionSurface);
16
+ writeWxtConfig(dir, displayName);
13
17
  writeEnvExample(dir);
14
18
  writePostcssConfig(dir);
19
+ writeTailwindConfig(dir);
15
20
  writeShadcnConfig(dir);
16
- writeReadme(dir, displayName, extensionSurface);
21
+ writeReadme(dir, displayName);
17
22
  writeCnUtil(dir);
18
23
  writeButtonComponent(dir);
19
- if (extensionSurface === 'sidepanel')
20
- writeBackground(dir);
21
- writeSurfaceEntry(dir, extensionSurface);
22
- writeSurfaceHtml(dir, displayName, extensionSurface);
23
- writeSurfaceApp(dir, extensionSurface);
24
+ writeBackground(dir);
25
+ writeSidepanelEntry(dir);
26
+ writeSidepanelHtml(dir, displayName);
27
+ writeSidepanelApp(dir);
24
28
  writeStyles(dir);
25
29
  }
26
30
  function writePackageJson(dir, projectName) {
@@ -38,10 +42,14 @@ function writePackageJson(dir, projectName) {
38
42
  lint: 'eslint src --max-warnings 0',
39
43
  },
40
44
  dependencies: EXTENSION_DEPENDENCIES.dependencies,
41
- devDependencies: { ...EXTENSION_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '9.39.5' },
45
+ devDependencies: {
46
+ ...EXTENSION_DEPENDENCIES.devDependencies,
47
+ '@workspace/eslint-config': 'workspace:*',
48
+ eslint: '10.0.3',
49
+ },
42
50
  }, null, 2) + '\n');
43
51
  }
44
- function writeWxtConfig(dir, displayName, surface) {
52
+ function writeWxtConfig(dir, displayName) {
45
53
  const nameLiteral = jsString(displayName);
46
54
  const descriptionLiteral = jsString(`${displayName} Chrome Extension`);
47
55
  const actionTitleLiteral = jsString(`Open ${displayName}`);
@@ -59,14 +67,15 @@ export default defineConfig({
59
67
  description: ${descriptionLiteral},
60
68
  version: '0.1.0',
61
69
  ...(crxPublicKey ? { key: crxPublicKey } : {}),
62
- ${surface === 'sidepanel' ? "minimum_chrome_version: '114'," : ''}
63
- permissions: ${surface === 'sidepanel' ? "['activeTab', 'scripting', 'sidePanel']" : "['activeTab', 'scripting']"},
70
+ minimum_chrome_version: '114',
71
+ permissions: ['activeTab', 'scripting', 'sidePanel'],
64
72
  host_permissions: [],
65
73
  action: {
66
74
  default_title: ${actionTitleLiteral},
67
- ${surface === 'popup' ? "default_popup: 'popup.html'," : ''}
68
75
  },
69
- ${surface === 'sidepanel' ? "side_panel: { default_path: 'sidepanel.html' }," : ''}
76
+ side_panel: {
77
+ default_path: 'sidepanel.html',
78
+ },
70
79
  };
71
80
  },
72
81
  modules: ['@wxt-dev/module-react'],
@@ -90,7 +99,8 @@ function writeEnvExample(dir) {
90
99
  function writePostcssConfig(dir) {
91
100
  writeFile(path.join(dir, 'postcss.config.cjs'), `module.exports = {
92
101
  plugins: {
93
- "@tailwindcss/postcss": {},
102
+ tailwindcss: {},
103
+ autoprefixer: {},
94
104
  },
95
105
  };
96
106
  `);
@@ -162,10 +172,62 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
162
172
  Button.displayName = 'Button';
163
173
  `);
164
174
  }
165
- function writeReadme(dir, displayName, surface) {
175
+ function writeTailwindConfig(dir) {
176
+ writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
177
+ module.exports = {
178
+ content: ['./src/**/*.{ts,tsx,html}'],
179
+ theme: {
180
+ extend: {
181
+ colors: {
182
+ border: 'hsl(var(--border))',
183
+ input: 'hsl(var(--input))',
184
+ ring: 'hsl(var(--ring))',
185
+ background: 'hsl(var(--background))',
186
+ foreground: 'hsl(var(--foreground))',
187
+ primary: {
188
+ DEFAULT: 'hsl(var(--primary))',
189
+ foreground: 'hsl(var(--primary-foreground))',
190
+ },
191
+ secondary: {
192
+ DEFAULT: 'hsl(var(--secondary))',
193
+ foreground: 'hsl(var(--secondary-foreground))',
194
+ },
195
+ destructive: {
196
+ DEFAULT: 'hsl(var(--destructive))',
197
+ foreground: 'hsl(var(--destructive-foreground))',
198
+ },
199
+ muted: {
200
+ DEFAULT: 'hsl(var(--muted))',
201
+ foreground: 'hsl(var(--muted-foreground))',
202
+ },
203
+ accent: {
204
+ DEFAULT: 'hsl(var(--accent))',
205
+ foreground: 'hsl(var(--accent-foreground))',
206
+ },
207
+ popover: {
208
+ DEFAULT: 'hsl(var(--popover))',
209
+ foreground: 'hsl(var(--popover-foreground))',
210
+ },
211
+ card: {
212
+ DEFAULT: 'hsl(var(--card))',
213
+ foreground: 'hsl(var(--card-foreground))',
214
+ },
215
+ },
216
+ borderRadius: {
217
+ lg: 'var(--radius)',
218
+ md: 'calc(var(--radius) - 2px)',
219
+ sm: 'calc(var(--radius) - 4px)',
220
+ },
221
+ },
222
+ },
223
+ plugins: [],
224
+ };
225
+ `);
226
+ }
227
+ function writeReadme(dir, displayName) {
166
228
  writeFile(path.join(dir, 'README.md'), `# ${markdownHeading(displayName)} Chrome Extension
167
229
 
168
- WXT ${surface} extension generated by anhedral. Choose the surface for the task; content UI, options pages and background-only behavior require targeted implementation.
230
+ WXT side-panel extension generated by anhedral.
169
231
 
170
232
  ## Development
171
233
 
@@ -182,25 +244,29 @@ Run these commands from \`apps/extension\`. Anhedral generates only \`.env.examp
182
244
 
183
245
  Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
184
246
 
185
- ${surface === 'sidepanel' ? "The browser action opens the Side Panel API surface (Chrome 114+) with the sidePanel permission." : "The browser action opens popup.html; no sidePanel permission or panel background handler is generated."} Active-page reads use a user-triggered activeTab + scripting grant instead of persistent site access. Remove these permissions and the example read action if the product does not need page inspection.
247
+ The extension uses Chrome's Side Panel API. The browser action opens \`sidepanel.html\`, \`wxt.config.ts\` declares the \`sidePanel\` permission and Chrome 114+ minimum version, and active-page reads use a user-triggered \`activeTab\` + \`scripting\` grant instead of persistent site access.
186
248
  `);
187
249
  }
188
250
  function writeBackground(dir) {
189
- writeFile(path.join(dir, 'src/entrypoints/background.ts'), `import { browser } from 'wxt/browser';
251
+ writeFile(path.join(dir, 'src/entrypoints/background.ts'), `type ChromeWithSidePanel = typeof chrome & {
252
+ sidePanel: {
253
+ setPanelBehavior: (behavior: { openPanelOnActionClick: boolean }) => Promise<void>;
254
+ };
255
+ };
190
256
 
191
257
  export default defineBackground(() => {
192
258
 
193
259
  // Open the side panel when the extension icon is clicked.
194
- browser.sidePanel
260
+ (chrome as ChromeWithSidePanel).sidePanel
195
261
  .setPanelBehavior({ openPanelOnActionClick: true })
196
262
  .catch(() => {});
197
263
  });
198
264
  `);
199
265
  }
200
- function writeSurfaceEntry(dir, surface) {
201
- writeFile(path.join(dir, `src/entrypoints/${surface}/main.tsx`), `import * as React from 'react';
266
+ function writeSidepanelEntry(dir) {
267
+ writeFile(path.join(dir, 'src/entrypoints/sidepanel/main.tsx'), `import * as React from 'react';
202
268
  import { createRoot } from 'react-dom/client';
203
- import { ExtensionApp } from './app';
269
+ import { SidePanelApp } from './app';
204
270
  import '../../styles/main.css';
205
271
 
206
272
  const container = document.getElementById('root');
@@ -208,14 +274,14 @@ if (container) {
208
274
  const root = createRoot(container);
209
275
  root.render(
210
276
  <React.StrictMode>
211
- <ExtensionApp />
277
+ <SidePanelApp />
212
278
  </React.StrictMode>
213
279
  );
214
280
  }
215
281
  `);
216
282
  }
217
- function writeSurfaceHtml(dir, displayName, surface) {
218
- writeFile(path.join(dir, `src/entrypoints/${surface}/index.html`), `<!DOCTYPE html>
283
+ function writeSidepanelHtml(dir, displayName) {
284
+ writeFile(path.join(dir, 'src/entrypoints/sidepanel/index.html'), `<!DOCTYPE html>
219
285
  <html lang="en">
220
286
  <head>
221
287
  <meta charset="UTF-8" />
@@ -229,9 +295,8 @@ function writeSurfaceHtml(dir, displayName, surface) {
229
295
  </html>
230
296
  `);
231
297
  }
232
- function writeSurfaceApp(dir, surface) {
233
- writeFile(path.join(dir, `src/entrypoints/${surface}/app.tsx`), `import * as React from 'react';
234
- import { browser } from 'wxt/browser';
298
+ function writeSidepanelApp(dir) {
299
+ writeFile(path.join(dir, 'src/entrypoints/sidepanel/app.tsx'), `import * as React from 'react';
235
300
  import { Button } from '../../components/ui/button';
236
301
 
237
302
  type PageSnapshot = {
@@ -240,21 +305,17 @@ type PageSnapshot = {
240
305
  };
241
306
 
242
307
  async function readPageSnapshot(): Promise<PageSnapshot> {
243
- const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
308
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
244
309
  if (!tab?.id) throw new Error('No active tab is available.');
245
- const [injection] = await browser.scripting.executeScript({
310
+ const [{ result }] = await chrome.scripting.executeScript({
246
311
  target: { tabId: tab.id },
247
312
  func: () => ({ title: document.title, location: window.location.href }),
248
313
  });
249
- return requireSnapshot(injection?.result);
250
- }
251
-
252
- function requireSnapshot(result: PageSnapshot | undefined): PageSnapshot {
253
314
  if (!result) throw new Error('The active page did not return a snapshot.');
254
- return result;
315
+ return result as PageSnapshot;
255
316
  }
256
317
 
257
- export function ExtensionApp() {
318
+ export function SidePanelApp() {
258
319
  const [page, setPage] = React.useState<PageSnapshot | null>(null);
259
320
  const [pageError, setPageError] = React.useState<string | null>(null);
260
321
 
@@ -269,7 +330,7 @@ export function ExtensionApp() {
269
330
 
270
331
 
271
332
  return (
272
- <div style={{ padding: 24${surface === 'popup' ? ', minWidth: 320' : ''} }}>
333
+ <div style={{ padding: 24 }}>
273
334
  <h2>Welcome!</h2>
274
335
  <Button type="button" onClick={() => void readActivePage()}>Read active page</Button>
275
336
  {page ? (
@@ -285,32 +346,9 @@ export function ExtensionApp() {
285
346
  `);
286
347
  }
287
348
  function writeStyles(dir) {
288
- writeFile(path.join(dir, 'src/styles/main.css'), `@import "tailwindcss";
289
-
290
- @theme inline {
291
- --color-background: hsl(var(--background));
292
- --color-foreground: hsl(var(--foreground));
293
- --color-card: hsl(var(--card));
294
- --color-card-foreground: hsl(var(--card-foreground));
295
- --color-popover: hsl(var(--popover));
296
- --color-popover-foreground: hsl(var(--popover-foreground));
297
- --color-primary: hsl(var(--primary));
298
- --color-primary-foreground: hsl(var(--primary-foreground));
299
- --color-secondary: hsl(var(--secondary));
300
- --color-secondary-foreground: hsl(var(--secondary-foreground));
301
- --color-muted: hsl(var(--muted));
302
- --color-muted-foreground: hsl(var(--muted-foreground));
303
- --color-accent: hsl(var(--accent));
304
- --color-accent-foreground: hsl(var(--accent-foreground));
305
- --color-destructive: hsl(var(--destructive));
306
- --color-destructive-foreground: hsl(var(--destructive-foreground));
307
- --color-border: hsl(var(--border));
308
- --color-input: hsl(var(--input));
309
- --color-ring: hsl(var(--ring));
310
- --radius-lg: var(--radius);
311
- --radius-md: calc(var(--radius) - 2px);
312
- --radius-sm: calc(var(--radius) - 4px);
313
- }
349
+ writeFile(path.join(dir, 'src/styles/main.css'), `@tailwind base;
350
+ @tailwind components;
351
+ @tailwind utilities;
314
352
 
315
353
  @layer base {
316
354
  :root {
@@ -1,2 +1,2 @@
1
- import type { ProjectOptions } from '../project.js';
2
- export declare function scaffoldMobile(root: string, options: ProjectOptions): Promise<void>;
1
+ import type { RecipeContext } from '../registry/types.js';
2
+ export declare function scaffoldExpo({ root, options }: RecipeContext): Promise<void>;
@@ -118,9 +118,7 @@ module.exports = {
118
118
  }
119
119
  function expoScheme(projectName) {
120
120
  const unscoped = projectName.replace(/^@[^/]+\//, '');
121
- const safe = unscoped
122
- .toLowerCase()
123
- .replace(/[^a-z0-9+.-]+/g, '-');
121
+ const safe = unscoped.toLowerCase().replace(/[^a-z0-9+.-]+/g, '-');
124
122
  let start = 0;
125
123
  let end = safe.length;
126
124
  while (start < end && '+.-'.includes(safe[start]))
@@ -131,9 +129,10 @@ function expoScheme(projectName) {
131
129
  const candidate = normalized || 'app';
132
130
  return /^[a-z]/.test(candidate) ? candidate : `app-${candidate}`;
133
131
  }
134
- export async function scaffoldMobile(root, options) {
132
+ export async function scaffoldExpo({ root, options }) {
135
133
  const dir = path.join(root, 'apps/mobile');
136
- const { projectName, displayName } = options;
134
+ const projectName = options.name;
135
+ const displayName = options.name;
137
136
  const nameLiteral = jsString(displayName);
138
137
  writeReactLintConfig(dir);
139
138
  writeFile(path.join(dir, 'package.json'), JSON.stringify({
@@ -158,8 +157,15 @@ export async function scaffoldMobile(root, options) {
158
157
  typecheck: 'tsc --noEmit',
159
158
  lint: 'eslint app components lib --max-warnings 0',
160
159
  },
161
- dependencies: { ...MOBILE_APP_DEPENDENCIES.dependencies, ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies },
162
- devDependencies: { ...MOBILE_APP_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '9.39.5' },
160
+ dependencies: {
161
+ ...MOBILE_APP_DEPENDENCIES.dependencies,
162
+ ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies,
163
+ },
164
+ devDependencies: {
165
+ ...MOBILE_APP_DEPENDENCIES.devDependencies,
166
+ '@workspace/eslint-config': 'workspace:*',
167
+ eslint: '10.0.3',
168
+ },
163
169
  }, null, 2) + '\n');
164
170
  writeFile(path.join(dir, 'app.json'), JSON.stringify({
165
171
  expo: {
@@ -1,6 +1,4 @@
1
1
  /** Preserve unrelated starter YAML while replacing the settings this initializer owns. */
2
2
  export declare function setPnpmPolicy(root: string, policy: Record<string, unknown>): void;
3
- /** Only reads the JSON-compatible values written above, never arbitrary client YAML. */
4
- export declare function generatedPnpmPolicy(root: string, key: string): Record<string, string>;
5
3
  /** Merge reviewed patches without deleting starter-maintained mappings. */
6
4
  export declare function mergePnpmPatches(root: string, patches: Record<string, string>): void;
@@ -17,12 +17,6 @@ export function setPnpmPolicy(root, policy) {
17
17
  }
18
18
  writeFile(file, source);
19
19
  }
20
- /** Only reads the JSON-compatible values written above, never arbitrary client YAML. */
21
- export function generatedPnpmPolicy(root, key) {
22
- const source = readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8');
23
- const value = source.match(new RegExp(`^${key}: (.+)$`, 'm'))?.[1];
24
- return value ? JSON.parse(value) : {};
25
- }
26
20
  /** Merge reviewed patches without deleting starter-maintained mappings. */
27
21
  export function mergePnpmPatches(root, patches) {
28
22
  const file = path.join(root, 'pnpm-workspace.yaml');
@@ -32,8 +26,11 @@ export function mergePnpmPatches(root, patches) {
32
26
  throw new Error('Duplicate patchedDependencies configuration');
33
27
  const block = blocks[0]?.[0] ?? '';
34
28
  const existing = patchRows(block, patches);
35
- const replacement = 'patchedDependencies:\n' + existing + Object.entries(patches)
36
- .map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)}\n`).join('');
29
+ const replacement = 'patchedDependencies:\n' +
30
+ existing +
31
+ Object.entries(patches)
32
+ .map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)}\n`)
33
+ .join('');
37
34
  const remaining = source.replace(policyBlock('patchedDependencies'), '');
38
35
  writeFile(file, remaining + `${remaining.endsWith('\n') || !remaining ? '' : '\n'}${replacement}`);
39
36
  }
@@ -44,18 +41,26 @@ function patchRows(block, patches) {
44
41
  const value = header.slice(header.indexOf(':') + 1).trim();
45
42
  if (value && !value.startsWith('#')) {
46
43
  const object = JSON.parse(value);
47
- if (!object || Array.isArray(object) || typeof object !== 'object' ||
44
+ if (!object ||
45
+ Array.isArray(object) ||
46
+ typeof object !== 'object' ||
48
47
  Object.values(object).some((entry) => typeof entry !== 'string'))
49
48
  throw new Error('Unsupported patchedDependencies mapping');
50
- return Object.entries(object).filter(([key]) => !Object.hasOwn(patches, key))
51
- .map(([key, entry]) => ` ${JSON.stringify(key)}: ${JSON.stringify(entry)}\n`).join('');
49
+ return Object.entries(object)
50
+ .filter(([key]) => !Object.hasOwn(patches, key))
51
+ .map(([key, entry]) => ` ${JSON.stringify(key)}: ${JSON.stringify(entry)}\n`)
52
+ .join('');
52
53
  }
53
- return rows.filter((row) => {
54
+ return rows
55
+ .filter((row) => {
54
56
  if (!row.trim() || row.trimStart().startsWith('#'))
55
57
  return true;
56
58
  const match = row.match(/^ (?:"([^"\\]+)"|'([^']+)'|([^\s:'"]+)):\s*(?:"[^"\\]+"|'[^']+'|[^\s{}[\]&*!|>]+)(?:\s+#.*)?\s*$/);
57
59
  if (!match)
58
60
  throw new Error('Unsupported patchedDependencies mapping');
59
61
  return !Object.hasOwn(patches, match[1] ?? match[2] ?? match[3]);
60
- }).filter((row, index, rows) => row || index < rows.length - 1).map((row) => `${row}\n`).join('');
62
+ })
63
+ .filter((row, index, rows) => row || index < rows.length - 1)
64
+ .map((row) => `${row}\n`)
65
+ .join('');
61
66
  }
@@ -0,0 +1,28 @@
1
+ export interface DeploymentTarget {
2
+ id: string;
3
+ directory: string;
4
+ workerName: string;
5
+ }
6
+ export interface ApplicationProject {
7
+ schemaVersion: 1;
8
+ id: string;
9
+ name: string;
10
+ ownership: 'customer';
11
+ provider: 'cloudflare';
12
+ accountId: string | null;
13
+ audience: 'unconfigured' | 'public' | 'private';
14
+ access?: {
15
+ emails: string[];
16
+ };
17
+ targets: DeploymentTarget[];
18
+ }
19
+ export type DeliveryRunner = (command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv) => Promise<string>;
20
+ export declare function atomicJson(file: string, value: unknown): void;
21
+ export declare function readConfig(directory: string): Record<string, unknown>;
22
+ export declare function targetDirectory(root: string, target: DeploymentTarget): string;
23
+ export declare function registerApplication(root: string, extendTargets?: boolean): ApplicationProject;
24
+ export declare function readApplication(root: string): ApplicationProject;
25
+ export declare const defaultRunner: DeliveryRunner;
26
+ export declare function environment(project: ApplicationProject): NodeJS.ProcessEnv;
27
+ export declare function authenticate(root: string, project: ApplicationProject, run: DeliveryRunner): Promise<void>;
28
+ export declare function withApplicationLock<T>(root: string, operation: () => Promise<T>): Promise<T>;
@@ -0,0 +1,226 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { parse } from 'jsonc-parser';
6
+ import { resolveCommand } from './command.js';
7
+ const manifestName = 'anhedral.project.json';
8
+ const stateDirectory = '.anhedral';
9
+ const accountPattern = /^[a-f0-9]{32}$/i;
10
+ const workerPattern = /^[a-z0-9][a-z0-9-]{0,62}$/;
11
+ export function atomicJson(file, value) {
12
+ const temporary = `${file}.${randomUUID()}.tmp`;
13
+ try {
14
+ writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
15
+ renameSync(temporary, file);
16
+ }
17
+ finally {
18
+ rmSync(temporary, { force: true });
19
+ }
20
+ }
21
+ export function readConfig(directory) {
22
+ const errors = [];
23
+ const value = parse(readFileSync(path.join(directory, 'wrangler.jsonc'), 'utf8'), errors);
24
+ if (errors.length || !value || typeof value !== 'object' || Array.isArray(value))
25
+ throw new Error('Invalid Wrangler JSONC configuration.');
26
+ return value;
27
+ }
28
+ export function targetDirectory(root, target) {
29
+ if (!/^apps\/[a-z0-9-]+$/.test(target.directory))
30
+ throw new Error('Deployment targets must be direct apps/* directories.');
31
+ const directory = realpathSync(path.join(root, target.directory));
32
+ if (!directory.startsWith(realpathSync(root) + path.sep))
33
+ throw new Error('Deployment target escapes the project.');
34
+ return directory;
35
+ }
36
+ export function registerApplication(root, extendTargets = false) {
37
+ root = realpathSync(root);
38
+ const existing = existsSync(path.join(root, manifestName)) ? readApplication(root) : undefined;
39
+ if (existing && !extendTargets)
40
+ return existing;
41
+ const setup = JSON.parse(readFileSync(path.join(root, 'anhedral.setup.json'), 'utf8'));
42
+ if (!['cloudflare', 'vercel'].includes(setup.hosting))
43
+ throw new Error('Automatic delivery currently supports Cloudflare projects.');
44
+ const targets = readdirSync(path.join(root, 'apps'), { withFileTypes: true })
45
+ .filter((entry) => entry.isDirectory())
46
+ .filter((entry) => existsSync(path.join(root, 'apps', entry.name, 'wrangler.jsonc')))
47
+ .map((entry) => {
48
+ const workerName = readConfig(path.join(root, 'apps', entry.name)).name;
49
+ if (typeof workerName !== 'string' || !workerPattern.test(workerName))
50
+ throw new Error('Invalid Worker name.');
51
+ return { id: entry.name, directory: `apps/${entry.name}`, workerName };
52
+ })
53
+ .sort((a, b) => (a.id === 'web' ? 1 : b.id === 'web' ? -1 : a.id.localeCompare(b.id)));
54
+ if (existing) {
55
+ if (existing.targets.some((target) => !targets.some((candidate) => candidate.id === target.id &&
56
+ candidate.directory === target.directory &&
57
+ candidate.workerName === target.workerName)))
58
+ throw new Error('Addition cannot replace existing delivery targets.');
59
+ const extended = { ...existing, targets };
60
+ atomicJson(path.join(root, manifestName), extended);
61
+ return extended;
62
+ }
63
+ const project = {
64
+ schemaVersion: 1,
65
+ id: randomUUID(),
66
+ name: JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? path.basename(root),
67
+ ownership: 'customer',
68
+ provider: 'cloudflare',
69
+ accountId: null,
70
+ audience: 'unconfigured',
71
+ targets,
72
+ };
73
+ const ignoreFile = path.join(root, '.gitignore');
74
+ const ignored = existsSync(ignoreFile) ? readFileSync(ignoreFile, 'utf8') : '';
75
+ if (!ignored.split(/\r?\n/).includes('.anhedral/'))
76
+ writeFileSync(ignoreFile, ignored + (ignored.endsWith('\n') || !ignored ? '' : '\n') + '.anhedral/\n');
77
+ atomicJson(path.join(root, manifestName), project);
78
+ return project;
79
+ }
80
+ export function readApplication(root) {
81
+ const project = JSON.parse(readFileSync(path.join(root, manifestName), 'utf8'));
82
+ if (project.schemaVersion !== 1 ||
83
+ typeof project.id !== 'string' ||
84
+ !/^[a-f0-9-]{36}$/i.test(project.id) ||
85
+ project.ownership !== 'customer' ||
86
+ project.provider !== 'cloudflare' ||
87
+ !(project.accountId === null || accountPattern.test(project.accountId)) ||
88
+ !['unconfigured', 'public', 'private'].includes(project.audience) ||
89
+ !Array.isArray(project.targets))
90
+ throw new Error('Invalid application manifest.');
91
+ if (project.access &&
92
+ (!Array.isArray(project.access.emails) ||
93
+ project.access.emails.some((email) => typeof email !== 'string')))
94
+ throw new Error('Invalid private audience configuration.');
95
+ if (project.audience === 'private' && !project.access?.emails.length)
96
+ throw new Error('Private delivery requires explicit allowed members.');
97
+ const ids = new Set();
98
+ const workers = new Set();
99
+ for (const target of project.targets) {
100
+ if (!target ||
101
+ typeof target.id !== 'string' ||
102
+ typeof target.workerName !== 'string' ||
103
+ !/^[a-z0-9-]+$/.test(target.id) ||
104
+ ids.has(target.id) ||
105
+ workers.has(target.workerName) ||
106
+ !workerPattern.test(target.workerName))
107
+ throw new Error('Invalid or duplicate deployment target.');
108
+ ids.add(target.id);
109
+ workers.add(target.workerName);
110
+ targetDirectory(root, target);
111
+ }
112
+ return project;
113
+ }
114
+ export const defaultRunner = (command, args, cwd, env) => new Promise((resolve, reject) => {
115
+ const invocation = resolveCommand(command, args);
116
+ const child = spawn(invocation.command, invocation.args, {
117
+ cwd,
118
+ env,
119
+ detached: process.platform !== 'win32',
120
+ stdio: ['ignore', 'pipe', 'pipe'],
121
+ });
122
+ let output = '';
123
+ let oversized = false;
124
+ let interrupted = false;
125
+ let timedOut = false;
126
+ let forceTimer;
127
+ const kill = (signal) => {
128
+ if (!child.pid)
129
+ return;
130
+ if (process.platform === 'win32')
131
+ spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
132
+ else {
133
+ try {
134
+ process.kill(-child.pid, signal);
135
+ }
136
+ catch {
137
+ /* The process group already exited. */
138
+ }
139
+ }
140
+ };
141
+ const stop = () => {
142
+ kill('SIGTERM');
143
+ forceTimer ??= setTimeout(() => kill('SIGKILL'), 2000);
144
+ forceTimer.unref();
145
+ };
146
+ const interrupt = () => {
147
+ interrupted = true;
148
+ stop();
149
+ };
150
+ process.once('SIGINT', interrupt);
151
+ process.once('SIGTERM', interrupt);
152
+ child.stdout.on('data', (chunk) => {
153
+ if (!oversized)
154
+ output += chunk.toString();
155
+ if (output.length > 8 * 1024 * 1024) {
156
+ oversized = true;
157
+ stop();
158
+ }
159
+ });
160
+ // Do not persist raw build/provider output: it can contain credentials or customer data.
161
+ child.stderr.resume();
162
+ const timer = setTimeout(() => {
163
+ timedOut = true;
164
+ stop();
165
+ }, 20 * 60 * 1000);
166
+ const clean = () => {
167
+ clearTimeout(timer);
168
+ if (forceTimer)
169
+ clearTimeout(forceTimer);
170
+ process.removeListener('SIGINT', interrupt);
171
+ process.removeListener('SIGTERM', interrupt);
172
+ };
173
+ child.on('error', () => {
174
+ clean();
175
+ reject(new Error(`Cannot run ${command}; check local tooling.`));
176
+ });
177
+ child.on('close', (code) => {
178
+ clean();
179
+ if (code === 0 && !oversized && !interrupted && !timedOut)
180
+ resolve(output);
181
+ else
182
+ reject(new Error(`${command} ${args.slice(0, 3).join(' ')} ${interrupted ? 'was interrupted' : timedOut ? 'timed out' : 'failed'}; inspect the project command locally.`));
183
+ });
184
+ });
185
+ export function environment(project) {
186
+ if (!project.accountId)
187
+ throw new Error('Connect the intended Cloudflare account first.');
188
+ if (process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_ACCOUNT_ID !== project.accountId)
189
+ throw new Error('Cloudflare environment account conflicts with the registered account.');
190
+ if (process.env.CLOUDFLARE_ENV)
191
+ throw new Error('Register a separate project configuration for the intended environment; CLOUDFLARE_ENV overrides are unsupported.');
192
+ return {
193
+ ...process.env,
194
+ CLOUDFLARE_ACCOUNT_ID: project.accountId,
195
+ CI: 'true',
196
+ NO_COLOR: '1',
197
+ FORCE_COLOR: '0',
198
+ };
199
+ }
200
+ export async function authenticate(root, project, run) {
201
+ const identity = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'whoami', '--json'], targetDirectory(root, project.targets[0]), environment(project)));
202
+ if (identity.loggedIn !== true ||
203
+ !Array.isArray(identity.accounts) ||
204
+ !identity.accounts.some((account) => account.id === project.accountId))
205
+ throw new Error('Wrangler cannot access the registered Cloudflare account. Run wrangler login or configure scoped credentials.');
206
+ }
207
+ export async function withApplicationLock(root, operation) {
208
+ root = realpathSync(root);
209
+ const state = path.join(root, stateDirectory);
210
+ mkdirSync(state, { recursive: true });
211
+ if (realpathSync(state) !== path.join(root, stateDirectory))
212
+ throw new Error('Delivery state directory must not be a symbolic link.');
213
+ const lock = path.join(state, 'ship.lock');
214
+ try {
215
+ mkdirSync(lock);
216
+ }
217
+ catch {
218
+ throw new Error('Another delivery is running or was interrupted. Inspect provider status and remove .anhedral/ship.lock only after confirming no delivery is active.');
219
+ }
220
+ try {
221
+ return await operation();
222
+ }
223
+ finally {
224
+ rmSync(lock, { recursive: true, force: true });
225
+ }
226
+ }