@alumbwe/anvil 1.0.41 → 1.0.49

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 (241) hide show
  1. package/bin/anvil.js +18 -0
  2. package/package.json +13 -6
  3. package/src/agents/bundled-agents.generated.ts +1 -1
  4. package/src/entry.ts +5 -0
  5. package/src/index.tsx +27 -4
  6. package/src/login/plain-login.ts +60 -0
  7. package/src/login/supabase-auth.ts +346 -0
  8. package/src/pre-init/client-env.ts +17 -0
  9. package/src/utils/anvil-client.ts +4 -0
  10. package/src/utils/auth.ts +16 -4
  11. package/src/utils/client-env-defaults.ts +28 -0
  12. package/src/utils/config-dir.ts +1 -1
  13. package/vendor/@anvil/common/src/constants/deepseek-direct.ts +30 -1
  14. package/vendor/@anvil/common/src/util/project-ignore.ts +4 -3
  15. package/vendor/@anvil/sdk/dist/index.cjs +46 -10
  16. package/vendor/@anvil/sdk/dist/index.mjs +46 -10
  17. package/src/commands/__tests__/anvil-command-aliases.test.ts +0 -56
  18. package/src/commands/__tests__/bash-command.test.ts +0 -445
  19. package/src/commands/__tests__/command-args.test.ts +0 -349
  20. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +0 -108
  21. package/src/commands/__tests__/copy-conversation.test.ts +0 -226
  22. package/src/commands/__tests__/diagnostics-command.test.ts +0 -35
  23. package/src/commands/__tests__/image.test.ts +0 -99
  24. package/src/commands/__tests__/init.test.ts +0 -456
  25. package/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -91
  26. package/src/commands/__tests__/process-diagnostics.test.ts +0 -54
  27. package/src/commands/__tests__/prompt-builders.test.ts +0 -28
  28. package/src/commands/__tests__/router-input.test.ts +0 -298
  29. package/src/components/__tests__/ad-banner.test.tsx +0 -83
  30. package/src/components/__tests__/anvil-landing-heading-row.test.tsx +0 -91
  31. package/src/components/__tests__/anvil-model-selector.test.tsx +0 -519
  32. package/src/components/__tests__/anvil-offer-invariants.test.ts +0 -54
  33. package/src/components/__tests__/anvil-takeover-prompt.test.tsx +0 -212
  34. package/src/components/__tests__/chat-input-bar.test.tsx +0 -130
  35. package/src/components/__tests__/grid-layout.integration.test.tsx +0 -304
  36. package/src/components/__tests__/grid-layout.test.tsx +0 -1051
  37. package/src/components/__tests__/message-block.completion.test.tsx +0 -104
  38. package/src/components/__tests__/message-block.streaming.test.tsx +0 -78
  39. package/src/components/__tests__/message-with-agents.test.tsx +0 -579
  40. package/src/components/__tests__/multiline-input.test.tsx +0 -1132
  41. package/src/components/__tests__/queue-panel.test.tsx +0 -398
  42. package/src/components/__tests__/selectable-list.test.ts +0 -232
  43. package/src/components/__tests__/status-bar.test.tsx +0 -44
  44. package/src/components/__tests__/status-indicator.test.tsx +0 -204
  45. package/src/components/__tests__/terminal-thinking-orb.test.tsx +0 -111
  46. package/src/components/__tests__/user-error-banner.test.tsx +0 -102
  47. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
  48. package/src/components/ask-user/__tests__/validation.test.ts +0 -213
  49. package/src/components/tools/__tests__/apply-patch.test.tsx +0 -84
  50. package/src/components/tools/__tests__/code-search.test.tsx +0 -45
  51. package/src/components/tools/__tests__/gravity-index.test.ts +0 -76
  52. package/src/components/tools/__tests__/render-ui.test.tsx +0 -92
  53. package/src/components/tools/__tests__/run-terminal-command.test.ts +0 -233
  54. package/src/contexts/__tests__/chat-runtime-context.test.tsx +0 -149
  55. package/src/data/slash-commands.test.ts +0 -24
  56. package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +0 -49
  57. package/src/hooks/__tests__/session-fetch-signal.test.ts +0 -63
  58. package/src/hooks/__tests__/use-activity-query.test.ts +0 -1000
  59. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -176
  60. package/src/hooks/__tests__/use-auth-query.test.ts +0 -21
  61. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +0 -66
  62. package/src/hooks/__tests__/use-clipboard.test.tsx +0 -73
  63. package/src/hooks/__tests__/use-connection-status.test.ts +0 -112
  64. package/src/hooks/__tests__/use-directory-browser.test.ts +0 -203
  65. package/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
  66. package/src/hooks/__tests__/use-grid-layout.test.ts +0 -349
  67. package/src/hooks/__tests__/use-input-history.test.ts +0 -699
  68. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +0 -209
  69. package/src/hooks/__tests__/use-message-queue.test.tsx +0 -71
  70. package/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -315
  71. package/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
  72. package/src/hooks/__tests__/use-queue-ui.test.tsx +0 -83
  73. package/src/hooks/__tests__/use-searchable-list.test.ts +0 -359
  74. package/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
  75. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -361
  76. package/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
  77. package/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
  78. package/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
  79. package/src/hooks/__tests__/use-timeout.test.ts +0 -330
  80. package/src/hooks/__tests__/use-usage-query.test.ts +0 -502
  81. package/src/hooks/__tests__/use-user-details-query.test.ts +0 -195
  82. package/src/hooks/helpers/__tests__/send-message.test.ts +0 -1918
  83. package/src/init/__tests__/init-direnv.test.ts +0 -526
  84. package/src/state/__tests__/chat-store-focus.test.ts +0 -22
  85. package/src/state/__tests__/feedback-store.test.ts +0 -257
  86. package/src/utils/__tests__/active-run.test.ts +0 -140
  87. package/src/utils/__tests__/activity-tracker.test.ts +0 -440
  88. package/src/utils/__tests__/agent-display.test.ts +0 -139
  89. package/src/utils/__tests__/analytics-client.test.ts +0 -262
  90. package/src/utils/__tests__/anonymous-id.test.ts +0 -74
  91. package/src/utils/__tests__/anvil-api.test.ts +0 -578
  92. package/src/utils/__tests__/anvil-instance-owner.test.ts +0 -69
  93. package/src/utils/__tests__/anvil-model-navigation.test.ts +0 -102
  94. package/src/utils/__tests__/anvil-premium-reset.test.ts +0 -79
  95. package/src/utils/__tests__/anvil-referral-cache.test.ts +0 -116
  96. package/src/utils/__tests__/anvil-session-api.test.ts +0 -215
  97. package/src/utils/__tests__/anvil-session-display.test.ts +0 -23
  98. package/src/utils/__tests__/anvil-streak-line.test.ts +0 -217
  99. package/src/utils/__tests__/arrays.test.ts +0 -264
  100. package/src/utils/__tests__/bash-context-processor.test.ts +0 -38
  101. package/src/utils/__tests__/block-processor.test.ts +0 -810
  102. package/src/utils/__tests__/chat-history.test.ts +0 -203
  103. package/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
  104. package/src/utils/__tests__/chat-meta.test.ts +0 -130
  105. package/src/utils/__tests__/clipboard-linux.test.ts +0 -359
  106. package/src/utils/__tests__/clipboard.test.ts +0 -910
  107. package/src/utils/__tests__/code-search-summary.test.ts +0 -84
  108. package/src/utils/__tests__/collapse-helpers.test.ts +0 -1109
  109. package/src/utils/__tests__/error-handling.test.ts +0 -658
  110. package/src/utils/__tests__/exit-cleanly.test.ts +0 -110
  111. package/src/utils/__tests__/feedback-helpers.test.ts +0 -444
  112. package/src/utils/__tests__/feedback-submission.test.ts +0 -26
  113. package/src/utils/__tests__/fetch-usage.test.ts +0 -355
  114. package/src/utils/__tests__/fingerprint.test.ts +0 -144
  115. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +0 -7
  116. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +0 -3
  117. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +0 -33
  118. package/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
  119. package/src/utils/__tests__/format-timeout.test.ts +0 -87
  120. package/src/utils/__tests__/image-dimensions.test.ts +0 -249
  121. package/src/utils/__tests__/image-processor.test.ts +0 -209
  122. package/src/utils/__tests__/implementor-helpers.test.ts +0 -1458
  123. package/src/utils/__tests__/keyboard-actions.test.ts +0 -660
  124. package/src/utils/__tests__/layout-helpers.test.ts +0 -36
  125. package/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
  126. package/src/utils/__tests__/log-shipper.test.ts +0 -53
  127. package/src/utils/__tests__/markdown-renderer.test.tsx +0 -421
  128. package/src/utils/__tests__/message-block-helpers.test.ts +0 -1263
  129. package/src/utils/__tests__/message-updater.test.ts +0 -645
  130. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
  131. package/src/utils/__tests__/pending-attachments.test.ts +0 -277
  132. package/src/utils/__tests__/polling-backoff.test.ts +0 -51
  133. package/src/utils/__tests__/queue-panel-actions.test.ts +0 -148
  134. package/src/utils/__tests__/run-state-storage.test.ts +0 -919
  135. package/src/utils/__tests__/safe-json.test.ts +0 -77
  136. package/src/utils/__tests__/sdk-event-handlers.test.ts +0 -685
  137. package/src/utils/__tests__/send-message-helpers.test.ts +0 -1898
  138. package/src/utils/__tests__/send-message-timer.test.ts +0 -64
  139. package/src/utils/__tests__/settings.test.ts +0 -99
  140. package/src/utils/__tests__/strings.test.ts +0 -192
  141. package/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
  142. package/src/utils/__tests__/terminal-command-broker.test.ts +0 -528
  143. package/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
  144. package/src/utils/__tests__/terminal-io.test.ts +0 -137
  145. package/src/utils/__tests__/terminal-protocol-controller.test.ts +0 -119
  146. package/src/utils/__tests__/text-layout.test.ts +0 -82
  147. package/src/utils/__tests__/theme-platform-detection.test.ts +0 -125
  148. package/src/utils/__tests__/think-tag-parser.test.ts +0 -159
  149. package/src/utils/__tests__/trace-writer.test.ts +0 -223
  150. package/src/utils/__tests__/trim-chat-logs.test.ts +0 -78
  151. package/src/utils/__tests__/usage-banner-state.test.ts +0 -298
  152. package/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
  153. package/src/utils/__tests__/windows-terminal-health.test.ts +0 -101
  154. package/src/utils/__tests__/write-file-atomic.test.ts +0 -127
  155. package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +0 -846
  156. package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +0 -101
  157. package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +0 -1408
  158. package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +0 -258
  159. package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +0 -48
  160. package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +0 -282
  161. package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +0 -526
  162. package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +0 -68
  163. package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +0 -132
  164. package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -422
  165. package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +0 -167
  166. package/vendor/@anvil/common/src/__tests__/env-process.test.ts +0 -145
  167. package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +0 -234
  168. package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +0 -379
  169. package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +0 -807
  170. package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +0 -154
  171. package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +0 -246
  172. package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +0 -77
  173. package/vendor/@anvil/common/src/__tests__/model-config.test.ts +0 -89
  174. package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +0 -298
  175. package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +0 -116
  176. package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +0 -167
  177. package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +0 -132
  178. package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +0 -72
  179. package/vendor/@anvil/common/src/__tests__/user-state.test.ts +0 -30
  180. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +0 -63
  181. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +0 -246
  182. package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +0 -106
  183. package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +0 -351
  184. package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +0 -169
  185. package/vendor/@anvil/common/src/testing/errors.ts +0 -33
  186. package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +0 -334
  187. package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +0 -6
  188. package/vendor/@anvil/common/src/testing/index.ts +0 -84
  189. package/vendor/@anvil/common/src/testing/mock-modules.ts +0 -53
  190. package/vendor/@anvil/common/src/testing/mock-types.ts +0 -123
  191. package/vendor/@anvil/common/src/testing/mocks/analytics.ts +0 -261
  192. package/vendor/@anvil/common/src/testing/mocks/child-process.ts +0 -93
  193. package/vendor/@anvil/common/src/testing/mocks/crypto.ts +0 -218
  194. package/vendor/@anvil/common/src/testing/mocks/database.ts +0 -337
  195. package/vendor/@anvil/common/src/testing/mocks/fetch.ts +0 -219
  196. package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +0 -166
  197. package/vendor/@anvil/common/src/testing/mocks/index.ts +0 -101
  198. package/vendor/@anvil/common/src/testing/mocks/logger.ts +0 -135
  199. package/vendor/@anvil/common/src/testing/mocks/stream.ts +0 -313
  200. package/vendor/@anvil/common/src/testing/mocks/timers.ts +0 -132
  201. package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +0 -127
  202. package/vendor/@anvil/common/src/testing/setup.ts +0 -282
  203. package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
  204. package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -213
  205. package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +0 -47
  206. package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +0 -65
  207. package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +0 -49
  208. package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
  209. package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +0 -26
  210. package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -122
  211. package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +0 -102
  212. package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +0 -143
  213. package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +0 -202
  214. package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +0 -193
  215. package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +0 -140
  216. package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +0 -148
  217. package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
  218. package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +0 -57
  219. package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +0 -774
  220. package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +0 -206
  221. package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +0 -173
  222. package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +0 -60
  223. package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +0 -31
  224. package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +0 -1252
  225. package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
  226. package/vendor/@anvil/common/src/util/__tests__/path.test.ts +0 -22
  227. package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +0 -90
  228. package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +0 -325
  229. package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +0 -28
  230. package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +0 -130
  231. package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +0 -1008
  232. package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +0 -289
  233. package/vendor/@anvil/common/src/util/__tests__/string.test.ts +0 -239
  234. package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +0 -70
  235. package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +0 -108
  236. package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +0 -180
  237. package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +0 -24
  238. package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +0 -88
  239. package/vendor/@anvil/common/src/util/skills.test.ts +0 -44
  240. package/vendor/@anvil/sdk/dist/index.cjs.map +0 -440
  241. package/vendor/@anvil/sdk/dist/index.mjs.map +0 -440
