@dench.com/cli 0.4.5 → 0.4.7
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/agent.ts +17 -453
- package/crm.ts +4 -51
- package/cron.ts +4 -9
- package/dench.ts +324 -295
- package/fs-daemon.ts +26 -18
- 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/fs-daemon.ts
CHANGED
|
@@ -68,6 +68,12 @@ import {
|
|
|
68
68
|
readMountInfo,
|
|
69
69
|
} from "./fs-daemon-mount";
|
|
70
70
|
|
|
71
|
+
type FileTreeSyncRow = {
|
|
72
|
+
path: string;
|
|
73
|
+
contentHash?: string;
|
|
74
|
+
isDir: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
71
77
|
type Args = {
|
|
72
78
|
orgId?: string;
|
|
73
79
|
workspace: string;
|
|
@@ -302,6 +308,9 @@ const api = {
|
|
|
302
308
|
),
|
|
303
309
|
deleteFile: makeFunctionReference<"mutation">("functions/files:deleteFile"),
|
|
304
310
|
listTree: makeFunctionReference<"query">("functions/files:listTree"),
|
|
311
|
+
listTreeSnapshot: makeFunctionReference<"query">(
|
|
312
|
+
"functions/files:listTreeSnapshot",
|
|
313
|
+
),
|
|
305
314
|
resolveApiKeyOrganization: makeFunctionReference<"query">(
|
|
306
315
|
"functions/files:resolveApiKeyOrganization",
|
|
307
316
|
),
|
|
@@ -404,13 +413,11 @@ class DenchFileClient {
|
|
|
404
413
|
* fs status` to recursively crawl the canonical tree and diff
|
|
405
414
|
* against the local FS snapshot.
|
|
406
415
|
*/
|
|
407
|
-
async listTree(
|
|
408
|
-
prefix: string,
|
|
409
|
-
): Promise<Array<{ path: string; contentHash?: string; isDir: boolean }>> {
|
|
416
|
+
async listTree(prefix: string): Promise<FileTreeSyncRow[]> {
|
|
410
417
|
const rows = (await this.http.query(api.files.listTree, {
|
|
411
418
|
prefix,
|
|
412
419
|
apiKey: this.apiKey,
|
|
413
|
-
})) as
|
|
420
|
+
})) as FileTreeSyncRow[];
|
|
414
421
|
return Array.isArray(rows) ? rows : [];
|
|
415
422
|
}
|
|
416
423
|
|
|
@@ -468,25 +475,26 @@ class DenchFileClient {
|
|
|
468
475
|
*/
|
|
469
476
|
subscribeFileTree(
|
|
470
477
|
convexUrl: string,
|
|
471
|
-
onChange: (
|
|
472
|
-
rows: Array<{ path: string; contentHash?: string; isDir: boolean }>,
|
|
473
|
-
) => void,
|
|
478
|
+
onChange: (rows: FileTreeSyncRow[]) => void,
|
|
474
479
|
): () => void {
|
|
475
480
|
if (!this.realtime) {
|
|
476
481
|
this.realtime = new ConvexClient(convexUrl);
|
|
477
482
|
}
|
|
478
483
|
const unsubscribe = this.realtime.onUpdate(
|
|
479
|
-
api.files.
|
|
480
|
-
{
|
|
481
|
-
(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
484
|
+
api.files.listTreeSnapshot,
|
|
485
|
+
{ apiKey: this.apiKey },
|
|
486
|
+
(snapshot: unknown) => {
|
|
487
|
+
// Remote writes may land deep in the tree (for example seeded
|
|
488
|
+
// `/skills/<slug>/SKILL.md` files). A root-only `listTree`
|
|
489
|
+
// subscription only sees the `/skills` directory row, so it
|
|
490
|
+
// never pulls the actual file bytes down to the volume.
|
|
491
|
+
const rows =
|
|
492
|
+
snapshot &&
|
|
493
|
+
typeof snapshot === "object" &&
|
|
494
|
+
Array.isArray((snapshot as { rows?: unknown }).rows)
|
|
495
|
+
? (snapshot as { rows: FileTreeSyncRow[] }).rows
|
|
496
|
+
: [];
|
|
497
|
+
onChange(rows);
|
|
490
498
|
},
|
|
491
499
|
);
|
|
492
500
|
return unsubscribe;
|
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 {
|