@alumbwe/anvil 1.0.42 → 1.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/anvil.js +18 -0
- package/package.json +13 -6
- package/src/agents/bundled-agents.generated.ts +1 -1
- package/src/index.tsx +27 -4
- package/src/login/plain-login.ts +60 -0
- package/src/login/supabase-auth.ts +346 -0
- package/src/utils/anvil-client.ts +4 -0
- package/src/utils/auth.ts +16 -4
- package/src/utils/config-dir.ts +1 -1
- package/vendor/@anvil/common/src/constants/deepseek-direct.ts +30 -1
- package/vendor/@anvil/common/src/util/project-ignore.ts +4 -3
- package/vendor/@anvil/sdk/dist/index.cjs +46 -10
- package/vendor/@anvil/sdk/dist/index.mjs +46 -10
- package/src/commands/__tests__/anvil-command-aliases.test.ts +0 -56
- package/src/commands/__tests__/bash-command.test.ts +0 -445
- package/src/commands/__tests__/command-args.test.ts +0 -349
- package/src/commands/__tests__/copy-conversation-clipboard.test.ts +0 -108
- package/src/commands/__tests__/copy-conversation.test.ts +0 -226
- package/src/commands/__tests__/diagnostics-command.test.ts +0 -35
- package/src/commands/__tests__/image.test.ts +0 -99
- package/src/commands/__tests__/init.test.ts +0 -456
- package/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -91
- package/src/commands/__tests__/process-diagnostics.test.ts +0 -54
- package/src/commands/__tests__/prompt-builders.test.ts +0 -28
- package/src/commands/__tests__/router-input.test.ts +0 -298
- package/src/components/__tests__/ad-banner.test.tsx +0 -83
- package/src/components/__tests__/anvil-landing-heading-row.test.tsx +0 -91
- package/src/components/__tests__/anvil-model-selector.test.tsx +0 -519
- package/src/components/__tests__/anvil-offer-invariants.test.ts +0 -54
- package/src/components/__tests__/anvil-takeover-prompt.test.tsx +0 -212
- package/src/components/__tests__/chat-input-bar.test.tsx +0 -130
- package/src/components/__tests__/grid-layout.integration.test.tsx +0 -304
- package/src/components/__tests__/grid-layout.test.tsx +0 -1051
- package/src/components/__tests__/message-block.completion.test.tsx +0 -104
- package/src/components/__tests__/message-block.streaming.test.tsx +0 -78
- package/src/components/__tests__/message-with-agents.test.tsx +0 -579
- package/src/components/__tests__/multiline-input.test.tsx +0 -1132
- package/src/components/__tests__/queue-panel.test.tsx +0 -398
- package/src/components/__tests__/selectable-list.test.ts +0 -232
- package/src/components/__tests__/status-bar.test.tsx +0 -44
- package/src/components/__tests__/status-indicator.test.tsx +0 -204
- package/src/components/__tests__/terminal-thinking-orb.test.tsx +0 -111
- package/src/components/__tests__/user-error-banner.test.tsx +0 -102
- package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
- package/src/components/ask-user/__tests__/validation.test.ts +0 -213
- package/src/components/tools/__tests__/apply-patch.test.tsx +0 -84
- package/src/components/tools/__tests__/code-search.test.tsx +0 -45
- package/src/components/tools/__tests__/gravity-index.test.ts +0 -76
- package/src/components/tools/__tests__/render-ui.test.tsx +0 -92
- package/src/components/tools/__tests__/run-terminal-command.test.ts +0 -233
- package/src/contexts/__tests__/chat-runtime-context.test.tsx +0 -149
- package/src/data/slash-commands.test.ts +0 -24
- package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +0 -49
- package/src/hooks/__tests__/session-fetch-signal.test.ts +0 -63
- package/src/hooks/__tests__/use-activity-query.test.ts +0 -1000
- package/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -176
- package/src/hooks/__tests__/use-auth-query.test.ts +0 -21
- package/src/hooks/__tests__/use-chat-input-remount.test.tsx +0 -66
- package/src/hooks/__tests__/use-clipboard.test.tsx +0 -73
- package/src/hooks/__tests__/use-connection-status.test.ts +0 -112
- package/src/hooks/__tests__/use-directory-browser.test.ts +0 -203
- package/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
- package/src/hooks/__tests__/use-grid-layout.test.ts +0 -349
- package/src/hooks/__tests__/use-input-history.test.ts +0 -699
- package/src/hooks/__tests__/use-message-queue-editing.test.tsx +0 -209
- package/src/hooks/__tests__/use-message-queue.test.tsx +0 -71
- package/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -315
- package/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
- package/src/hooks/__tests__/use-queue-ui.test.tsx +0 -83
- package/src/hooks/__tests__/use-searchable-list.test.ts +0 -359
- package/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
- package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -361
- package/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
- package/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
- package/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
- package/src/hooks/__tests__/use-timeout.test.ts +0 -330
- package/src/hooks/__tests__/use-usage-query.test.ts +0 -502
- package/src/hooks/__tests__/use-user-details-query.test.ts +0 -195
- package/src/hooks/helpers/__tests__/send-message.test.ts +0 -1918
- package/src/init/__tests__/init-direnv.test.ts +0 -526
- package/src/state/__tests__/chat-store-focus.test.ts +0 -22
- package/src/state/__tests__/feedback-store.test.ts +0 -257
- package/src/utils/__tests__/active-run.test.ts +0 -140
- package/src/utils/__tests__/activity-tracker.test.ts +0 -440
- package/src/utils/__tests__/agent-display.test.ts +0 -139
- package/src/utils/__tests__/analytics-client.test.ts +0 -262
- package/src/utils/__tests__/anonymous-id.test.ts +0 -74
- package/src/utils/__tests__/anvil-api.test.ts +0 -578
- package/src/utils/__tests__/anvil-instance-owner.test.ts +0 -69
- package/src/utils/__tests__/anvil-model-navigation.test.ts +0 -102
- package/src/utils/__tests__/anvil-premium-reset.test.ts +0 -79
- package/src/utils/__tests__/anvil-referral-cache.test.ts +0 -116
- package/src/utils/__tests__/anvil-session-api.test.ts +0 -215
- package/src/utils/__tests__/anvil-session-display.test.ts +0 -23
- package/src/utils/__tests__/anvil-streak-line.test.ts +0 -217
- package/src/utils/__tests__/arrays.test.ts +0 -264
- package/src/utils/__tests__/bash-context-processor.test.ts +0 -38
- package/src/utils/__tests__/block-processor.test.ts +0 -810
- package/src/utils/__tests__/chat-history.test.ts +0 -203
- package/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
- package/src/utils/__tests__/chat-meta.test.ts +0 -130
- package/src/utils/__tests__/clipboard-linux.test.ts +0 -359
- package/src/utils/__tests__/clipboard.test.ts +0 -910
- package/src/utils/__tests__/code-search-summary.test.ts +0 -84
- package/src/utils/__tests__/collapse-helpers.test.ts +0 -1109
- package/src/utils/__tests__/error-handling.test.ts +0 -658
- package/src/utils/__tests__/exit-cleanly.test.ts +0 -110
- package/src/utils/__tests__/feedback-helpers.test.ts +0 -444
- package/src/utils/__tests__/feedback-submission.test.ts +0 -26
- package/src/utils/__tests__/fetch-usage.test.ts +0 -355
- package/src/utils/__tests__/fingerprint.test.ts +0 -144
- package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +0 -7
- package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +0 -3
- package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +0 -33
- package/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
- package/src/utils/__tests__/format-timeout.test.ts +0 -87
- package/src/utils/__tests__/image-dimensions.test.ts +0 -249
- package/src/utils/__tests__/image-processor.test.ts +0 -209
- package/src/utils/__tests__/implementor-helpers.test.ts +0 -1458
- package/src/utils/__tests__/keyboard-actions.test.ts +0 -660
- package/src/utils/__tests__/layout-helpers.test.ts +0 -36
- package/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
- package/src/utils/__tests__/log-shipper.test.ts +0 -53
- package/src/utils/__tests__/markdown-renderer.test.tsx +0 -421
- package/src/utils/__tests__/message-block-helpers.test.ts +0 -1263
- package/src/utils/__tests__/message-updater.test.ts +0 -645
- package/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
- package/src/utils/__tests__/pending-attachments.test.ts +0 -277
- package/src/utils/__tests__/polling-backoff.test.ts +0 -51
- package/src/utils/__tests__/queue-panel-actions.test.ts +0 -148
- package/src/utils/__tests__/run-state-storage.test.ts +0 -919
- package/src/utils/__tests__/safe-json.test.ts +0 -77
- package/src/utils/__tests__/sdk-event-handlers.test.ts +0 -685
- package/src/utils/__tests__/send-message-helpers.test.ts +0 -1898
- package/src/utils/__tests__/send-message-timer.test.ts +0 -64
- package/src/utils/__tests__/settings.test.ts +0 -99
- package/src/utils/__tests__/strings.test.ts +0 -192
- package/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
- package/src/utils/__tests__/terminal-command-broker.test.ts +0 -528
- package/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
- package/src/utils/__tests__/terminal-io.test.ts +0 -137
- package/src/utils/__tests__/terminal-protocol-controller.test.ts +0 -119
- package/src/utils/__tests__/text-layout.test.ts +0 -82
- package/src/utils/__tests__/theme-platform-detection.test.ts +0 -125
- package/src/utils/__tests__/think-tag-parser.test.ts +0 -159
- package/src/utils/__tests__/trace-writer.test.ts +0 -223
- package/src/utils/__tests__/trim-chat-logs.test.ts +0 -78
- package/src/utils/__tests__/usage-banner-state.test.ts +0 -298
- package/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
- package/src/utils/__tests__/windows-terminal-health.test.ts +0 -101
- package/src/utils/__tests__/write-file-atomic.test.ts +0 -127
- package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +0 -846
- package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +0 -101
- package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +0 -1408
- package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +0 -258
- package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +0 -48
- package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +0 -282
- package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +0 -526
- package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +0 -68
- package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +0 -132
- package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -422
- package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +0 -167
- package/vendor/@anvil/common/src/__tests__/env-process.test.ts +0 -145
- package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +0 -234
- package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +0 -379
- package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +0 -807
- package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +0 -154
- package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +0 -246
- package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +0 -77
- package/vendor/@anvil/common/src/__tests__/model-config.test.ts +0 -89
- package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +0 -298
- package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +0 -116
- package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +0 -167
- package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +0 -132
- package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +0 -72
- package/vendor/@anvil/common/src/__tests__/user-state.test.ts +0 -30
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +0 -63
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +0 -246
- package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +0 -106
- package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +0 -351
- package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +0 -169
- package/vendor/@anvil/common/src/testing/errors.ts +0 -33
- package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +0 -334
- package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +0 -6
- package/vendor/@anvil/common/src/testing/index.ts +0 -84
- package/vendor/@anvil/common/src/testing/mock-modules.ts +0 -53
- package/vendor/@anvil/common/src/testing/mock-types.ts +0 -123
- package/vendor/@anvil/common/src/testing/mocks/analytics.ts +0 -261
- package/vendor/@anvil/common/src/testing/mocks/child-process.ts +0 -93
- package/vendor/@anvil/common/src/testing/mocks/crypto.ts +0 -218
- package/vendor/@anvil/common/src/testing/mocks/database.ts +0 -337
- package/vendor/@anvil/common/src/testing/mocks/fetch.ts +0 -219
- package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +0 -166
- package/vendor/@anvil/common/src/testing/mocks/index.ts +0 -101
- package/vendor/@anvil/common/src/testing/mocks/logger.ts +0 -135
- package/vendor/@anvil/common/src/testing/mocks/stream.ts +0 -313
- package/vendor/@anvil/common/src/testing/mocks/timers.ts +0 -132
- package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +0 -127
- package/vendor/@anvil/common/src/testing/setup.ts +0 -282
- package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
- package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -213
- package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +0 -47
- package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +0 -65
- package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +0 -49
- package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
- package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +0 -26
- package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -122
- package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +0 -102
- package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +0 -143
- package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +0 -202
- package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +0 -193
- package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +0 -140
- package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +0 -148
- package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
- package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +0 -57
- package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +0 -774
- package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +0 -206
- package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +0 -173
- package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +0 -60
- package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +0 -31
- package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +0 -1252
- package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
- package/vendor/@anvil/common/src/util/__tests__/path.test.ts +0 -22
- package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +0 -90
- package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +0 -325
- package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +0 -28
- package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +0 -130
- package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +0 -1008
- package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +0 -289
- package/vendor/@anvil/common/src/util/__tests__/string.test.ts +0 -239
- package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +0 -70
- package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +0 -108
- package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +0 -180
- package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +0 -24
- package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +0 -88
- package/vendor/@anvil/common/src/util/skills.test.ts +0 -44
- package/vendor/@anvil/sdk/dist/index.cjs.map +0 -440
- 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.
|
|
3
|
+
"version": "1.0.50",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"anvil": "
|
|
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
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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",
|
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:
|
|
252
|
-
//
|
|
253
|
-
|
|
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
|
|
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()
|
package/src/login/plain-login.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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 {
|
|
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
|
-
//
|
|
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
|
|
package/src/utils/config-dir.ts
CHANGED
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
* served by DeepSeek at all and falls back to the default DeepSeek model.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
|
|
21
|
+
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
|
|
22
|
+
|
|
23
|
+
// The managed gateway is deployed with the CLI's Supabase project. Keep this
|
|
24
|
+
// public endpoint in the client so an installed CLI can run without a local
|
|
25
|
+
// .env file; authentication still comes from the user's stored Supabase JWT.
|
|
26
|
+
export const MANAGED_GATEWAY_URL =
|
|
27
|
+
'https://tqolntrdmbbaptmxihtq.supabase.co/functions/v1/cli-agent'
|
|
22
28
|
|
|
23
29
|
/** Model name sent to DeepSeek for ids that are not a DeepSeek lane model. */
|
|
24
30
|
export const DEFAULT_DIRECT_DEEPSEEK_MODEL = 'deepseek-v4-pro'
|
|
@@ -31,6 +37,29 @@ export const getDirectDeepSeekApiKey = (): string | undefined =>
|
|
|
31
37
|
export const isDirectDeepSeekMode = (): boolean =>
|
|
32
38
|
Boolean(getDirectDeepSeekApiKey())
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Managed gateway mode ("no direct mode"): the CLI routes every model call
|
|
42
|
+
* through the Anvil managed gateway (the Supabase `cli-agent` Edge Function)
|
|
43
|
+
* instead of calling a provider directly. The gateway holds the provider key,
|
|
44
|
+
* injects the private system prompt server-side, and meters credits — so the
|
|
45
|
+
* brain and the key never ship in the client.
|
|
46
|
+
*
|
|
47
|
+
* Enabled whenever a gateway URL is configured: `ANVIL_GATEWAY_URL` wins,
|
|
48
|
+
* otherwise it is derived from `SUPABASE_URL` / `NEXT_PUBLIC_SUPABASE_URL`.
|
|
49
|
+
* When on, it takes precedence over direct mode, and the CLI uses a local run
|
|
50
|
+
* identity (no external account backend is required).
|
|
51
|
+
*/
|
|
52
|
+
export const getManagedGatewayUrl = (): string | undefined => {
|
|
53
|
+
// ANVIL_GATEWAY_URL remains an escape hatch for development and tests, but
|
|
54
|
+
// production installs must not depend on a locally supplied environment.
|
|
55
|
+
const explicit = process.env.ANVIL_GATEWAY_URL?.trim()
|
|
56
|
+
return (explicit || MANAGED_GATEWAY_URL).replace(/\/+$/, '')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when the managed gateway is configured (see {@link getManagedGatewayUrl}). */
|
|
60
|
+
export const isManagedGatewayMode = (): boolean =>
|
|
61
|
+
Boolean(getManagedGatewayUrl())
|
|
62
|
+
|
|
34
63
|
/** Map a Codebuff model ID to the model name DeepSeek's API accepts. */
|
|
35
64
|
export function remapModelForDirectDeepSeek(model: string): string {
|
|
36
65
|
const override = process.env.DEEPSEEK_MODEL
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as ignore from 'ignore'
|
|
2
2
|
|
|
3
3
|
export const PROJECT_IGNORE_FILES = [
|
|
4
|
-
'.gitignore',
|
|
5
|
-
'.anvilignore',
|
|
6
|
-
|
|
4
|
+
'.gitignore',
|
|
5
|
+
'.anvilignore',
|
|
6
|
+
// Legacy name retained so existing projects keep their ignore rules.
|
|
7
|
+
'.manicodeignore',
|
|
7
8
|
// Legacy: respect ignore files created by pre-Anvil installs.
|
|
8
9
|
'.codebuffignore',
|
|
9
10
|
] as const
|