package/bin/anvil.js CHANGED
@@ -26,6 +26,24 @@ const PKG_ROOT = path.resolve(HERE, '..')
26
26
  const ENTRY = path.join(PKG_ROOT, 'src', 'entry.ts')
27
27
  const IS_WIN = process.platform === 'win32'
28
28
  const BUN_EXE = IS_WIN ? 'bun.exe' : 'bun'
29
+ const COMPILED_BINARY = IS_WIN
30
+ ? path.join(HERE, 'anvil.exe')
31
+ : path.join(HERE, 'anvil')
32
+
33
+ // Release installs include a compiled binary. Prefer it so the global npm
34
+ // shim runs the same build as `cli/bin/anvil.exe` and does not require Bun.
35
+ if (!process.versions.bun && fs.existsSync(COMPILED_BINARY)) {
36
+ const result = spawnSync(COMPILED_BINARY, process.argv.slice(2), {
37
+ stdio: 'inherit',
38
+ env: process.env,
39
+ windowsHide: true,
40
+ })
41
+ if (result.error) {
42
+ console.error(`Failed to start compiled Anvil (${COMPILED_BINARY}):`, result.error.message)
43
+ process.exit(1)
44
+ }
45
+ process.exit(result.status ?? (result.signal ? 1 : 0))
46
+ }
29
47
 
