@dench.com/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/agentKind.ts +58 -0
- package/crm.ts +1402 -0
- package/dench +10 -0
- package/dench.mjs +10 -0
- package/dench.ts +3305 -0
- package/fs-daemon +10 -0
- package/fs-daemon.ts +682 -0
- package/host.ts +44 -0
- package/lib/cli-args.ts +52 -0
- package/openUrl.ts +120 -0
- package/package.json +35 -0
- package/session.ts +504 -0
package/host.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const PRODUCTION_HOST = "https://dench.dev";
|
|
2
|
+
export const STAGING_HOST = "https://workspace-staging.dench.com";
|
|
3
|
+
export const DEFAULT_HOST = PRODUCTION_HOST;
|
|
4
|
+
|
|
5
|
+
function optionFromArgs(args: string[], name: string) {
|
|
6
|
+
const index = args.indexOf(name);
|
|
7
|
+
if (index === -1) return undefined;
|
|
8
|
+
return args[index + 1];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hasFlag(args: string[], name: string) {
|
|
12
|
+
return args.includes(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeHost(input: string) {
|
|
16
|
+
const withProtocol = /^https?:\/\//.test(input) ? input : `https://${input}`;
|
|
17
|
+
const url = new URL(withProtocol);
|
|
18
|
+
return url.origin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isLocalHost(input: string) {
|
|
22
|
+
const hostname = new URL(normalizeHost(input)).hostname;
|
|
23
|
+
return (
|
|
24
|
+
hostname === "localhost" ||
|
|
25
|
+
hostname === "127.0.0.1" ||
|
|
26
|
+
hostname === "0.0.0.0" ||
|
|
27
|
+
hostname === "::1" ||
|
|
28
|
+
hostname.endsWith(".localhost")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveHostFromArgs(
|
|
33
|
+
args: string[],
|
|
34
|
+
configHost?: string,
|
|
35
|
+
envHost = process.env.DENCH_HOST,
|
|
36
|
+
) {
|
|
37
|
+
if (hasFlag(args, "--prod")) return PRODUCTION_HOST;
|
|
38
|
+
if (hasFlag(args, "--staging")) return STAGING_HOST;
|
|
39
|
+
|
|
40
|
+
const explicit = optionFromArgs(args, "--host") ?? envHost;
|
|
41
|
+
if (explicit) return normalizeHost(explicit);
|
|
42
|
+
if (configHost) return normalizeHost(configHost);
|
|
43
|
+
return DEFAULT_HOST;
|
|
44
|
+
}
|
package/lib/cli-args.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI argument-parsing helpers used by `dench crm` + `dench agent`.
|
|
3
|
+
*
|
|
4
|
+
* Both CLI surfaces hand-parse argv slices (we deliberately avoid pulling
|
|
5
|
+
* in commander/yargs to keep the cli bundle small). These helpers are the
|
|
6
|
+
* tiny abstraction across both:
|
|
7
|
+
* - shift(args, expected): pop the next positional arg or throw.
|
|
8
|
+
* - getFlag(args, name): pull the value following `--name` and remove
|
|
9
|
+
* both tokens from args. Returns undefined if the flag is missing.
|
|
10
|
+
* - hasFlag(args, name): true iff `--name` appears (and removes it).
|
|
11
|
+
* - parseJson(value): JSON.parse with a CliError-style throw on
|
|
12
|
+
* failure.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: each module wraps its own *CliError class so we don't import
|
|
15
|
+
* across cli/ entry points; this module's errors throw plain Error and
|
|
16
|
+
* the caller adapts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class CliArgError extends Error {}
|
|
20
|
+
|
|
21
|
+
export function shift(args: string[], expected: string): string {
|
|
22
|
+
const value = args.shift();
|
|
23
|
+
if (!value) throw new CliArgError(`Missing ${expected}`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getFlag(
|
|
28
|
+
args: string[],
|
|
29
|
+
name: string,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
const idx = args.indexOf(name);
|
|
32
|
+
if (idx === -1) return undefined;
|
|
33
|
+
const value = args[idx + 1];
|
|
34
|
+
args.splice(idx, 2);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasFlag(args: string[], name: string): boolean {
|
|
39
|
+
const idx = args.indexOf(name);
|
|
40
|
+
if (idx === -1) return false;
|
|
41
|
+
args.splice(idx, 1);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseJson(value: string | undefined): unknown {
|
|
46
|
+
if (!value) return undefined;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(value);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new CliArgError(`Invalid JSON: ${value}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/openUrl.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { spawn as nodeSpawn, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { platform as nodePlatform } from "node:os";
|
|
3
|
+
|
|
4
|
+
type SpawnedProcess = {
|
|
5
|
+
once(
|
|
6
|
+
event: "spawn" | "error",
|
|
7
|
+
listener: (error?: Error) => void,
|
|
8
|
+
): SpawnedProcess;
|
|
9
|
+
unref(): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SpawnLike = (
|
|
13
|
+
command: string,
|
|
14
|
+
args: string[],
|
|
15
|
+
options: SpawnOptions,
|
|
16
|
+
) => SpawnedProcess;
|
|
17
|
+
|
|
18
|
+
export type OpenUrlResult =
|
|
19
|
+
| { status: "opened"; command: string; args: string[] }
|
|
20
|
+
| { status: "skipped"; reason: string }
|
|
21
|
+
| { status: "failed"; command: string; args: string[]; error: string };
|
|
22
|
+
|
|
23
|
+
type OpenUrlOptions = {
|
|
24
|
+
noOpen?: boolean;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
env?: Record<string, string | undefined>;
|
|
27
|
+
platform?: NodeJS.Platform;
|
|
28
|
+
spawn?: SpawnLike;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function truthyEnv(value: string | undefined) {
|
|
32
|
+
if (!value) return false;
|
|
33
|
+
return !["0", "false", "no"].includes(value.trim().toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function disabledReason(options: OpenUrlOptions) {
|
|
37
|
+
if (options.noOpen) return "--no-open";
|
|
38
|
+
if (options.json) return "--json";
|
|
39
|
+
if (truthyEnv(options.env?.DENCH_NO_OPEN)) return "DENCH_NO_OPEN";
|
|
40
|
+
if (truthyEnv(options.env?.CI)) return "CI";
|
|
41
|
+
if (
|
|
42
|
+
["none", "false"].includes(options.env?.BROWSER?.trim().toLowerCase() ?? "")
|
|
43
|
+
) {
|
|
44
|
+
return "BROWSER";
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function openerCommand(url: string, currentPlatform: NodeJS.Platform) {
|
|
50
|
+
if (currentPlatform === "darwin") {
|
|
51
|
+
return { command: "open", args: [url] };
|
|
52
|
+
}
|
|
53
|
+
if (currentPlatform === "win32") {
|
|
54
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
55
|
+
}
|
|
56
|
+
return { command: "xdg-open", args: [url] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function openUrl(
|
|
60
|
+
url: string,
|
|
61
|
+
options: OpenUrlOptions = {},
|
|
62
|
+
): Promise<OpenUrlResult> {
|
|
63
|
+
const reason = disabledReason(options);
|
|
64
|
+
if (reason) return { status: "skipped", reason };
|
|
65
|
+
|
|
66
|
+
const { command, args } = openerCommand(
|
|
67
|
+
url,
|
|
68
|
+
options.platform ?? nodePlatform(),
|
|
69
|
+
);
|
|
70
|
+
const spawn = options.spawn ?? (nodeSpawn as SpawnLike);
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
let settled = false;
|
|
74
|
+
const settle = (result: OpenUrlResult) => {
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true;
|
|
77
|
+
resolve(result);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const child = spawn(command, args, {
|
|
83
|
+
detached: true,
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
});
|
|
86
|
+
child.once("error", (error) =>
|
|
87
|
+
settle({
|
|
88
|
+
status: "failed",
|
|
89
|
+
command,
|
|
90
|
+
args,
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
child.once("spawn", () => {
|
|
95
|
+
child.unref();
|
|
96
|
+
settle({ status: "opened", command, args });
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
settle({
|
|
100
|
+
status: "failed",
|
|
101
|
+
command,
|
|
102
|
+
args,
|
|
103
|
+
error: error instanceof Error ? error.message : String(error),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function formatApprovalOpenMessage(url: string, result: OpenUrlResult) {
|
|
110
|
+
if (result.status === "opened") {
|
|
111
|
+
return `Attempted to open Dench approval link in your browser. If it did not open, use this link: ${url}`;
|
|
112
|
+
}
|
|
113
|
+
if (result.status === "failed") {
|
|
114
|
+
return `Could not open browser automatically. Open this Dench approval link: ${url}`;
|
|
115
|
+
}
|
|
116
|
+
if (result.reason === "--no-open") {
|
|
117
|
+
return `Open this Dench approval link: ${url}`;
|
|
118
|
+
}
|
|
119
|
+
return `Automatic browser open is disabled (${result.reason}). Open this Dench approval link: ${url}`;
|
|
120
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dench.com/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Dench agent workspace CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dench": "dench",
|
|
8
|
+
"dench-fs-daemon": "fs-daemon"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dench",
|
|
12
|
+
"dench.mjs",
|
|
13
|
+
"dench.ts",
|
|
14
|
+
"fs-daemon",
|
|
15
|
+
"fs-daemon.ts",
|
|
16
|
+
"crm.ts",
|
|
17
|
+
"agentKind.ts",
|
|
18
|
+
"host.ts",
|
|
19
|
+
"openUrl.ts",
|
|
20
|
+
"session.ts",
|
|
21
|
+
"lib/",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chokidar": "^5.0.0",
|
|
29
|
+
"convex": "1.34.0",
|
|
30
|
+
"tsx": "^4.21.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|