@dench.com/cli 0.4.6 → 0.4.8
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/README.md +40 -2
- package/crm.ts +33 -1
- package/dench.ts +369 -20
- package/host.ts +60 -1
- package/lib/enrichment-gateway.ts +7 -4
- package/lib/load-dev-env.mjs +90 -15
- package/lib/load-dev-env.test.mjs +44 -0
- package/package.json +1 -1
- package/tools.ts +6 -2
package/README.md
CHANGED
|
@@ -23,7 +23,27 @@ dench login --name "AI Agent - Billing Repo"
|
|
|
23
23
|
to lowercase snake_case before send (e.g. `--kind "Claude Code"` becomes
|
|
24
24
|
`claude_code`).
|
|
25
25
|
|
|
26
|
-
The default
|
|
26
|
+
The default backend is `https://dench.dev` (production). Use `dench backend`
|
|
27
|
+
to switch:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dench backend show
|
|
31
|
+
dench backend set production
|
|
32
|
+
dench backend set staging
|
|
33
|
+
dench backend set local # http://localhost:3000
|
|
34
|
+
dench backend set https://example.com # custom URL
|
|
35
|
+
dench backend clear # revert to production
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For one-shot overrides (no config change):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
dench --prod <cmd>
|
|
42
|
+
dench --staging <cmd>
|
|
43
|
+
dench --backend local <cmd>
|
|
44
|
+
dench --host https://example.com <cmd>
|
|
45
|
+
DENCH_HOST=https://example.com dench <cmd>
|
|
46
|
+
```
|
|
27
47
|
|
|
28
48
|
The smooth setup flow is:
|
|
29
49
|
|
|
@@ -197,13 +217,31 @@ Auth precedence:
|
|
|
197
217
|
|
|
198
218
|
1. Explicit `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
199
219
|
`DENCH_WORKSPACE`, `DENCH_DEV_AGENT_KEY`).
|
|
200
|
-
2. A locally-stored `dench login` session for the current
|
|
220
|
+
2. A locally-stored `dench login` session for the current backend (set via
|
|
221
|
+
`dench backend`, `--host`, `--prod`, or `--staging`).
|
|
201
222
|
3. `DENCH_API_KEY` + `CONVEX_URL` (sandbox / automation path).
|
|
202
223
|
|
|
203
224
|
Plain terminal usage does not silently fall back to dev env vars. Without
|
|
204
225
|
`--dev`, a saved session, or `DENCH_API_KEY` + `CONVEX_URL`, live commands
|
|
205
226
|
return `login_required`.
|
|
206
227
|
|
|
228
|
+
### `.env` auto-load is opt-in
|
|
229
|
+
|
|
230
|
+
When `dench` runs from inside the `dench.com` repo it does **not**
|
|
231
|
+
auto-load workspace `.env` / `.env.local` files by default — that used
|
|
232
|
+
to silently flip `NEXT_PUBLIC_CONVEX_URL` to the dev Convex deployment
|
|
233
|
+
and made the CLI talk to local Convex even when the user expected
|
|
234
|
+
production.
|
|
235
|
+
|
|
236
|
+
Auto-load now turns on only when one of:
|
|
237
|
+
|
|
238
|
+
- `--dev` (also switches the runtime to dev-workspace mode)
|
|
239
|
+
- `--use-dev-env` (load `.env` files but keep the rest of the runtime)
|
|
240
|
+
- `DENCH_USE_DEV_ENV=1` in the shell env (e.g. via `direnv`)
|
|
241
|
+
- `dench backend set local` (a stored local backend implies dev env)
|
|
242
|
+
|
|
243
|
+
Explicit shell exports (`GATEWAY_URL=… ./dench …`) always win regardless.
|
|
244
|
+
|
|
207
245
|
Commands that need to impersonate a specific agent (`dench memory`,
|
|
208
246
|
`dench task`, `dench claim`, `dench log`, `dench approval request`,
|
|
209
247
|
etc.) still require an agent session (`dench login`) — the unified API
|
package/crm.ts
CHANGED
|
@@ -412,6 +412,9 @@ const api = {
|
|
|
412
412
|
"functions/crm/actions:startActionRun",
|
|
413
413
|
),
|
|
414
414
|
},
|
|
415
|
+
members: {
|
|
416
|
+
list: makeFunctionReference<"query">("functions/crm/members:list"),
|
|
417
|
+
},
|
|
415
418
|
};
|
|
416
419
|
|
|
417
420
|
export async function runCrmCommand(opts: {
|
|
@@ -472,11 +475,31 @@ export async function runCrmCommand(opts: {
|
|
|
472
475
|
return await runDocsCommand(ctx);
|
|
473
476
|
case "actions":
|
|
474
477
|
return await runActionsCommand(ctx);
|
|
478
|
+
case "members":
|
|
479
|
+
// Alias for `dench members` so the AI agent finds the roster
|
|
480
|
+
// surface from inside the `dench crm` namespace it's primed
|
|
481
|
+
// on. Delegates to the shared implementation in cli/members.ts
|
|
482
|
+
// so payload formatting + flag handling stay in lockstep.
|
|
483
|
+
return await runCrmMembersCommand(ctx);
|
|
475
484
|
default:
|
|
476
485
|
throw new CrmCliError(`Unknown crm subcommand: ${subcommand}`);
|
|
477
486
|
}
|
|
478
487
|
}
|
|
479
488
|
|
|
489
|
+
async function runCrmMembersCommand(ctx: CrmCliContext): Promise<void> {
|
|
490
|
+
const { runMembersCommand } = await import("./members");
|
|
491
|
+
// Re-prepend `--json` so the members dispatcher's own `hasFlag`
|
|
492
|
+
// pass sees it again — we already drained it once when computing
|
|
493
|
+
// `ctx.jsonOutput`, but the members CLI re-parses argv from scratch
|
|
494
|
+
// (same shape as a top-level invocation).
|
|
495
|
+
const args = ctx.jsonOutput ? ["--json", ...ctx.args] : [...ctx.args];
|
|
496
|
+
await runMembersCommand({
|
|
497
|
+
convex: ctx.convex,
|
|
498
|
+
args,
|
|
499
|
+
sessionToken: ctx.sessionToken,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
480
503
|
async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
|
|
481
504
|
const verb = ctx.args.shift();
|
|
482
505
|
switch (verb) {
|
|
@@ -1653,7 +1676,7 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1653
1676
|
|
|
1654
1677
|
async function enrichOneEntry(
|
|
1655
1678
|
ctx: CrmCliContext,
|
|
1656
|
-
args: CellEnrichmentArgs & {
|
|
1679
|
+
args: Omit<CellEnrichmentArgs, "entryId"> & {
|
|
1657
1680
|
target: EnrichmentTarget;
|
|
1658
1681
|
entry: CrmEntryForEnrichment;
|
|
1659
1682
|
},
|
|
@@ -2057,6 +2080,15 @@ Statuses (kanban columns):
|
|
|
2057
2080
|
dench crm statuses list <object>
|
|
2058
2081
|
dench crm statuses set <object> --statuses '[{"name":"New","color":"#94a3b8"},...]'
|
|
2059
2082
|
|
|
2083
|
+
Workspace members (for user-type fields):
|
|
2084
|
+
dench crm members list [--json] (alias: dench members list)
|
|
2085
|
+
List \`{ userId, role, name, email }\` for every member of the
|
|
2086
|
+
current org. Use the userIds when setting \`Owner\`, \`Assignee\`,
|
|
2087
|
+
or any other \`user\`-type field so the UI shows a real linked
|
|
2088
|
+
avatar instead of an unlinked literal-string badge:
|
|
2089
|
+
dench crm cells set task --entry <id> --field Owner --value <userId>
|
|
2090
|
+
dench crm entries create task --data '{"Name":"X","Owner":"<userId>"}'
|
|
2091
|
+
|
|
2060
2092
|
Batch:
|
|
2061
2093
|
dench crm batch --file ops.jsonl [--idempotency-key <key>]
|
|
2062
2094
|
For repeated writes, prefer entries create-many/update-many or cells set-many;
|
package/dench.ts
CHANGED
|
@@ -18,7 +18,12 @@ import { agentKindLabel, normalizeAgentKind } from "./agentKind";
|
|
|
18
18
|
import {
|
|
19
19
|
DEFAULT_HOST,
|
|
20
20
|
isLocalHost,
|
|
21
|
+
LOCAL_HOST,
|
|
21
22
|
normalizeHost,
|
|
23
|
+
PRODUCTION_API_URL,
|
|
24
|
+
PRODUCTION_CONVEX_URL,
|
|
25
|
+
PRODUCTION_HOST,
|
|
26
|
+
resolveBackendAlias,
|
|
22
27
|
resolveHostFromArgs,
|
|
23
28
|
STAGING_HOST,
|
|
24
29
|
} from "./host";
|
|
@@ -29,7 +34,6 @@ import {
|
|
|
29
34
|
listStoredSessionEntries,
|
|
30
35
|
removeAllStoredSessions,
|
|
31
36
|
removeStoredSession,
|
|
32
|
-
resolveConfigHost,
|
|
33
37
|
resolveSessionScope,
|
|
34
38
|
type SessionScope,
|
|
35
39
|
type StoredSessionEntry,
|
|
@@ -65,6 +69,24 @@ type StoredSession = {
|
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
type ConfigFile = {
|
|
72
|
+
/**
|
|
73
|
+
* Explicit user choice of which Dench backend to talk to by default,
|
|
74
|
+
* set via `dench backend set <local|staging|prod|<url>>`. Takes
|
|
75
|
+
* precedence over the legacy `currentHost` field (which used to be
|
|
76
|
+
* silently updated by every `dench login` and caused the CLI to
|
|
77
|
+
* silently follow the last-logged-in environment).
|
|
78
|
+
*
|
|
79
|
+
* Resolution order in `resolveHost`:
|
|
80
|
+
* 1. CLI flags (`--prod`, `--staging`, `--backend`, `--host`)
|
|
81
|
+
* 2. `DENCH_HOST` env var
|
|
82
|
+
* 3. `defaultBackend` from this config
|
|
83
|
+
* 4. `PRODUCTION_HOST` (the safe fallback)
|
|
84
|
+
*
|
|
85
|
+
* `currentHost` / `currentHosts` / `currentSessionKey` /
|
|
86
|
+
* `currentSessionKeys` keep their old role of remembering which
|
|
87
|
+
* stored session to use, but they no longer drive backend choice.
|
|
88
|
+
*/
|
|
89
|
+
defaultBackend?: string;
|
|
68
90
|
currentHost?: string;
|
|
69
91
|
currentSessionKey?: string;
|
|
70
92
|
currentHosts?: Record<string, string>;
|
|
@@ -244,6 +266,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
244
266
|
"--staging",
|
|
245
267
|
"--prod",
|
|
246
268
|
"--no-open",
|
|
269
|
+
"--use-dev-env",
|
|
247
270
|
"--version",
|
|
248
271
|
"--yolo",
|
|
249
272
|
"--ask-before-publishing",
|
|
@@ -407,6 +430,7 @@ Usage:
|
|
|
407
430
|
dench sessions [--host <host>] [--json]
|
|
408
431
|
dench use <session-key-or-workspace-slug> [--host <host>] [--json]
|
|
409
432
|
dench logout [session-key-or-workspace-slug] [--session <key-or-scope>] [--host <host>] [--all] [--json]
|
|
433
|
+
dench backend [show|set|clear] [<production|staging|local|<url>>] [--json]
|
|
410
434
|
dench context [--json]
|
|
411
435
|
dench status [--mine|--self] [--json]
|
|
412
436
|
dench tasks [--json]
|
|
@@ -454,6 +478,13 @@ CRM (Daytona-native rewrite):
|
|
|
454
478
|
Batch write helpers: entries create-many/update-many, cells set-many
|
|
455
479
|
Help: dench crm help
|
|
456
480
|
|
|
481
|
+
Workspace members (for CRM \`user\`-type fields):
|
|
482
|
+
dench members list [--json] (top-level alias)
|
|
483
|
+
dench crm members list [--json] (canonical CRM-scoped name)
|
|
484
|
+
Look up real userIds for the active org so \`Owner\` / \`Assignee\`
|
|
485
|
+
cells assign to real members instead of being stored as unlinked
|
|
486
|
+
display-name strings. See \`dench members help\`.
|
|
487
|
+
|
|
457
488
|
Live web search (Exa via the Dench Cloud Gateway, auths with DENCH_API_KEY):
|
|
458
489
|
dench search "<query>" [--num-results N] [--type auto|fast|deep|deep-reasoning|neural] [--category news|company|people|...] [--include-domains a.com,b.com] [--exclude-domains c.com] [--max-chars 800] [--json]
|
|
459
490
|
dench search contents <url> [<url>...] [--max-chars 4000] [--summary "..."] [--json]
|
|
@@ -549,13 +580,25 @@ Agent setup:
|
|
|
549
580
|
Use dench memory and dench artifacts to continue from durable context.
|
|
550
581
|
Do not create, claim, or log a setup task by default.
|
|
551
582
|
|
|
552
|
-
|
|
553
|
-
Default
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
583
|
+
Backends and hosts:
|
|
584
|
+
Default backend: ${DEFAULT_HOST} (overridden by \`dench backend set\`)
|
|
585
|
+
Show / change the persistent default:
|
|
586
|
+
dench backend show
|
|
587
|
+
dench backend set production | staging | local | <url>
|
|
588
|
+
dench backend clear
|
|
589
|
+
One-shot overrides for a single command:
|
|
590
|
+
dench --prod <cmd>
|
|
591
|
+
dench --staging <cmd>
|
|
592
|
+
dench --backend local <cmd>
|
|
593
|
+
dench --host https://example.com <cmd>
|
|
594
|
+
DENCH_HOST=https://example.com dench <cmd>
|
|
595
|
+
|
|
596
|
+
Dev fallback (run against a local Convex deployment with a dev key):
|
|
558
597
|
DENCH_DEV_AGENT_KEY=... DENCH_WORKSPACE=... NEXT_PUBLIC_CONVEX_URL=... dench status --dev
|
|
598
|
+
Inside the dench.com repo, --dev also auto-loads .env / .env.local
|
|
599
|
+
so GATEWAY_URL etc. flow through. Use --use-dev-env (or
|
|
600
|
+
DENCH_USE_DEV_ENV=1) when you want that auto-load without switching
|
|
601
|
+
the runtime to the dev workspace mode.
|
|
559
602
|
|
|
560
603
|
Advanced:
|
|
561
604
|
For long-lived agents or when the human asks:
|
|
@@ -728,8 +771,49 @@ Notes:
|
|
|
728
771
|
`);
|
|
729
772
|
}
|
|
730
773
|
|
|
731
|
-
function
|
|
732
|
-
|
|
774
|
+
function backendHelp() {
|
|
775
|
+
console.log(`Dench backend
|
|
776
|
+
|
|
777
|
+
Usage:
|
|
778
|
+
dench backend # alias for "show"
|
|
779
|
+
dench backend show [--json]
|
|
780
|
+
dench backend set <production|staging|local|<url>> [--json]
|
|
781
|
+
dench backend clear [--json]
|
|
782
|
+
|
|
783
|
+
Aliases: prod = production, dev / localhost = local, stage = staging.
|
|
784
|
+
|
|
785
|
+
What this does:
|
|
786
|
+
The default Dench backend is production (${PRODUCTION_HOST}). "dench backend set"
|
|
787
|
+
stores an explicit override in ~/.dench/config.json so every future
|
|
788
|
+
"dench" invocation talks to that backend by default. Use it instead of
|
|
789
|
+
relying on a stale currentHost from a past "dench login".
|
|
790
|
+
|
|
791
|
+
Setting the backend to "local" also opts the CLI into auto-loading
|
|
792
|
+
.env and .env.local from the dench.com workspace at startup, which is
|
|
793
|
+
needed for GATEWAY_URL and similar local-only env vars.
|
|
794
|
+
|
|
795
|
+
One-shot overrides (no config change):
|
|
796
|
+
dench --prod <cmd> # one command against production
|
|
797
|
+
dench --staging <cmd> # one command against staging
|
|
798
|
+
dench --backend local <cmd> # one command against http://localhost:3000
|
|
799
|
+
dench --host https://… <cmd> # one command against a custom URL
|
|
800
|
+
DENCH_HOST=https://… dench … # same, via env
|
|
801
|
+
|
|
802
|
+
Opt into dev .env auto-loading without changing the default backend:
|
|
803
|
+
--use-dev-env # one-shot flag
|
|
804
|
+
DENCH_USE_DEV_ENV=1 # env var (e.g. via direnv)
|
|
805
|
+
`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function resolveHost(config: ConfigFile | undefined, _scope: SessionScope) {
|
|
809
|
+
// `defaultBackend` is only ever set via `dench backend set`, so it
|
|
810
|
+
// represents an explicit user choice. Production wins on fresh
|
|
811
|
+
// installs (no flags, no env, no config) — which is the safe default
|
|
812
|
+
// for an agent CLI. The per-scope `currentHosts[scope.key]` map is
|
|
813
|
+
// intentionally NOT consulted here so that a past `dench login
|
|
814
|
+
// --host http://localhost:3000` doesn't silently keep routing every
|
|
815
|
+
// future command at local dev.
|
|
816
|
+
return resolveHostFromArgs(filteredArgs, config?.defaultBackend);
|
|
733
817
|
}
|
|
734
818
|
|
|
735
819
|
async function loadConfig(): Promise<ConfigFile> {
|
|
@@ -868,6 +952,194 @@ function formatSessionChoices(
|
|
|
868
952
|
return lines.length > 0 ? lines.join("\n") : " No Dench sessions found.";
|
|
869
953
|
}
|
|
870
954
|
|
|
955
|
+
function backendLabelForHost(host: string) {
|
|
956
|
+
const normalized = normalizeHost(host);
|
|
957
|
+
if (normalized === PRODUCTION_HOST) return "production";
|
|
958
|
+
if (normalized === STAGING_HOST) return "staging";
|
|
959
|
+
if (isLocalHost(normalized)) return "local";
|
|
960
|
+
return "custom";
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function backendSnapshot(config: ConfigFile) {
|
|
964
|
+
const resolved = resolveHost(config, {
|
|
965
|
+
key: "auto:backend-show",
|
|
966
|
+
label: "cwd",
|
|
967
|
+
});
|
|
968
|
+
const stored = config.defaultBackend?.trim()
|
|
969
|
+
? normalizeHost(config.defaultBackend.trim())
|
|
970
|
+
: null;
|
|
971
|
+
const envHost = process.env.DENCH_HOST?.trim() || null;
|
|
972
|
+
const argBackend =
|
|
973
|
+
option("--backend") ?? (hasFlag("--prod")
|
|
974
|
+
? "production"
|
|
975
|
+
: hasFlag("--staging")
|
|
976
|
+
? "staging"
|
|
977
|
+
: option("--host") ?? null);
|
|
978
|
+
|
|
979
|
+
let source: "flag" | "env" | "config" | "default";
|
|
980
|
+
if (argBackend) source = "flag";
|
|
981
|
+
else if (envHost) source = "env";
|
|
982
|
+
else if (stored) source = "config";
|
|
983
|
+
else source = "default";
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
host: resolved,
|
|
987
|
+
label: backendLabelForHost(resolved),
|
|
988
|
+
source,
|
|
989
|
+
defaultBackend: stored,
|
|
990
|
+
envHost,
|
|
991
|
+
knownBackends: {
|
|
992
|
+
production: PRODUCTION_HOST,
|
|
993
|
+
staging: STAGING_HOST,
|
|
994
|
+
local: LOCAL_HOST,
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function formatBackendShow(snapshot: ReturnType<typeof backendSnapshot>) {
|
|
1000
|
+
const sourceHint =
|
|
1001
|
+
snapshot.source === "flag"
|
|
1002
|
+
? "(set via CLI flag for this invocation)"
|
|
1003
|
+
: snapshot.source === "env"
|
|
1004
|
+
? "(set via DENCH_HOST env var)"
|
|
1005
|
+
: snapshot.source === "config"
|
|
1006
|
+
? "(set via `dench backend set`)"
|
|
1007
|
+
: "(default — no explicit choice in env or config)";
|
|
1008
|
+
const lines = [
|
|
1009
|
+
`Active backend: ${snapshot.host} [${snapshot.label}] ${sourceHint}`,
|
|
1010
|
+
];
|
|
1011
|
+
if (snapshot.defaultBackend && snapshot.source !== "config") {
|
|
1012
|
+
lines.push(`Stored default backend: ${snapshot.defaultBackend}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (snapshot.envHost) {
|
|
1015
|
+
lines.push(`DENCH_HOST env: ${snapshot.envHost}`);
|
|
1016
|
+
}
|
|
1017
|
+
lines.push(
|
|
1018
|
+
"",
|
|
1019
|
+
"Manage default backend:",
|
|
1020
|
+
" dench backend set production # default fallback",
|
|
1021
|
+
" dench backend set staging",
|
|
1022
|
+
" dench backend set local # http://localhost:3000",
|
|
1023
|
+
" dench backend set https://example.com # custom URL",
|
|
1024
|
+
" dench backend clear # revert to production default",
|
|
1025
|
+
"",
|
|
1026
|
+
"One-shot override flags also work on every command:",
|
|
1027
|
+
" dench --prod <cmd> dench --staging <cmd>",
|
|
1028
|
+
" dench --backend local <cmd> dench --host https://example.com <cmd>",
|
|
1029
|
+
);
|
|
1030
|
+
return lines.join("\n");
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function backendShow() {
|
|
1034
|
+
const config = await loadConfig();
|
|
1035
|
+
const snapshot = backendSnapshot(config);
|
|
1036
|
+
print(json ? snapshot : formatBackendShow(snapshot));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function backendSet() {
|
|
1040
|
+
const target = positional(2);
|
|
1041
|
+
if (!target) {
|
|
1042
|
+
throw new CliError(
|
|
1043
|
+
"Missing backend value. Pass local, staging, prod, or a URL.",
|
|
1044
|
+
{
|
|
1045
|
+
code: "backend_set_missing_value",
|
|
1046
|
+
nextActions: [
|
|
1047
|
+
"Run dench backend set production.",
|
|
1048
|
+
"Run dench backend set local.",
|
|
1049
|
+
"Run dench backend set https://your-host.example.com.",
|
|
1050
|
+
],
|
|
1051
|
+
},
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
let resolved: string;
|
|
1055
|
+
try {
|
|
1056
|
+
resolved = resolveBackendAlias(target);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1059
|
+
throw new CliError(message, {
|
|
1060
|
+
code: "backend_set_invalid_value",
|
|
1061
|
+
nextActions: [
|
|
1062
|
+
"Run dench backend set production / staging / local.",
|
|
1063
|
+
"Or pass an explicit URL: dench backend set https://your-host.example.com.",
|
|
1064
|
+
],
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const config = await loadConfig();
|
|
1068
|
+
await saveConfig({ ...config, defaultBackend: resolved });
|
|
1069
|
+
const label = backendLabelForHost(resolved);
|
|
1070
|
+
if (json) {
|
|
1071
|
+
print({
|
|
1072
|
+
ok: true,
|
|
1073
|
+
defaultBackend: resolved,
|
|
1074
|
+
label,
|
|
1075
|
+
message: `Default Dench backend set to ${resolved} (${label}).`,
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
print(`Default Dench backend set to ${resolved} (${label}).`);
|
|
1080
|
+
if (label === "local") {
|
|
1081
|
+
print(
|
|
1082
|
+
"Local backend selected. dench will now also load .env / .env.local from the workspace so GATEWAY_URL etc. are picked up.",
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
print("Future `dench` commands without --prod/--staging/--host will target this backend.");
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function backendClear() {
|
|
1089
|
+
const config = await loadConfig();
|
|
1090
|
+
if (!config.defaultBackend) {
|
|
1091
|
+
print(
|
|
1092
|
+
json
|
|
1093
|
+
? { ok: true, cleared: false, defaultBackend: null }
|
|
1094
|
+
: "No default backend is set. dench already falls back to production.",
|
|
1095
|
+
);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const previous = config.defaultBackend;
|
|
1099
|
+
const next: ConfigFile = { ...config };
|
|
1100
|
+
delete next.defaultBackend;
|
|
1101
|
+
await saveConfig(next);
|
|
1102
|
+
print(
|
|
1103
|
+
json
|
|
1104
|
+
? {
|
|
1105
|
+
ok: true,
|
|
1106
|
+
cleared: true,
|
|
1107
|
+
previous,
|
|
1108
|
+
defaultBackend: null,
|
|
1109
|
+
fallback: PRODUCTION_HOST,
|
|
1110
|
+
}
|
|
1111
|
+
: `Cleared default backend (was ${previous}). dench will fall back to ${PRODUCTION_HOST}.`,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function runBackendCommand() {
|
|
1116
|
+
const sub = positional(1);
|
|
1117
|
+
if (!sub || sub === "show" || sub === "status") {
|
|
1118
|
+
await backendShow();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (sub === "set") {
|
|
1122
|
+
await backendSet();
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (sub === "clear" || sub === "reset" || sub === "unset") {
|
|
1126
|
+
await backendClear();
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
1130
|
+
backendHelp();
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
throw new CliError(`Unknown dench backend subcommand: ${sub}`, {
|
|
1134
|
+
code: "unknown_backend_subcommand",
|
|
1135
|
+
nextActions: [
|
|
1136
|
+
"Run dench backend show to print the active backend.",
|
|
1137
|
+
"Run dench backend set <local|staging|prod|<url>>.",
|
|
1138
|
+
"Run dench backend clear to revert to production.",
|
|
1139
|
+
],
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
871
1143
|
async function listSessions() {
|
|
872
1144
|
const config = await loadConfig();
|
|
873
1145
|
const scope = resolveSessionScope({ args: filteredArgs });
|
|
@@ -1487,12 +1759,37 @@ function hasDevEnv() {
|
|
|
1487
1759
|
* dev typically only has `NEXT_PUBLIC_CONVEX_URL` exported. We accept
|
|
1488
1760
|
* either so the same code path works in both shells without callers
|
|
1489
1761
|
* needing to know which env var is set.
|
|
1762
|
+
*
|
|
1763
|
+
* When neither env var is set we fall back to the baked-in production
|
|
1764
|
+
* Convex URL (`PRODUCTION_CONVEX_URL`). This is a deliberate
|
|
1765
|
+
* defense-in-depth move: if a sandbox is provisioned while a Vercel
|
|
1766
|
+
* deployment is half-configured and `CONVEX_URL` lands as empty
|
|
1767
|
+
* string, the CLI inside it still ends up pointed at the production
|
|
1768
|
+
* deployment instead of crashing on a "missing Convex URL" error.
|
|
1769
|
+
* Dev / staging shells override this by exporting `CONVEX_URL` or
|
|
1770
|
+
* `NEXT_PUBLIC_CONVEX_URL` explicitly.
|
|
1490
1771
|
*/
|
|
1491
1772
|
function resolveApiKeyConvexUrl(): string | undefined {
|
|
1492
1773
|
return (
|
|
1493
1774
|
process.env.CONVEX_URL?.trim() ||
|
|
1494
1775
|
process.env.NEXT_PUBLIC_CONVEX_URL?.trim() ||
|
|
1495
|
-
|
|
1776
|
+
PRODUCTION_CONVEX_URL
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Resolve the Next.js API host the CLI should hit for HTTP calls (e.g.
|
|
1782
|
+
* Stripe billing topup, the gateway proxy). Same defense-in-depth
|
|
1783
|
+
* pattern as `resolveApiKeyConvexUrl`: env wins, then the resolved
|
|
1784
|
+
* Dench host the runtime is targeting, and finally
|
|
1785
|
+
* `PRODUCTION_API_URL` as the last resort.
|
|
1786
|
+
*/
|
|
1787
|
+
function resolveApiHost(fallbackHost: string): string {
|
|
1788
|
+
return (
|
|
1789
|
+
process.env.DENCH_API_URL?.trim() ||
|
|
1790
|
+
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1791
|
+
fallbackHost ||
|
|
1792
|
+
PRODUCTION_API_URL
|
|
1496
1793
|
);
|
|
1497
1794
|
}
|
|
1498
1795
|
|
|
@@ -1556,10 +1853,7 @@ async function getRuntime() {
|
|
|
1556
1853
|
if (sandboxAgentToken && sandboxConvexUrl) {
|
|
1557
1854
|
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1558
1855
|
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1559
|
-
const apiHost =
|
|
1560
|
-
process.env.DENCH_API_URL?.trim() ||
|
|
1561
|
-
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1562
|
-
host;
|
|
1856
|
+
const apiHost = resolveApiHost(host);
|
|
1563
1857
|
return {
|
|
1564
1858
|
mode: "session" as const,
|
|
1565
1859
|
host: apiHost,
|
|
@@ -1583,10 +1877,7 @@ async function getRuntime() {
|
|
|
1583
1877
|
if (apiKey && convexUrl) {
|
|
1584
1878
|
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1585
1879
|
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1586
|
-
const apiHost =
|
|
1587
|
-
process.env.DENCH_API_URL?.trim() ||
|
|
1588
|
-
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1589
|
-
host;
|
|
1880
|
+
const apiHost = resolveApiHost(host);
|
|
1590
1881
|
return {
|
|
1591
1882
|
mode: "apiKey" as const,
|
|
1592
1883
|
host: apiHost,
|
|
@@ -1619,8 +1910,13 @@ async function getRuntime() {
|
|
|
1619
1910
|
type Runtime = Awaited<ReturnType<typeof getRuntime>>;
|
|
1620
1911
|
|
|
1621
1912
|
function stripRuntimeFlags(args: string[]) {
|
|
1622
|
-
const booleanFlags = new Set([
|
|
1623
|
-
|
|
1913
|
+
const booleanFlags = new Set([
|
|
1914
|
+
"--dev",
|
|
1915
|
+
"--prod",
|
|
1916
|
+
"--staging",
|
|
1917
|
+
"--use-dev-env",
|
|
1918
|
+
]);
|
|
1919
|
+
const valueFlags = new Set(["--convex-url", "--host", "--backend"]);
|
|
1624
1920
|
const stripped: string[] = [];
|
|
1625
1921
|
for (let i = 0; i < args.length; i++) {
|
|
1626
1922
|
const arg = args[i];
|
|
@@ -2937,6 +3233,14 @@ async function main() {
|
|
|
2937
3233
|
return;
|
|
2938
3234
|
}
|
|
2939
3235
|
|
|
3236
|
+
if (
|
|
3237
|
+
command === "backend" &&
|
|
3238
|
+
(subcommand === "help" || hasFlag("--help"))
|
|
3239
|
+
) {
|
|
3240
|
+
backendHelp();
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
2940
3244
|
if (command === "crm") {
|
|
2941
3245
|
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("crm") + 1));
|
|
2942
3246
|
const sub = subArgs[0];
|
|
@@ -2975,6 +3279,46 @@ async function main() {
|
|
|
2975
3279
|
return;
|
|
2976
3280
|
}
|
|
2977
3281
|
|
|
3282
|
+
if (command === "members") {
|
|
3283
|
+
// Top-level convenience surface for the same Convex query that
|
|
3284
|
+
// `dench crm members list` calls. Lives here (rather than only
|
|
3285
|
+
// under `dench crm`) so it shows up in top-level help and tab
|
|
3286
|
+
// completion — members aren't strictly a CRM concept, even though
|
|
3287
|
+
// the canonical caller today is the AI agent looking up userIds
|
|
3288
|
+
// for `user`-typed CRM cells.
|
|
3289
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("members") + 1));
|
|
3290
|
+
const sub = subArgs[0];
|
|
3291
|
+
if (!sub || sub === "help" || sub === "--help") {
|
|
3292
|
+
const { runMembersCommand } = await import("./members");
|
|
3293
|
+
// biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
|
|
3294
|
+
await runMembersCommand({ convex: {} as any, args: subArgs });
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
const { runMembersCommand } = await import("./members");
|
|
3298
|
+
const runtime = await getRuntime();
|
|
3299
|
+
// Member listing is org-scoped via `requireCrmAccess`, identical
|
|
3300
|
+
// auth shape to `dench crm`. Dev workspaces forward the encoded
|
|
3301
|
+
// dev token so local --dev runs work the same way.
|
|
3302
|
+
if (runtime.mode === "dev") {
|
|
3303
|
+
await runMembersCommand({
|
|
3304
|
+
convex: runtime.client,
|
|
3305
|
+
args: subArgs,
|
|
3306
|
+
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
3307
|
+
});
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
3311
|
+
runtime,
|
|
3312
|
+
"dench members",
|
|
3313
|
+
);
|
|
3314
|
+
await runMembersCommand({
|
|
3315
|
+
convex: authedRuntime.client,
|
|
3316
|
+
args: subArgs,
|
|
3317
|
+
sessionToken: authedRuntime.sessionToken,
|
|
3318
|
+
});
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
2978
3322
|
if (command === "fs") {
|
|
2979
3323
|
await runFsCommand();
|
|
2980
3324
|
return;
|
|
@@ -3267,6 +3611,11 @@ async function main() {
|
|
|
3267
3611
|
return;
|
|
3268
3612
|
}
|
|
3269
3613
|
|
|
3614
|
+
if (command === "backend") {
|
|
3615
|
+
await runBackendCommand();
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3270
3619
|
const runtime = await getRuntime();
|
|
3271
3620
|
|
|
3272
3621
|
if (command === "billing") {
|
package/host.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
export const PRODUCTION_HOST = "https://dench.dev";
|
|
2
2
|
export const STAGING_HOST = "https://workspace-staging.dench.com";
|
|
3
|
+
export const LOCAL_HOST = "http://localhost:3000";
|
|
3
4
|
export const DEFAULT_HOST = PRODUCTION_HOST;
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Production-only fallbacks baked into the CLI binary so it does the
|
|
8
|
+
* right thing even when run with no config and no env vars (e.g. in a
|
|
9
|
+
* sandbox whose `buildSandboxEnvVars` map was assembled while the
|
|
10
|
+
* Vercel deployment was missing one of these keys).
|
|
11
|
+
*
|
|
12
|
+
* If any of these is wrong, EVERY install of @dench.com/cli is broken
|
|
13
|
+
* until republished — keep them in sync with whichever Convex
|
|
14
|
+
* deployment + Next.js host backs `https://www.dench.com`.
|
|
15
|
+
*/
|
|
16
|
+
export const PRODUCTION_API_URL = "https://www.dench.com";
|
|
17
|
+
export const PRODUCTION_CONVEX_URL = "https://beaming-alligator-67.convex.cloud";
|
|
18
|
+
export const PRODUCTION_GATEWAY_URL = "https://gateway.merseoriginals.com";
|
|
19
|
+
|
|
20
|
+
export type BackendAlias = "production" | "prod" | "staging" | "local";
|
|
21
|
+
|
|
5
22
|
function optionFromArgs(args: string[], name: string) {
|
|
6
23
|
const index = args.indexOf(name);
|
|
7
24
|
if (index === -1) return undefined;
|
|
@@ -13,7 +30,13 @@ function hasFlag(args: string[], name: string) {
|
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
export function normalizeHost(input: string) {
|
|
16
|
-
const
|
|
33
|
+
const trimmed = input.trim();
|
|
34
|
+
if (/^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::|$)/.test(trimmed)) {
|
|
35
|
+
return new URL(`http://${trimmed}`).origin;
|
|
36
|
+
}
|
|
37
|
+
const withProtocol = /^https?:\/\//.test(trimmed)
|
|
38
|
+
? trimmed
|
|
39
|
+
: `https://${trimmed}`;
|
|
17
40
|
const url = new URL(withProtocol);
|
|
18
41
|
return url.origin;
|
|
19
42
|
}
|
|
@@ -29,6 +52,39 @@ export function isLocalHost(input: string) {
|
|
|
29
52
|
);
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Resolve `local|staging|prod|production|<url>` -> a normalized host
|
|
57
|
+
* origin. Aliases let `dench backend set <alias>` (and `--backend
|
|
58
|
+
* <alias>`) accept the short forms most agents reach for first.
|
|
59
|
+
* Returns `undefined` for empty input and throws on values that
|
|
60
|
+
* neither match an alias nor parse as a URL.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveBackendAlias(input: string): string {
|
|
63
|
+
const value = input.trim();
|
|
64
|
+
if (!value) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Backend value is empty. Pass local, staging, prod, or a URL like https://dench.dev.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const normalized = value.toLowerCase();
|
|
70
|
+
if (normalized === "prod" || normalized === "production") {
|
|
71
|
+
return PRODUCTION_HOST;
|
|
72
|
+
}
|
|
73
|
+
if (normalized === "staging" || normalized === "stage") {
|
|
74
|
+
return STAGING_HOST;
|
|
75
|
+
}
|
|
76
|
+
if (normalized === "local" || normalized === "localhost" || normalized === "dev") {
|
|
77
|
+
return LOCAL_HOST;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return normalizeHost(value);
|
|
81
|
+
} catch (_error) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Unrecognized backend "${value}". Use local, staging, prod, or a URL like https://dench.dev.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
32
88
|
export function resolveHostFromArgs(
|
|
33
89
|
args: string[],
|
|
34
90
|
configHost?: string,
|
|
@@ -37,6 +93,9 @@ export function resolveHostFromArgs(
|
|
|
37
93
|
if (hasFlag(args, "--prod")) return PRODUCTION_HOST;
|
|
38
94
|
if (hasFlag(args, "--staging")) return STAGING_HOST;
|
|
39
95
|
|
|
96
|
+
const explicitBackend = optionFromArgs(args, "--backend");
|
|
97
|
+
if (explicitBackend) return resolveBackendAlias(explicitBackend);
|
|
98
|
+
|
|
40
99
|
const explicit = optionFromArgs(args, "--host") ?? envHost;
|
|
41
100
|
if (explicit) return normalizeHost(explicit);
|
|
42
101
|
if (configHost) return normalizeHost(configHost);
|
|
@@ -5,10 +5,13 @@ export type FetchLike = (
|
|
|
5
5
|
init?: RequestInit,
|
|
6
6
|
) => Promise<Response>;
|
|
7
7
|
|
|
8
|
-
export type EnrichmentGatewayEnv =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
export type EnrichmentGatewayEnv = {
|
|
9
|
+
DENCH_API_KEY?: string;
|
|
10
|
+
DENCH_GATEWAY_URL?: string;
|
|
11
|
+
GATEWAY_URL?: string;
|
|
12
|
+
DENCH_HOST?: string;
|
|
13
|
+
[key: string]: string | undefined;
|
|
14
|
+
};
|
|
12
15
|
|
|
13
16
|
export class EnrichmentGatewayError extends Error {
|
|
14
17
|
readonly status?: number;
|
package/lib/load-dev-env.mjs
CHANGED
|
@@ -1,27 +1,100 @@
|
|
|
1
|
-
//
|
|
2
|
-
// dench.com source tree
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// (`https://gateway.merseoriginals.com`), because `cli/tools.ts`
|
|
7
|
-
// resolves the gateway URL purely from `process.env`. A dev-issued
|
|
8
|
-
// `DENCH_API_KEY` then 401'd against prod with `invalid_api_key`.
|
|
1
|
+
// `./dench` and `./dench.mjs` ship as Node entry points. When Node runs
|
|
2
|
+
// them from inside the dench.com source tree it does NOT auto-load
|
|
3
|
+
// workspace `.env` files the way `bun run` does, so we have a one-shot
|
|
4
|
+
// chance here to inject env vars (e.g. `GATEWAY_URL`,
|
|
5
|
+
// `NEXT_PUBLIC_CONVEX_URL`) before `dench.ts` is imported.
|
|
9
6
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// that
|
|
14
|
-
//
|
|
15
|
-
// `
|
|
7
|
+
// HISTORY: this helper used to ALWAYS load `.env` and `.env.local` when
|
|
8
|
+
// the parent package was `ironclaw-cloud`. That fixed gateway-backed
|
|
9
|
+
// subcommands (`dench apps`, `dench tool …`, the enrichment helpers)
|
|
10
|
+
// that would otherwise silently 401 against production gateway with a
|
|
11
|
+
// dev-issued `DENCH_API_KEY`. The side effect was severe: the auto-load
|
|
12
|
+
// also injected `NEXT_PUBLIC_CONVEX_URL=…dev convex…` and friends, so
|
|
13
|
+
// every Convex call from the CLI silently pointed at the dev
|
|
14
|
+
// deployment, even when the user expected production.
|
|
15
|
+
//
|
|
16
|
+
// FIX: dev-env loading is now OPT-IN. The default is "do nothing" so
|
|
17
|
+
// the CLI always defaults to production, no matter where it is invoked
|
|
18
|
+
// from. Users opt into local/dev env vars one of three ways:
|
|
19
|
+
//
|
|
20
|
+
// 1. `--dev` flag in argv (existing flag — also flips the runtime to
|
|
21
|
+
// dev workspace mode).
|
|
22
|
+
// 2. `--use-dev-env` flag in argv (load `.env` files without
|
|
23
|
+
// switching to dev workspace mode — useful when you want
|
|
24
|
+
// `GATEWAY_URL=http://0.0.0.0:8787` baked in but still talk to a
|
|
25
|
+
// stored session).
|
|
26
|
+
// 3. `DENCH_USE_DEV_ENV=1` env var (same effect as `--use-dev-env`,
|
|
27
|
+
// handy for `direnv` setups).
|
|
28
|
+
// 4. `defaultBackend: "http://localhost:…"` (or any local URL) in
|
|
29
|
+
// `~/.dench/config.json` — set via `dench backend set local`. If
|
|
30
|
+
// the user has explicitly pointed the CLI at localhost they almost
|
|
31
|
+
// always also want the dev gateway URL.
|
|
32
|
+
//
|
|
33
|
+
// Existing `process.env` values always win, so explicit shell exports
|
|
34
|
+
// (e.g. `GATEWAY_URL=… ./dench …`) keep working even with auto-load
|
|
35
|
+
// disabled.
|
|
16
36
|
|
|
17
37
|
import { existsSync, readFileSync } from "node:fs";
|
|
38
|
+
import { homedir } from "node:os";
|
|
18
39
|
import { dirname, join } from "node:path";
|
|
19
40
|
import { fileURLToPath } from "node:url";
|
|
20
41
|
|
|
21
42
|
const WORKSPACE_PACKAGE_NAME = "ironclaw-cloud";
|
|
22
43
|
const ENV_FILES = [".env", ".env.local"];
|
|
44
|
+
const CONFIG_PATH = join(homedir(), ".dench", "config.json");
|
|
45
|
+
|
|
46
|
+
const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
47
|
+
|
|
48
|
+
function envFlagEnabled(value) {
|
|
49
|
+
if (!value) return false;
|
|
50
|
+
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
51
|
+
}
|
|
23
52
|
|
|
24
|
-
|
|
53
|
+
function argvOptIn(argv) {
|
|
54
|
+
if (!Array.isArray(argv)) return false;
|
|
55
|
+
return argv.includes("--dev") || argv.includes("--use-dev-env");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function configOptIn() {
|
|
59
|
+
try {
|
|
60
|
+
if (!existsSync(CONFIG_PATH)) return false;
|
|
61
|
+
const raw = readFileSync(CONFIG_PATH, "utf8");
|
|
62
|
+
const config = JSON.parse(raw);
|
|
63
|
+
const backend = config?.defaultBackend;
|
|
64
|
+
if (typeof backend !== "string" || !backend.trim()) return false;
|
|
65
|
+
return isLocalUrl(backend.trim());
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isLocalUrl(value) {
|
|
72
|
+
try {
|
|
73
|
+
const url = new URL(/^https?:\/\//.test(value) ? value : `https://${value}`);
|
|
74
|
+
const host = url.hostname;
|
|
75
|
+
return (
|
|
76
|
+
host === "localhost" ||
|
|
77
|
+
host === "127.0.0.1" ||
|
|
78
|
+
host === "0.0.0.0" ||
|
|
79
|
+
host === "::1" ||
|
|
80
|
+
host.endsWith(".localhost")
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function shouldLoadDevEnv({
|
|
88
|
+
argv = process.argv.slice(2),
|
|
89
|
+
env = process.env,
|
|
90
|
+
} = {}) {
|
|
91
|
+
if (envFlagEnabled(env.DENCH_USE_DEV_ENV)) return "env";
|
|
92
|
+
if (argvOptIn(argv)) return "argv";
|
|
93
|
+
if (configOptIn()) return "config";
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function loadDevEnv(callerUrl, options = {}) {
|
|
25
98
|
try {
|
|
26
99
|
const here = dirname(fileURLToPath(callerUrl));
|
|
27
100
|
const repoRoot = dirname(here);
|
|
@@ -30,6 +103,8 @@ export function loadDevEnv(callerUrl) {
|
|
|
30
103
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
31
104
|
if (pkg?.name !== WORKSPACE_PACKAGE_NAME) return;
|
|
32
105
|
|
|
106
|
+
if (!shouldLoadDevEnv(options)) return;
|
|
107
|
+
|
|
33
108
|
for (const filename of ENV_FILES) {
|
|
34
109
|
const path = join(repoRoot, filename);
|
|
35
110
|
if (!existsSync(path)) continue;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { shouldLoadDevEnv } from "./load-dev-env.mjs";
|
|
3
|
+
|
|
4
|
+
describe("shouldLoadDevEnv", () => {
|
|
5
|
+
it("returns false on a fresh install with no opt-in", () => {
|
|
6
|
+
expect(shouldLoadDevEnv({ argv: [], env: {} })).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("opts in when --dev is in argv", () => {
|
|
10
|
+
expect(shouldLoadDevEnv({ argv: ["status", "--dev"], env: {} })).toBe(
|
|
11
|
+
"argv",
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("opts in when --use-dev-env is in argv", () => {
|
|
16
|
+
expect(
|
|
17
|
+
shouldLoadDevEnv({ argv: ["apps", "--use-dev-env"], env: {} }),
|
|
18
|
+
).toBe("argv");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("opts in when DENCH_USE_DEV_ENV is set to a truthy value", () => {
|
|
22
|
+
expect(shouldLoadDevEnv({ argv: [], env: { DENCH_USE_DEV_ENV: "1" } })).toBe(
|
|
23
|
+
"env",
|
|
24
|
+
);
|
|
25
|
+
expect(
|
|
26
|
+
shouldLoadDevEnv({ argv: [], env: { DENCH_USE_DEV_ENV: "true" } }),
|
|
27
|
+
).toBe("env");
|
|
28
|
+
expect(
|
|
29
|
+
shouldLoadDevEnv({ argv: [], env: { DENCH_USE_DEV_ENV: "yes" } }),
|
|
30
|
+
).toBe("env");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("ignores DENCH_USE_DEV_ENV when set to a falsy/empty value", () => {
|
|
34
|
+
expect(shouldLoadDevEnv({ argv: [], env: { DENCH_USE_DEV_ENV: "" } })).toBe(
|
|
35
|
+
false,
|
|
36
|
+
);
|
|
37
|
+
expect(shouldLoadDevEnv({ argv: [], env: { DENCH_USE_DEV_ENV: "0" } })).toBe(
|
|
38
|
+
false,
|
|
39
|
+
);
|
|
40
|
+
expect(
|
|
41
|
+
shouldLoadDevEnv({ argv: [], env: { DENCH_USE_DEV_ENV: "false" } }),
|
|
42
|
+
).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
package/package.json
CHANGED
package/tools.ts
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
* working.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
+
import {
|
|
40
|
+
PRODUCTION_API_URL,
|
|
41
|
+
PRODUCTION_GATEWAY_URL,
|
|
42
|
+
} from "./host";
|
|
39
43
|
import {
|
|
40
44
|
CliArgError,
|
|
41
45
|
getFlag,
|
|
@@ -380,7 +384,7 @@ function resolveGatewayBaseUrl(): string {
|
|
|
380
384
|
if (host && /localhost|127\.0\.0\.1/.test(host)) {
|
|
381
385
|
return "http://localhost:8787";
|
|
382
386
|
}
|
|
383
|
-
return
|
|
387
|
+
return PRODUCTION_GATEWAY_URL;
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
/**
|
|
@@ -403,7 +407,7 @@ function resolveAppPublicOrigin(): string {
|
|
|
403
407
|
const trimmed = candidate?.trim();
|
|
404
408
|
if (trimmed) return trimmed.replace(/\/+$/, "");
|
|
405
409
|
}
|
|
406
|
-
return
|
|
410
|
+
return PRODUCTION_API_URL;
|
|
407
411
|
}
|
|
408
412
|
|
|
409
413
|
function requireApiKey(): string {
|