30
48
  /** Check that a candidate command is a working bun executable. */
31
49
  function isBun(cmd) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@alumbwe/anvil",
3
- "version": "1.0.41",
3
+ "version": "1.0.49",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "anvil": "./bin/anvil.js"
7
+ "anvil": "bin/anvil.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/anvil.js",
@@ -13,10 +13,16 @@
13
13
  "vendor/",
14
14
  "tsconfig.json",
15
15
  "bunfig.toml",
16
- "!src/__tests__/",
17
- "!src/testing/",
18
- "!src/smoke/",
19
- "!src/dist-private/"
16
+ "!**/__tests__/**",
17
+ "!**/__mocks__/**",
18
+ "!**/__fixtures__/**",
19
+ "!**/*.test.*",
20
+ "!**/*.spec.*",
21
+ "!**/testing/**",
22
+ "!**/smoke/**",
23
+ "!src/dist-private/**",
24
+ "!**/*.map",
25
+ "!**/*.tsbuildinfo"
20
26
  ],
21
27
  "repository": {
22
28
  "type": "git",
@@ -63,6 +69,7 @@
63
69
  "node-machine-id": "^1.1.12",
64
70
  "open": "^10.1.0",
65
71
  "partial-json": "^0.1.7",
72
+ "picocolors": "^1.1.1",
66
73
  "pino": "9.4.0",
67
74
  "posthog-node": "^5.8.0",
68
75
  "react": "^19.0.0",
@@ -4,7 +4,7 @@
4
4
  * This file is generated by scripts/prebuild-agents.ts
5
5
  * It contains all bundled agent definitions from the agents/ directory.
6
6
  *
7
- * Generated at: 2026-09-11T11:23:27.005Z
7
+ * Generated at: 2026-11-01T15:44:55.857Z
8
8
  * Agent count: 80
9
9
  */
10
10
 
package/src/entry.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ // MUST be first: applies production defaults for NEXT_PUBLIC_* before any
4
+ // module in the import graph evaluates `@anvil/common/env`, which validates
5
+ // the client env at import time and throws on machines without an env file.
6
+ import './pre-init/client-env'
7
+
3
8
  import {
4
9
  isTerminalCommandBrokerInvocation,
5
10
  serveTerminalCommandBroker,
package/src/index.tsx CHANGED
@@ -27,6 +27,8 @@ import { App } from './app'
27
27
  import { loadPackageVersion, parseArgs } from './cli-args'
28
28
  import { handlePublish } from './commands/publish'
29
29
  import { runPlainLogin } from './login/plain-login'
30
+ import { ensureSignedIn } from './login/supabase-auth'
31
+ import { isManagedGatewayMode } from '@anvil/common/constants/deepseek-direct'
30
32
  import { initializeApp } from './init/init-app'
31
33
  import { loadEnvLocal } from './setup/load-env'
32
34
  import { runKeySetup } from './setup/key-setup'
@@ -248,19 +250,40 @@ async function main(): Promise<void> {
248
250
  process.exit(0)
249
251
  }
250
252
 
251
- // First-run: if required API keys are missing, launch the setup wizard.
252
- // This runs BEFORE the TUI renderer so it can use plain console I/O.
253
- if (!process.env.DEEPSEEK_API_KEY || !process.env.GEMINI_API_KEY) {
253
+ // First-run: required local API keys. In managed gateway mode the DeepSeek
254
+ // (model) key lives on the SERVER — the gateway injects it — so the CLI must
255
+ // Managed mode does not require local provider keys. In direct development
256
+ // mode both provider keys are required locally.
257
+ const gatewayMode = isManagedGatewayMode()
258
+ // Managed mode keeps both model keys in Supabase; an installed CLI only
259
+ // needs its stored Supabase access token. Local provider keys are required
260
+ // only for direct development mode.
261
+ if (!gatewayMode && (!process.env.DEEPSEEK_API_KEY || !process.env.GEMINI_API_KEY)) {
254
262
  const result = await runKeySetup(process.cwd())
255
263
  if (!result.allRequired) {
256
264
  console.error(
257
- '\n DeepSeek and Gemini API keys are required.\n' +
265
+ '\n Required API keys are missing.\n' +
258
266
  ' Run with `setup` to reconfigure, or set them in .env.local manually.\n',
259
267
  )
260
268
  process.exit(1)
261
269
  }
262
270
  }
263
271
 
272
+ // Managed gateway mode: `anvil` recognizes whether you're signed in and, if
273
+ // not, launches the browser sign-in before starting. (Direct mode and the
274
+ // login/setup commands are handled above and need no gate here.)
275
+ if (gatewayMode && !isPublishCommand) {
276
+ try {
277
+ await ensureSignedIn()
278
+ setApiClientAuthToken(getAuthToken())
279
+ } catch {
280
+ console.error(
281
+ '\n Sign-in required to use Anvil. Run `anvil login` and try again.\n',
282
+ )
283
+ process.exit(1)
284
+ }
285
+ }
286
+
264
287
  // Show project picker only when user starts at the home directory or an ancestor
265
288
  const projectRoot = getProjectRoot()
266
289
  const homeDir = os.homedir()
@@ -1,8 +1,14 @@
1
1
  import { AnalyticsEvent } from '@anvil/common/constants/analytics-events'
2
+ import { isManagedGatewayMode } from '@anvil/common/constants/deepseek-direct'
2
3
  import { cyan, green, red, yellow, bold } from 'picocolors'
3
4
 
4
5
  import { LOGIN_WEBSITE_URL } from './constants'
5
6
  import { generateLoginUrl, pollLoginStatus } from './login-flow'
7
+ import {
8
+ isSupabaseAuthConfigured,
9
+ loginViaBrowser,
10
+ signInWithPassword,
11
+ } from './supabase-auth'
6
12
  import {
7
13
  flushAnalytics,
8
14
  identifyUser,
@@ -23,7 +29,61 @@ import type { User } from '../utils/auth'
23
29
  * This is the escape hatch for remote/SSH environments where the TUI's
24
30
  * clipboard and browser integration don't work.
25
31
  */
32
+ /**
33
+ * Managed gateway mode: authenticate with Supabase (the cli-agent gateway
34
+ * verifies a Supabase JWT). Opens the browser to the hosted sign-in page and
35
+ * captures the session via a local loopback callback. Non-interactive when
36
+ * ANVIL_LOGIN_EMAIL + ANVIL_LOGIN_PASSWORD are set.
37
+ */
38
+ export async function runSupabaseLogin(): Promise<void> {
39
+ console.log()
40
+ console.log(bold('Anvil Login'))
41
+ console.log()
42
+
43
+ const envEmail = process.env.ANVIL_LOGIN_EMAIL
44
+ const envPassword = process.env.ANVIL_LOGIN_PASSWORD
45
+
46
+ try {
47
+ let user
48
+ if (envEmail && envPassword) {
49
+ console.log(`Signing in as ${envEmail}...`)
50
+ user = await signInWithPassword(envEmail, envPassword)
51
+ } else {
52
+ console.log('Waiting for you to finish signing in in your browser…')
53
+ user = await loginViaBrowser()
54
+ }
55
+
56
+ if (user.id) {
57
+ identifyUser(user.id, { email: user.email, anvil: IS_ANVIL })
58
+ trackEvent(AnalyticsEvent.LOGIN, {
59
+ userId: user.id,
60
+ via: 'plain_command',
61
+ hasEmail: Boolean(user.email),
62
+ hasName: Boolean(user.name),
63
+ })
64
+ await flushAnalytics()
65
+ }
66
+
67
+ console.log()
68
+ console.log(green(`✓ Logged in as ${user.name} (${user.email})`))
69
+ console.log()
70
+ console.log('You can now run ' + cyan('anvil') + ' to start.')
71
+ process.exit(0)
72
+ } catch (error) {
73
+ console.error(
74
+ red(`Login failed: ${error instanceof Error ? error.message : String(error)}`),
75
+ )
76
+ process.exit(1)
77
+ }
78
+ }
79
+
26
80
  export async function runPlainLogin(): Promise<void> {
81
+ // Managed gateway mode uses Supabase auth (the gateway verifies a Supabase
82
+ // JWT), not the Anvil device-code flow.
83
+ if (isManagedGatewayMode() && isSupabaseAuthConfigured()) {
84
+ return runSupabaseLogin()
85
+ }
86
+
27
87
  const fingerprintId = await getFingerprintId()
28
88
 
29
89
  console.log()
@@ -0,0 +1,346 @@
1
+ /**
2
+ * supabase-auth.ts — CLI authentication for managed gateway mode.
3
+ *
4
+ * The cli-agent gateway authenticates each request with a Supabase JWT
5
+ * (`sb.auth.getUser(token)`). So in managed mode the CLI must hold a Supabase
6
+ * session, not an Anvil API key. This module talks to Supabase's GoTrue REST
7
+ * API directly (no extra dependency), stores the session in the normal
8
+ * credentials file as `authToken` (= access token) plus the refresh token and
9
+ * expiry, and refreshes the access token before it expires.
10
+ *
11
+ * Supported sign-in methods:
12
+ * - email OTP: requestEmailOtp() → verifyEmailOtp()
13
+ * - email + password: signInWithPassword()
14
+ */
15
+
16
+ import http from 'node:http'
17
+
18
+ import open from 'open'
19
+
20
+ import { logger } from '../utils/logger'
21
+ import { getUserCredentials, saveUserCredentials, type User } from '../utils/auth'
22
+
23
+ /** GoTrue session as returned by /token and /verify. */
24
+ interface SupabaseSession {
25
+ access_token: string
26
+ refresh_token: string
27
+ expires_at?: number
28
+ expires_in?: number
29
+ user?: {
30
+ id?: string
31
+ email?: string
32
+ user_metadata?: { name?: string; full_name?: string }
33
+ }
34
+ }
35
+
36
+ function supabaseUrl(): string | undefined {
37
+ const url = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL
38
+ return url ? url.replace(/\/+$/, '') : undefined
39
+ }
40
+
41
+ /** The anon/publishable key — GoTrue requires it as the `apikey` header. */
42
+ function supabaseAnonKey(): string | undefined {
43
+ return (
44
+ process.env.SUPABASE_PUBLISHABLE_KEY ??
45
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ??
46
+ process.env.SUPABASE_ANON_KEY ??
47
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
48
+ )
49
+ }
50
+
51
+ /** True when Supabase auth can run (URL + anon key configured). */
52
+ export function isSupabaseAuthConfigured(): boolean {
53
+ return Boolean(supabaseUrl() && supabaseAnonKey())
54
+ }
55
+
56
+ function authConfig(): { url: string; anonKey: string } {
57
+ const url = supabaseUrl()
58
+ const anonKey = supabaseAnonKey()
59
+ if (!url || !anonKey) {
60
+ throw new Error(
61
+ 'Supabase is not configured. Set SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.',
62
+ )
63
+ }
64
+ return { url, anonKey }
65
+ }
66
+
67
+ async function gotrue(
68
+ path: string,
69
+ body: Record<string, unknown>,
70
+ ): Promise<SupabaseSession> {
71
+ const { url, anonKey } = authConfig()
72
+ const res = await fetch(`${url}/auth/v1${path}`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ apikey: anonKey,
77
+ Authorization: `Bearer ${anonKey}`,
78
+ },
79
+ body: JSON.stringify(body),
80
+ })
81
+ const text = await res.text()
82
+ let json: unknown
83
+ try {
84
+ json = text ? JSON.parse(text) : {}
85
+ } catch {
86
+ json = {}
87
+ }
88
+ if (!res.ok) {
89
+ const message =
90
+ (json as { error_description?: string; msg?: string; error?: string })
91
+ ?.error_description ??
92
+ (json as { msg?: string }).msg ??
93
+ (json as { error?: string }).error ??
94
+ `Supabase auth failed (${res.status})`
95
+ throw new Error(message)
96
+ }
97
+ return json as SupabaseSession
98
+ }
99
+
100
+ /** Normalize a session's expiry to epoch seconds. */
101
+ function expiresAtOf(session: SupabaseSession): number {
102
+ if (typeof session.expires_at === 'number') return session.expires_at
103
+ const ttl = typeof session.expires_in === 'number' ? session.expires_in : 3600
104
+ return Math.floor(Date.now() / 1000) + ttl
105
+ }
106
+
107
+ /** Map a Supabase session to the credentials `User` shape and persist it. */
108
+ export function saveSupabaseSession(session: SupabaseSession): User {
109
+ const meta = session.user?.user_metadata
110
+ const user: User = {
111
+ id: session.user?.id,
112
+ email: session.user?.email ?? '',
113
+ name: meta?.name ?? meta?.full_name ?? session.user?.email ?? 'Anvil user',
114
+ authToken: session.access_token,
115
+ refreshToken: session.refresh_token,
116
+ expiresAt: expiresAtOf(session),
117
+ provider: 'supabase',
118
+ }
119
+ saveUserCredentials(user)
120
+ return user
121
+ }
122
+
123
+ /** Send an email OTP (6-digit code / magic link) for sign-in or signup. */
124
+ export async function requestEmailOtp(email: string): Promise<void> {
125
+ await gotrue('/otp', { email, create_user: true })
126
+ }
127
+
128
+ /** Verify an email OTP code and persist the resulting session. */
129
+ export async function verifyEmailOtp(email: string, token: string): Promise<User> {
130
+ const session = await gotrue('/verify', { type: 'email', email, token })
131
+ return saveSupabaseSession(session)
132
+ }
133
+
134
+ /** Sign in with email + password and persist the resulting session. */
135
+ export async function signInWithPassword(
136
+ email: string,
137
+ password: string,
138
+ ): Promise<User> {
139
+ const session = await gotrue('/token?grant_type=password', { email, password })
140
+ return saveSupabaseSession(session)
141
+ }
142
+
143
+ /** Exchange a refresh token for a new session and persist it. */
144
+ async function refreshSupabaseSession(refreshToken: string): Promise<User> {
145
+ const session = await gotrue('/token?grant_type=refresh_token', {
146
+ refresh_token: refreshToken,
147
+ })
148
+ return saveSupabaseSession(session)
149
+ }
150
+
151
+ // ── Browser sign-in (loopback) ─────────────────────────────────────────────
152
+
153
+ /** Hosted sign-in page that completes auth in the browser, then redirects the
154
+ * session back to the CLI's loopback callback. Override with ANVIL_LOGIN_URL. */
155
+ export function getLoginPageUrl(): string {
156
+ return (
157
+ process.env.ANVIL_LOGIN_URL ?? 'https://cli.tryanvil.dev/login'
158
+ ).replace(/\/+$/, '')
159
+ }
160
+
161
+ /** Decode a JWT payload (no verification — only for display id/email). */
162
+ function decodeJwt(token: string): { sub?: string; email?: string } {
163
+ try {
164
+ const part = token.split('.')[1] ?? ''
165
+ const json = Buffer.from(
166
+ part.replace(/-/g, '+').replace(/_/g, '/'),
167
+ 'base64',
168
+ ).toString('utf8')
169
+ return JSON.parse(json)
170
+ } catch {
171
+ return {}
172
+ }
173
+ }
174
+
175
+ /** Served on the loopback /callback: reads the URL fragment and posts the
176
+ * tokens back to this same server (same-origin, so no mixed-content issues). */
177
+ function callbackHtml(): string {
178
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
179
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
180
+ <title>Anvil — signing in</title>
181
+ <style>body{margin:0;height:100vh;display:grid;place-items:center;background:#0a0a0a;color:#f5f5f5;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}div{text-align:center}p{color:#a0a0a0}</style>
182
+ </head><body><div><h2>Signing you in…</h2><p id="m">One moment.</p></div>
183
+ <script>
184
+ (function(){
185
+ var p = new URLSearchParams(location.hash.replace(/^#/,''));
186
+ var body = {access_token:p.get('access_token'),refresh_token:p.get('refresh_token'),expires_at:p.get('expires_at')};
187
+ if(!body.access_token){document.getElementById('m').textContent='No token found. Return to the terminal and run anvil login again.';return;}
188
+ fetch('/token',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)})
189
+ .then(function(){document.getElementById('m').textContent='You are signed in. You can close this tab and return to your terminal.';})
190
+ .catch(function(){document.getElementById('m').textContent='Could not reach the CLI. Is "anvil login" still running?';});
191
+ })();
192
+ </script></body></html>`
193
+ }
194
+
195
+ /**
196
+ * Open the browser to the hosted sign-in page and capture the session via a
197
+ * local loopback callback (the gh/vercel-style flow). Resolves once the
198
+ * browser posts back the Supabase session, which is then persisted.
199
+ */
200
+ export async function loginViaBrowser(timeoutMs = 300_000): Promise<User> {
201
+ if (!isSupabaseAuthConfigured()) {
202
+ throw new Error(
203
+ 'Supabase is not configured. Set SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.',
204
+ )
205
+ }
206
+
207
+ return new Promise<User>((resolve, reject) => {
208
+ let settled = false
209
+ const server = http.createServer((req, res) => {
210
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1')
211
+ if (req.method === 'GET' && url.pathname === '/callback') {
212
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
213
+ res.end(callbackHtml())
214
+ return
215
+ }
216
+ if (req.method === 'POST' && url.pathname === '/token') {
217
+ let data = ''
218
+ req.on('data', (c) => {
219
+ data += c
220
+ if (data.length > 1_000_000) req.destroy()
221
+ })
222
+ req.on('end', () => {
223
+ try {
224
+ const parsed = JSON.parse(data || '{}') as {
225
+ access_token?: string
226
+ refresh_token?: string
227
+ expires_at?: string
228
+ }
229
+ if (!parsed.access_token) {
230
+ res.writeHead(400)
231
+ res.end('{"error":"no token"}')
232
+ return
233
+ }
234
+ const claims = decodeJwt(parsed.access_token)
235
+ const user = saveSupabaseSession({
236
+ access_token: parsed.access_token,
237
+ refresh_token: parsed.refresh_token ?? '',
238
+ expires_at: parsed.expires_at ? Number(parsed.expires_at) : undefined,
239
+ user: {
240
+ id: claims.sub,
241
+ email: claims.email,
242
+ },
243
+ })
244
+ res.writeHead(200, { 'content-type': 'application/json' })
245
+ res.end('{"ok":true}')
246
+ finish(() => resolve(user))
247
+ } catch (error) {
248
+ res.writeHead(500)
249
+ res.end('{"error":"bad request"}')
250
+ finish(() =>
251
+ reject(error instanceof Error ? error : new Error(String(error))),
252
+ )
253
+ }
254
+ })
255
+ return
256
+ }
257
+ res.writeHead(404)
258
+ res.end('not found')
259
+ })
260
+
261
+ const timer = setTimeout(
262
+ () => finish(() => reject(new Error('Login timed out. Please run anvil login again.'))),
263
+ timeoutMs,
264
+ )
265
+
266
+ function finish(then: () => void) {
267
+ if (settled) return
268
+ settled = true
269
+ clearTimeout(timer)
270
+ // Give the browser a beat to receive the 200 before the socket closes.
271
+ setTimeout(() => {
272
+ try {
273
+ server.close()
274
+ } catch {
275
+ /* already closed */
276
+ }
277
+ then()
278
+ }, 150)
279
+ }
280
+
281
+ server.on('error', (error) => finish(() => reject(error)))
282
+ server.listen(0, '127.0.0.1', () => {
283
+ const address = server.address()
284
+ const port = typeof address === 'object' && address ? address.port : 0
285
+ const redirectUri = `http://127.0.0.1:${port}/callback`
286
+ const loginUrl = `${getLoginPageUrl()}?redirect_uri=${encodeURIComponent(redirectUri)}`
287
+ console.log('\nOpening your browser to sign in…')
288
+ console.log(loginUrl, '\n')
289
+ void open(loginUrl).catch(() => {
290
+ console.log('Could not open a browser automatically — open the URL above.')
291
+ })
292
+ })
293
+ })
294
+ }
295
+
296
+ /** True when a Supabase session is already stored. */
297
+ export function hasSupabaseSession(): boolean {
298
+ const user = getUserCredentials()
299
+ return Boolean(user?.provider === 'supabase' && user.authToken)
300
+ }
301
+
302
+ /**
303
+ * Ensure the user is signed in for managed gateway mode. No-op when a Supabase
304
+ * session already exists. Non-interactive with ANVIL_LOGIN_EMAIL/PASSWORD;
305
+ * otherwise opens the browser sign-in. Throws if sign-in doesn't complete.
306
+ */
307
+ export async function ensureSignedIn(): Promise<void> {
308
+ if (hasSupabaseSession()) return
309
+ const email = process.env.ANVIL_LOGIN_EMAIL
310
+ const password = process.env.ANVIL_LOGIN_PASSWORD
311
+ if (email && password) {
312
+ await signInWithPassword(email, password)
313
+ return
314
+ }
315
+ await loginViaBrowser()
316
+ }
317
+
318
+ /** Seconds of headroom before expiry at which we proactively refresh. */
319
+ const REFRESH_SKEW_SECONDS = 60
320
+
321
+ /**
322
+ * Ensure the stored Supabase access token is still valid, refreshing it when
323
+ * it is within {@link REFRESH_SKEW_SECONDS} of expiry. No-op for non-Supabase
324
+ * credentials (direct/anvil). Returns the current valid access token, or
325
+ * undefined when there is nothing to refresh / refresh failed.
326
+ */
327
+ export async function ensureFreshAuthToken(): Promise<string | undefined> {
328
+ const user = getUserCredentials()
329
+ if (!user || user.provider !== 'supabase') return user?.authToken
330
+ const now = Math.floor(Date.now() / 1000)
331
+ const notExpiring =
332
+ typeof user.expiresAt === 'number' &&
333
+ user.expiresAt - REFRESH_SKEW_SECONDS > now
334
+ if (notExpiring) return user.authToken
335
+ if (!user.refreshToken) return user.authToken
336
+ try {
337
+ const refreshed = await refreshSupabaseSession(user.refreshToken)
338
+ return refreshed.authToken
339
+ } catch (error) {
340
+ logger.warn(
341
+ { error: error instanceof Error ? error.message : String(error) },
342
+ 'Supabase token refresh failed; using the existing token',
343
+ )
344
+ return user.authToken
345
+ }
346
+ }
@@ -0,0 +1,17 @@
1
+ // Apply production defaults for NEXT_PUBLIC_* vars BEFORE anything that
2
+ // imports `@anvil/common/env`, which validates the client env at import time
3
+ // and throws on a machine without an env file (any global npm install).
4
+ //
5
+ // Must be imported before `./index` in `entry.ts`, mirroring the tree-sitter
6
+ // pre-init: both prepare runtime state the eagerly-evaluated import graph
7
+ // needs at module-evaluation time.
8
+ //
9
+ // Gap-filling only: real values already in process.env (from ~/.env.local,
10
+ // the shell, or --define flags baked into compiled binaries) always win.
11
+ import { CLIENT_ENV_DEFAULTS } from '../utils/client-env-defaults'
12
+
13
+ for (const [key, value] of Object.entries(CLIENT_ENV_DEFAULTS)) {
14
+ if (process.env[key] === undefined) {
15
+ process.env[key] = value
16
+ }
17
+ }
@@ -3,6 +3,7 @@ import { AskUserBridge } from '@anvil/common/utils/ask-user-bridge'
3
3
  import { AnvilClient } from '@anvil/sdk'
4
4
 
5
5
  import { getAuthTokenDetails } from './auth'
6
+ import { ensureFreshAuthToken } from '../login/supabase-auth'
6
7
  import { getCliEnv, getSystemProcessEnv } from './env'
7
8
  import { loadAgentDefinitions } from './local-agent-registry'
8
9
  import { logger } from './logger'
@@ -48,6 +49,9 @@ export function resetAnvilClient(): void {
48
49
 
49
50
  export async function getAnvilClient(): Promise<AnvilClient | null> {
50
51
  if (!clientInstance) {
52
+ // Managed gateway mode: refresh the Supabase access token if it is near
53
+ // expiry before reading it (no-op for direct/anvil credentials).
54
+ await ensureFreshAuthToken()
51
55
  const { token: apiKey } = getAuthTokenDetails()
52
56
 
53
57
  if (!apiKey) {
package/src/utils/auth.ts CHANGED
@@ -2,7 +2,10 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
 
4
4
  import { getCiEnv } from '@anvil/common/env-ci'
5
- import { getDirectDeepSeekApiKey } from '@anvil/common/constants/deepseek-direct'
5
+ import {
6
+ getDirectDeepSeekApiKey,
7
+ isManagedGatewayMode,
8
+ } from '@anvil/common/constants/deepseek-direct'
6
9
  import { z } from 'zod'
7
10
 
8
11
 
@@ -21,6 +24,13 @@ const userSchema = z.object({
21
24
  fingerprintId: z.string().optional(),
22
25
  fingerprintHash: z.string().optional(),
23
26
  credits: z.number().optional(),
27
+ // Managed gateway mode (Supabase): `authToken` holds the Supabase access
28
+ // token (a JWT the cli-agent gateway verifies). These let the CLI refresh it
29
+ // without a re-login. Optional + additive, so existing credentials still parse.
30
+ provider: z.string().optional(),
31
+ refreshToken: z.string().optional(),
32
+ /** Access-token expiry, epoch seconds. */
33
+ expiresAt: z.number().optional(),
24
34
  })
25
35
 
26
36
  export type User = z.infer<typeof userSchema>
@@ -117,10 +127,12 @@ export const getAuthTokenDetails = (
117
127
  return { token: envToken, source: 'environment' }
118
128
  }
119
129
 
120
- // Direct-to-DeepSeek mode: the DeepSeek key doubles as the auth token so
121
- // the CLI treats the user as authenticated without a Anvil backend.
130
+ // Direct-to-DeepSeek mode: the DeepSeek key doubles as the auth token so the
131
+ // CLI treats the user as authenticated without an Anvil backend. In managed
132
+ // gateway mode this does NOT apply — the gateway needs a real Supabase JWT,
133
+ // so a local DeepSeek key must never count as being signed in.
122
134
  const deepseekToken = getDirectDeepSeekApiKey()
123
- if (deepseekToken) {
135
+ if (deepseekToken && !isManagedGatewayMode()) {
124
136
  return { token: deepseekToken, source: 'environment' }
125
137
  }
126
138
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Production defaults for the NEXT_PUBLIC_* client env vars.
3
+ *
4
+ * `@anvil/common/env` validates all NEXT_PUBLIC_* vars at *import time*, so a
5
+ * global npm install (no .env.local anywhere on the machine) dies before any
6
+ * of our code runs. These defaults — all public-by-design values, the same
7
+ * set `cli/scripts/build-binary.ts` bakes into compiled binaries — are applied
8
+ * to `process.env` at pre-init so the CLI boots on any machine, cmd or
9
+ * PowerShell, with or without an env file.
10
+ *
11
+ * IMPORTANT: the pre-init loader (`cli/src/pre-init/client-env.ts`) imports
12
+ * this table, and `build-binary.ts` reads it at build time. Keep the two in
13
+ * sync through this module — never duplicate the values.
14
+ *
15
+ * User-set values always win: the loader only fills gaps, it never overwrites
16
+ * anything already in process.env (including values Bun loads from
17
+ * .env.local in dev, and the --define flags baked into binaries).
18
+ */
19
+ export const CLIENT_ENV_DEFAULTS: Readonly<Record<string, string>> = {
20
+ NEXT_PUBLIC_CB_ENVIRONMENT: 'prod',
21
+ NEXT_PUBLIC_ANVIL_APP_URL: 'https://anvil.dev',
22
+ NEXT_PUBLIC_SUPPORT_EMAIL: 'support@anvil.dev',
23
+ NEXT_PUBLIC_POSTHOG_API_KEY: 'phc_placeholder',
24
+ NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://us.i.posthog.com',
25
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_placeholder',
26
+ NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL: 'https://billing.stripe.com/portal',
27
+ NEXT_PUBLIC_WEB_PORT: '3000',
28
+ }