@dharta/cli 0.6.77
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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/agent.d.ts +17 -0
- package/dist/agent.js +134 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-error.d.ts +3 -0
- package/dist/api-error.js +80 -0
- package/dist/api-error.js.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +19 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +1978 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +131 -0
- package/dist/commands/agents.js +439 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/analytics.d.ts +25 -0
- package/dist/commands/analytics.js +166 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/api.d.ts +19 -0
- package/dist/commands/api.js +278 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/appearance.d.ts +19 -0
- package/dist/commands/appearance.js +63 -0
- package/dist/commands/appearance.js.map +1 -0
- package/dist/commands/artifact.d.ts +33 -0
- package/dist/commands/artifact.js +235 -0
- package/dist/commands/artifact.js.map +1 -0
- package/dist/commands/audit-log.d.ts +32 -0
- package/dist/commands/audit-log.js +102 -0
- package/dist/commands/audit-log.js.map +1 -0
- package/dist/commands/auth.d.ts +47 -0
- package/dist/commands/auth.js +117 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/caps.d.ts +82 -0
- package/dist/commands/caps.js +213 -0
- package/dist/commands/caps.js.map +1 -0
- package/dist/commands/create.d.ts +22 -0
- package/dist/commands/create.js +76 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +45 -0
- package/dist/commands/deploy.js +892 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +33 -0
- package/dist/commands/dev.js +655 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/directories.d.ts +60 -0
- package/dist/commands/directories.js +83 -0
- package/dist/commands/directories.js.map +1 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/doctor.js +123 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/embed-keys.d.ts +61 -0
- package/dist/commands/embed-keys.js +165 -0
- package/dist/commands/embed-keys.js.map +1 -0
- package/dist/commands/env.d.ts +59 -0
- package/dist/commands/env.js +300 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/files.d.ts +14 -0
- package/dist/commands/files.js +84 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/git-credential.d.ts +1 -0
- package/dist/commands/git-credential.js +70 -0
- package/dist/commands/git-credential.js.map +1 -0
- package/dist/commands/kartas.d.ts +101 -0
- package/dist/commands/kartas.js +289 -0
- package/dist/commands/kartas.js.map +1 -0
- package/dist/commands/keys.d.ts +33 -0
- package/dist/commands/keys.js +141 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +78 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +14 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/members.d.ts +32 -0
- package/dist/commands/members.js +77 -0
- package/dist/commands/members.js.map +1 -0
- package/dist/commands/model-keys.d.ts +22 -0
- package/dist/commands/model-keys.js +222 -0
- package/dist/commands/model-keys.js.map +1 -0
- package/dist/commands/open.d.ts +29 -0
- package/dist/commands/open.js +119 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/parity.d.ts +15 -0
- package/dist/commands/parity.js +73 -0
- package/dist/commands/parity.js.map +1 -0
- package/dist/commands/rollback.d.ts +21 -0
- package/dist/commands/rollback.js +26 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/schedules.d.ts +52 -0
- package/dist/commands/schedules.js +231 -0
- package/dist/commands/schedules.js.map +1 -0
- package/dist/commands/scopes.d.ts +3 -0
- package/dist/commands/scopes.js +21 -0
- package/dist/commands/scopes.js.map +1 -0
- package/dist/commands/sessions.d.ts +53 -0
- package/dist/commands/sessions.js +142 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/setup.d.ts +28 -0
- package/dist/commands/setup.js +949 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +19 -0
- package/dist/commands/status.js +63 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +120 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/usage.d.ts +3 -0
- package/dist/commands/usage.js +37 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/webhooks.d.ts +47 -0
- package/dist/commands/webhooks.js +156 -0
- package/dist/commands/webhooks.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/completions.d.ts +13 -0
- package/dist/completions.js +120 -0
- package/dist/completions.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +14 -0
- package/dist/context.js +38 -0
- package/dist/context.js.map +1 -0
- package/dist/device-flow.d.ts +9 -0
- package/dist/device-flow.js +127 -0
- package/dist/device-flow.js.map +1 -0
- package/dist/harness-preflight.d.ts +24 -0
- package/dist/harness-preflight.js +252 -0
- package/dist/harness-preflight.js.map +1 -0
- package/dist/harnesses.d.ts +11 -0
- package/dist/harnesses.js +112 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/login-offer.d.ts +9 -0
- package/dist/login-offer.js +36 -0
- package/dist/login-offer.js.map +1 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +142 -0
- package/dist/manifest.js.map +1 -0
- package/dist/money.d.ts +1 -0
- package/dist/money.js +16 -0
- package/dist/money.js.map +1 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.js +20 -0
- package/dist/output.js.map +1 -0
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.js +95 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scaffold.d.ts +16 -0
- package/dist/scaffold.js +74 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/setup-scan.d.ts +21 -0
- package/dist/setup-scan.js +127 -0
- package/dist/setup-scan.js.map +1 -0
- package/dist/templates.d.ts +14 -0
- package/dist/templates.js +65 -0
- package/dist/templates.js.map +1 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +116 -0
- package/dist/validators.js.map +1 -0
- package/dist/version-check.d.ts +25 -0
- package/dist/version-check.js +68 -0
- package/dist/version-check.js.map +1 -0
- package/node_modules/@dharta/sdk/LICENSE +21 -0
- package/node_modules/@dharta/sdk/README.md +87 -0
- package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
- package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/errors.js +51 -0
- package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.js +136 -0
- package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.js +21 -0
- package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
- package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/resources.js +150 -0
- package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
- package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/types.js +59 -0
- package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
- package/node_modules/@dharta/sdk/package.json +32 -0
- package/node_modules/@dharta/sdk/src/errors.ts +58 -0
- package/node_modules/@dharta/sdk/src/http.ts +166 -0
- package/node_modules/@dharta/sdk/src/index.ts +36 -0
- package/node_modules/@dharta/sdk/src/resources.ts +197 -0
- package/node_modules/@dharta/sdk/src/types.ts +179 -0
- package/package.json +38 -0
- package/src/agent.ts +147 -0
- package/src/api-error.ts +88 -0
- package/src/browser.ts +20 -0
- package/src/cli.ts +2339 -0
- package/src/commands/agents.ts +635 -0
- package/src/commands/analytics.ts +181 -0
- package/src/commands/api.ts +297 -0
- package/src/commands/appearance.ts +79 -0
- package/src/commands/artifact.ts +331 -0
- package/src/commands/audit-log.ts +125 -0
- package/src/commands/auth.ts +186 -0
- package/src/commands/caps.ts +302 -0
- package/src/commands/create.ts +114 -0
- package/src/commands/deploy.ts +1122 -0
- package/src/commands/dev.ts +792 -0
- package/src/commands/directories.ts +136 -0
- package/src/commands/doctor.ts +138 -0
- package/src/commands/embed-keys.ts +237 -0
- package/src/commands/env.ts +369 -0
- package/src/commands/files.ts +124 -0
- package/src/commands/git-credential.ts +78 -0
- package/src/commands/kartas.ts +439 -0
- package/src/commands/keys.ts +206 -0
- package/src/commands/login.ts +115 -0
- package/src/commands/logs.ts +111 -0
- package/src/commands/members.ts +109 -0
- package/src/commands/model-keys.ts +278 -0
- package/src/commands/open.ts +173 -0
- package/src/commands/parity.ts +96 -0
- package/src/commands/rollback.ts +45 -0
- package/src/commands/schedules.ts +290 -0
- package/src/commands/scopes.ts +22 -0
- package/src/commands/sessions.ts +225 -0
- package/src/commands/setup.ts +1176 -0
- package/src/commands/status.ts +94 -0
- package/src/commands/update.ts +157 -0
- package/src/commands/usage.ts +41 -0
- package/src/commands/webhooks.ts +200 -0
- package/src/commands/whoami.ts +24 -0
- package/src/completions.ts +168 -0
- package/src/config.ts +143 -0
- package/src/context.ts +59 -0
- package/src/device-flow.ts +174 -0
- package/src/harness-preflight.ts +293 -0
- package/src/harnesses.ts +117 -0
- package/src/index.ts +3 -0
- package/src/login-offer.ts +49 -0
- package/src/manifest.ts +153 -0
- package/src/money.ts +15 -0
- package/src/output.ts +22 -0
- package/src/runtime.ts +127 -0
- package/src/scaffold.ts +102 -0
- package/src/setup-scan.ts +154 -0
- package/src/templates.ts +85 -0
- package/src/validators.ts +124 -0
- package/src/version-check.ts +75 -0
- package/templates/blank/CLAUDE.md +21 -0
- package/templates/blank/dharta.toml +11 -0
- package/templates/faq-agent/CLAUDE.md +51 -0
- package/templates/faq-agent/dharta.toml +11 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { HttpClient } from "./http.js";
|
|
2
|
+
import type {
|
|
3
|
+
ApiKey,
|
|
4
|
+
ApiKeyCreateResponse,
|
|
5
|
+
ApiKeyScope,
|
|
6
|
+
BillingStatus,
|
|
7
|
+
ModelKey,
|
|
8
|
+
ModelKeyCreateParams,
|
|
9
|
+
ModelKeyProvider,
|
|
10
|
+
UsageSummary,
|
|
11
|
+
WebhookEndpoint,
|
|
12
|
+
WebhookEndpointCreateResponse,
|
|
13
|
+
WebhookEventType,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
interface ListEnvelope<T> {
|
|
17
|
+
data: T[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ModelKeyEnvelope {
|
|
21
|
+
model_key: ModelKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface WebhookEndpointEnvelope {
|
|
25
|
+
webhook_endpoint: WebhookEndpoint;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ApiKeysResource {
|
|
29
|
+
constructor(private readonly http: HttpClient) {}
|
|
30
|
+
|
|
31
|
+
async list(): Promise<ApiKey[]> {
|
|
32
|
+
const res = await this.http.request<ListEnvelope<ApiKey>>({ path: "/api_keys" });
|
|
33
|
+
return res.data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async create(name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreateResponse> {
|
|
37
|
+
return this.http.request<ApiKeyCreateResponse>({
|
|
38
|
+
method: "POST",
|
|
39
|
+
path: "/api_keys",
|
|
40
|
+
headers: idempotencyHeaders(),
|
|
41
|
+
body: { name, scopes },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async rename(id: string, name: string): Promise<ApiKey> {
|
|
46
|
+
const res = await this.http.request<{ api_key: ApiKey }>({
|
|
47
|
+
method: "PATCH",
|
|
48
|
+
path: `/api_keys/${encodeURIComponent(id)}`,
|
|
49
|
+
headers: idempotencyHeaders(),
|
|
50
|
+
body: { name },
|
|
51
|
+
});
|
|
52
|
+
return res.api_key;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async revoke(id: string): Promise<void> {
|
|
56
|
+
await this.http.request<void>({
|
|
57
|
+
method: "DELETE",
|
|
58
|
+
path: `/api_keys/${encodeURIComponent(id)}`,
|
|
59
|
+
headers: idempotencyHeaders(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ModelKeysResource {
|
|
65
|
+
constructor(private readonly http: HttpClient) {}
|
|
66
|
+
|
|
67
|
+
async list(): Promise<ModelKey[]> {
|
|
68
|
+
const res = await this.http.request<ListEnvelope<ModelKey>>({ path: "/model_keys" });
|
|
69
|
+
return res.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async create(provider: ModelKeyProvider, plaintext: string, name?: string): Promise<ModelKey>;
|
|
73
|
+
async create(params: ModelKeyCreateParams): Promise<ModelKey>;
|
|
74
|
+
async create(
|
|
75
|
+
providerOrParams: ModelKeyProvider | ModelKeyCreateParams,
|
|
76
|
+
plaintext?: string,
|
|
77
|
+
name?: string,
|
|
78
|
+
): Promise<ModelKey> {
|
|
79
|
+
const body = typeof providerOrParams === "string"
|
|
80
|
+
? { provider: providerOrParams, plaintext, name }
|
|
81
|
+
: providerOrParams;
|
|
82
|
+
const res = await this.http.request<ModelKeyEnvelope>({
|
|
83
|
+
method: "POST",
|
|
84
|
+
path: "/model_keys",
|
|
85
|
+
headers: idempotencyHeaders(),
|
|
86
|
+
body,
|
|
87
|
+
});
|
|
88
|
+
return res.model_key;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async revoke(id: string | number): Promise<void> {
|
|
92
|
+
await this.http.request<void>({
|
|
93
|
+
method: "DELETE",
|
|
94
|
+
path: `/model_keys/${encodeURIComponent(String(id))}`,
|
|
95
|
+
headers: idempotencyHeaders(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface WebhookEndpointCreateParams {
|
|
101
|
+
url: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
event_types?: WebhookEventType[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface WebhookEndpointUpdateParams {
|
|
107
|
+
name?: string;
|
|
108
|
+
url?: string;
|
|
109
|
+
event_types?: WebhookEventType[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class WebhookEndpointsResource {
|
|
113
|
+
constructor(private readonly http: HttpClient) {}
|
|
114
|
+
|
|
115
|
+
async list(): Promise<WebhookEndpoint[]> {
|
|
116
|
+
const res = await this.http.request<ListEnvelope<WebhookEndpoint>>({ path: "/api/webhook_endpoints" });
|
|
117
|
+
return res.data;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async create(params: WebhookEndpointCreateParams): Promise<WebhookEndpointCreateResponse> {
|
|
121
|
+
return this.http.request<WebhookEndpointCreateResponse>({
|
|
122
|
+
method: "POST",
|
|
123
|
+
path: "/api/webhook_endpoints",
|
|
124
|
+
headers: idempotencyHeaders(),
|
|
125
|
+
body: params,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async update(id: string, params: WebhookEndpointUpdateParams): Promise<WebhookEndpoint> {
|
|
130
|
+
const res = await this.http.request<WebhookEndpointEnvelope>({
|
|
131
|
+
method: "PATCH",
|
|
132
|
+
path: `/api/webhook_endpoints/${encodeURIComponent(id)}`,
|
|
133
|
+
headers: idempotencyHeaders(),
|
|
134
|
+
body: params,
|
|
135
|
+
});
|
|
136
|
+
return res.webhook_endpoint;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async enable(id: string): Promise<WebhookEndpoint> {
|
|
140
|
+
const res = await this.http.request<WebhookEndpointEnvelope>({
|
|
141
|
+
method: "POST",
|
|
142
|
+
path: `/api/webhook_endpoints/${encodeURIComponent(id)}/enable`,
|
|
143
|
+
headers: idempotencyHeaders(),
|
|
144
|
+
});
|
|
145
|
+
return res.webhook_endpoint;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async delete(id: string): Promise<void> {
|
|
149
|
+
await this.http.request<void>({
|
|
150
|
+
method: "DELETE",
|
|
151
|
+
path: `/api/webhook_endpoints/${encodeURIComponent(id)}`,
|
|
152
|
+
headers: idempotencyHeaders(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class UsageResource {
|
|
158
|
+
constructor(private readonly http: HttpClient) {}
|
|
159
|
+
|
|
160
|
+
async summary(): Promise<UsageSummary> {
|
|
161
|
+
return this.http.request<UsageSummary>({ path: "/api/usage" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class BillingResource {
|
|
166
|
+
constructor(private readonly http: HttpClient) {}
|
|
167
|
+
|
|
168
|
+
async status(): Promise<BillingStatus> {
|
|
169
|
+
return this.http.request<BillingStatus>({ path: "/api/billing" });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function idempotencyHeaders(): Record<string, string> {
|
|
174
|
+
return { "Idempotency-Key": randomIdempotencyKey() };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function randomIdempotencyKey(): string {
|
|
178
|
+
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
179
|
+
const bytes = new Uint8Array(16);
|
|
180
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
181
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
182
|
+
} else {
|
|
183
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
184
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
188
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
189
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
|
|
190
|
+
return [
|
|
191
|
+
hex.slice(0, 4).join(""),
|
|
192
|
+
hex.slice(4, 6).join(""),
|
|
193
|
+
hex.slice(6, 8).join(""),
|
|
194
|
+
hex.slice(8, 10).join(""),
|
|
195
|
+
hex.slice(10, 16).join(""),
|
|
196
|
+
].join("-");
|
|
197
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Hand-written types mirroring the Rails OpenAPI spec
|
|
2
|
+
// (apps/dharta-web/app/lib/openapi/spec.rb). Keep tight: only what
|
|
3
|
+
// the Dharta API actually returns / accepts. If a server field
|
|
4
|
+
// disappears, this file should drop it on the next bump rather than
|
|
5
|
+
// carrying dead optionals forever.
|
|
6
|
+
|
|
7
|
+
export const API_KEY_SCOPES = [
|
|
8
|
+
"agents:read",
|
|
9
|
+
"appearance:read",
|
|
10
|
+
"analytics:read",
|
|
11
|
+
"caps:read",
|
|
12
|
+
"directories:read",
|
|
13
|
+
"embed_keys:read",
|
|
14
|
+
"env:read",
|
|
15
|
+
"keys:read",
|
|
16
|
+
"members:read",
|
|
17
|
+
"model_keys:read",
|
|
18
|
+
"releases:read",
|
|
19
|
+
"schedules:read",
|
|
20
|
+
"sessions:read",
|
|
21
|
+
"source_repos:read",
|
|
22
|
+
"webhooks:read",
|
|
23
|
+
"agents:write",
|
|
24
|
+
"appearance:write",
|
|
25
|
+
"env:write",
|
|
26
|
+
"releases:write",
|
|
27
|
+
"schedules:write",
|
|
28
|
+
"webhooks:write",
|
|
29
|
+
"embed_keys:write",
|
|
30
|
+
"caps:write",
|
|
31
|
+
"source_repos:write",
|
|
32
|
+
"env:secrets:read",
|
|
33
|
+
"source_archives:read",
|
|
34
|
+
"workspace_tokens:create",
|
|
35
|
+
"audit_log:export",
|
|
36
|
+
"members:invite",
|
|
37
|
+
"members:write",
|
|
38
|
+
"members:owner_role:write",
|
|
39
|
+
"agents:delete",
|
|
40
|
+
"keys:write",
|
|
41
|
+
"model_keys:write",
|
|
42
|
+
"billing:read",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
|
|
46
|
+
|
|
47
|
+
export type ModelKeyProvider =
|
|
48
|
+
| "anthropic"
|
|
49
|
+
| "openai"
|
|
50
|
+
| "bedrock"
|
|
51
|
+
| "vertex"
|
|
52
|
+
| "openrouter"
|
|
53
|
+
| "xai"
|
|
54
|
+
| "google";
|
|
55
|
+
|
|
56
|
+
export type ModelApiFormat =
|
|
57
|
+
| "anthropic_messages"
|
|
58
|
+
| "openai_chat_completions"
|
|
59
|
+
| "openai_responses"
|
|
60
|
+
| "bedrock_converse"
|
|
61
|
+
| "vertex_generate_content";
|
|
62
|
+
|
|
63
|
+
export type ModelReasoningEffort = "low" | "medium" | "high";
|
|
64
|
+
|
|
65
|
+
export interface ModelParameters {
|
|
66
|
+
temperature?: number;
|
|
67
|
+
top_p?: number;
|
|
68
|
+
max_output_tokens?: number;
|
|
69
|
+
reasoning_effort?: ModelReasoningEffort;
|
|
70
|
+
provider_options?: {
|
|
71
|
+
openai?: {
|
|
72
|
+
service_tier?: "auto" | "default" | "flex" | "priority";
|
|
73
|
+
verbosity?: "low" | "medium" | "high";
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const WEBHOOK_EVENT_TYPES = [
|
|
79
|
+
"api_key.created",
|
|
80
|
+
"api_key.revoked",
|
|
81
|
+
"subscription.created",
|
|
82
|
+
"subscription.updated",
|
|
83
|
+
"subscription.canceled",
|
|
84
|
+
"invoice.paid",
|
|
85
|
+
"invoice.payment_failed",
|
|
86
|
+
"organization.suspended",
|
|
87
|
+
"organization.resumed",
|
|
88
|
+
"usage.budget_threshold_reached",
|
|
89
|
+
"usage.budget_exhausted",
|
|
90
|
+
"session.started",
|
|
91
|
+
"turn.completed",
|
|
92
|
+
"turn.failed",
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
|
|
96
|
+
|
|
97
|
+
export interface ApiKey {
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
prefix: string;
|
|
101
|
+
scopes: ApiKeyScope[];
|
|
102
|
+
mode?: "live" | "test";
|
|
103
|
+
expires_at?: string | null;
|
|
104
|
+
created_at: string;
|
|
105
|
+
last_used_at: string | null;
|
|
106
|
+
revoked_at: string | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ApiKeyCreateResponse {
|
|
110
|
+
api_key: ApiKey;
|
|
111
|
+
// Plaintext secret — shown exactly once, on create. Store it now.
|
|
112
|
+
secret: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ModelKey {
|
|
116
|
+
id: number;
|
|
117
|
+
provider: ModelKeyProvider;
|
|
118
|
+
name: string;
|
|
119
|
+
tail: string;
|
|
120
|
+
auth_type: "api_key" | "oauth_subscription";
|
|
121
|
+
endpoint_url: string | null;
|
|
122
|
+
api_format: ModelApiFormat | null;
|
|
123
|
+
created_at: string;
|
|
124
|
+
last_verified_at: string | null;
|
|
125
|
+
verification_error: string | null;
|
|
126
|
+
revoked_at: string | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ModelKeyCreateParams {
|
|
130
|
+
provider: ModelKeyProvider;
|
|
131
|
+
plaintext: string;
|
|
132
|
+
name?: string;
|
|
133
|
+
auth_type?: "api_key" | "oauth_subscription";
|
|
134
|
+
endpoint_url?: string;
|
|
135
|
+
api_format?: ModelApiFormat;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface WebhookEndpoint {
|
|
139
|
+
id: string;
|
|
140
|
+
name: string;
|
|
141
|
+
url: string;
|
|
142
|
+
event_types: WebhookEventType[];
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
consecutive_failures: number;
|
|
145
|
+
disabled_at: string | null;
|
|
146
|
+
created_at: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface WebhookEndpointCreateResponse {
|
|
150
|
+
webhook_endpoint: WebhookEndpoint;
|
|
151
|
+
secret: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface UsageSummary {
|
|
155
|
+
totals: {
|
|
156
|
+
input_tokens?: number;
|
|
157
|
+
output_tokens?: number;
|
|
158
|
+
requests?: number;
|
|
159
|
+
[k: string]: number | undefined;
|
|
160
|
+
};
|
|
161
|
+
budget: {
|
|
162
|
+
monthly_cap_cents: number | null;
|
|
163
|
+
period_start: string;
|
|
164
|
+
period_end: string;
|
|
165
|
+
} | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface BillingStatus {
|
|
169
|
+
plan_key: string;
|
|
170
|
+
stripe_configured: boolean;
|
|
171
|
+
subscription_state: string | null;
|
|
172
|
+
current_period_end: string | null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ErrorBody {
|
|
176
|
+
error: string;
|
|
177
|
+
message?: string;
|
|
178
|
+
details?: Record<string, unknown>;
|
|
179
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dharta/cli",
|
|
3
|
+
"version": "0.6.77",
|
|
4
|
+
"description": "Command-line interface for the Dharta API.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dharta": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"templates"
|
|
15
|
+
],
|
|
16
|
+
"bundledDependencies": [
|
|
17
|
+
"@dharta/sdk"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.17.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@dharta/sdk": "^0.6.77",
|
|
29
|
+
"@iarna/toml": "^2.2.5",
|
|
30
|
+
"commander": "^15.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^26.0.1",
|
|
34
|
+
"msw": "^2.14.6",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vitest": "^4.1.9"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Agent identity inference, shared by every per-agent command (RFC 0024 + the
|
|
2
|
+
// CLI surface spec). The agent a command targets comes from its dharta.toml -
|
|
3
|
+
// NOT the `dharta` git remote, which now points at the SOURCE REPO (one repo,
|
|
4
|
+
// many agents), so remote-slug inference would resolve the wrong thing.
|
|
5
|
+
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { existsSync, statSync } from "node:fs";
|
|
8
|
+
import { findGitRoot, scanAgentFolders, describeFolder, type AgentFolder } from "./setup-scan.js";
|
|
9
|
+
import { normalizeNonBlank } from "./validators.js";
|
|
10
|
+
|
|
11
|
+
const AGENT_SLUG_FORMAT = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
12
|
+
const MAX_AGENT_NAME_CHARACTERS = 100;
|
|
13
|
+
|
|
14
|
+
export interface AgentInferOptions {
|
|
15
|
+
agent?: string; // -a/--agent (or an explicit [slug]); the deployed agent
|
|
16
|
+
project?: string; // deprecated alias for `agent` (the -p/--project flag)
|
|
17
|
+
path?: string; // [path] positional - the agent's folder
|
|
18
|
+
dir?: string; // cwd (default process.cwd())
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
scan?: (root: string, opts: { isGitRepo: boolean }) => Promise<AgentFolder[]>; // test seam
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Resolve the agent slug a per-agent command operates on. Order (explicit
|
|
24
|
+
// beats inferred): --agent/-p, an explicit [path], the nearest containing
|
|
25
|
+
// agent folder, the repo's sole agent, then DHARTA_PROJECT (CI). A multi-agent
|
|
26
|
+
// repo root with no selector is a loud error rather than a wrong guess.
|
|
27
|
+
// Identity for a scanned agent folder: an explicit dharta.toml `slug` wins
|
|
28
|
+
// (pinned identity, free-form display name); otherwise derive from the name.
|
|
29
|
+
function slugForFolder(folder: { slug?: string; name?: string }): string {
|
|
30
|
+
if (folder.slug) return validateAgentSlug(folder.slug, "dharta.toml slug");
|
|
31
|
+
return deriveSlug(folder.name as string);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function inferAgentSlug(opts: AgentInferOptions = {}): Promise<string> {
|
|
35
|
+
validateAgentSelectorOptions(opts);
|
|
36
|
+
if (opts.agent !== undefined) return normalizeAgentSlug(opts.agent, "--agent");
|
|
37
|
+
if (opts.project !== undefined) return normalizeAgentSlug(opts.project, "--project");
|
|
38
|
+
|
|
39
|
+
const cwd = path.resolve(opts.dir ?? process.cwd());
|
|
40
|
+
|
|
41
|
+
if (opts.path) {
|
|
42
|
+
const dir = path.resolve(cwd, opts.path);
|
|
43
|
+
const folder = await describeFolder(dir, path.basename(dir));
|
|
44
|
+
if (folder.name) return slugForFolder(folder);
|
|
45
|
+
throw new Error(`No agent at ${opts.path} - it has no dharta.toml with a name.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const gitRoot = findGitRoot(cwd);
|
|
49
|
+
let localDir = cwd;
|
|
50
|
+
for (;;) {
|
|
51
|
+
if (existsSync(path.join(localDir, "dharta.toml"))) {
|
|
52
|
+
const local = await describeFolder(localDir, path.basename(localDir));
|
|
53
|
+
if (local.name) return slugForFolder(local);
|
|
54
|
+
throw new Error(`${path.join(localDir, "dharta.toml")} must declare an agent name.`);
|
|
55
|
+
}
|
|
56
|
+
if (gitRoot && localDir === path.resolve(gitRoot)) break;
|
|
57
|
+
const parent = path.dirname(localDir);
|
|
58
|
+
if (parent === localDir) break;
|
|
59
|
+
localDir = parent;
|
|
60
|
+
}
|
|
61
|
+
const scan = opts.scan ?? scanAgentFolders;
|
|
62
|
+
const agents = (await scan(gitRoot ?? cwd, { isGitRepo: !!gitRoot })).filter((f) => f.name);
|
|
63
|
+
const containing = agents
|
|
64
|
+
.filter((folder) => cwd === path.resolve(folder.dir) || cwd.startsWith(`${path.resolve(folder.dir)}${path.sep}`))
|
|
65
|
+
.sort((a, b) => path.resolve(b.dir).length - path.resolve(a.dir).length);
|
|
66
|
+
if (containing.length > 0) return slugForFolder(containing[0]);
|
|
67
|
+
if (agents.length === 1) return slugForFolder(agents[0]);
|
|
68
|
+
|
|
69
|
+
// The sole local agent wins over the env (local context beats CI); the env
|
|
70
|
+
// is the fallback when the local context is empty or ambiguous (a monorepo
|
|
71
|
+
// CI job names its target with DHARTA_PROJECT).
|
|
72
|
+
const envAgent = (opts.env ?? process.env).DHARTA_PROJECT;
|
|
73
|
+
if (envAgent !== undefined) return normalizeAgentSlug(envAgent, "DHARTA_PROJECT");
|
|
74
|
+
|
|
75
|
+
if (agents.length === 0) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"No agent here. Run inside an agent folder (one with a dharta.toml), or pass --agent <slug>.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const list = agents.map((f) => f.rel).join(", ");
|
|
81
|
+
throw new Error(`Multiple agents here (${list}). Pass --agent <slug> or a [path] to pick one.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validateAgentSelectorOptions(opts: Pick<AgentInferOptions, "agent" | "project" | "dir"> = {}): void {
|
|
85
|
+
if (opts.agent !== undefined) validateAgentSlug(opts.agent, "--agent");
|
|
86
|
+
if (opts.project !== undefined) validateAgentSlug(opts.project, "--project");
|
|
87
|
+
if (opts.dir === undefined) return;
|
|
88
|
+
const dir = path.resolve(normalizeNonBlank(opts.dir, "--dir"));
|
|
89
|
+
try {
|
|
90
|
+
if (!statSync(dir).isDirectory()) {
|
|
91
|
+
throw new Error(`Cannot use --dir ${dir}: expected a directory.`);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
95
|
+
throw new Error(`Cannot use --dir ${dir}: directory does not exist.`);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// https://git.dharta.ai/{org}/{slug}.git -> slug. Still used by `deploy` to read
|
|
102
|
+
// the wired `dharta` remote (a source-repo slug there, which is what deploy wants).
|
|
103
|
+
export function slugFromRemote(url: string): string | null {
|
|
104
|
+
const match = url.match(/\/([^/]+)\/([^/]+?)(\.git)?$/);
|
|
105
|
+
return match ? match[2] : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Port of the server's Agent.derive_slug: lowercase, underscores/spaces
|
|
109
|
+
// -> hyphens, drop disallowed chars, truncate to 63, strip edge hyphens, fall
|
|
110
|
+
// back to "agent". Shared by setup (default name), the agent resolver above,
|
|
111
|
+
// and the live preview JS.
|
|
112
|
+
export function deriveSlug(name: string): string {
|
|
113
|
+
let base = name.toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
114
|
+
base = base.slice(0, 63).replace(/^-+|-+$/g, "");
|
|
115
|
+
return base || "agent";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validateAgentSlug(value: string, label: string): string {
|
|
119
|
+
const normalized = normalizeNonBlank(value, label);
|
|
120
|
+
if (!AGENT_SLUG_FORMAT.test(normalized)) {
|
|
121
|
+
const suggestion = slugSuggestion(normalized);
|
|
122
|
+
throw new Error(
|
|
123
|
+
`${label} must be a valid agent slug: lowercase letters, digits, hyphens; max 63.` +
|
|
124
|
+
(suggestion ? ` Did you mean ${suggestion}?` : ""),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Mirrors Agent's public display-name contract. Count Unicode code points,
|
|
131
|
+
// rather than UTF-16 code units, so client-side validation agrees with Ruby's
|
|
132
|
+
// String#length for non-ASCII names.
|
|
133
|
+
export function validateAgentName(value: string, label = "agent name"): string {
|
|
134
|
+
const normalized = normalizeNonBlank(value, label);
|
|
135
|
+
if ([...normalized].length > MAX_AGENT_NAME_CHARACTERS) {
|
|
136
|
+
throw new Error(`${label} must be 1-${MAX_AGENT_NAME_CHARACTERS} characters.`);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const normalizeAgentSlug = validateAgentSlug;
|
|
142
|
+
|
|
143
|
+
function slugSuggestion(value: string): string | null {
|
|
144
|
+
if (!/^[A-Za-z0-9 _-]+$/.test(value)) return null;
|
|
145
|
+
const suggestion = deriveSlug(value);
|
|
146
|
+
return suggestion !== value && AGENT_SLUG_FORMAT.test(suggestion) ? suggestion : null;
|
|
147
|
+
}
|
package/src/api-error.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Error formatting for the CLI's control-plane calls. Passed to the SDK
|
|
2
|
+
// HttpClient as `formatError` so CLI errors keep their human message + the
|
|
3
|
+
// `err.status`/`err.body` shape (instead of the SDK's typed errorFor()
|
|
4
|
+
// hierarchy, which the CLI doesn't surface).
|
|
5
|
+
|
|
6
|
+
import { SENSITIVE_API_KEY_SCOPES } from "./commands/scopes.js";
|
|
7
|
+
|
|
8
|
+
export function cliFormatError(status: number, parsed: unknown): Error {
|
|
9
|
+
// Server messages already name fixes (e.g. the transcript-access 403 points
|
|
10
|
+
// at the settings page) - keep them verbatim. Rails sends `message`; the
|
|
11
|
+
// agent host (FastAPI) sends `detail`; some raw upload endpoints send
|
|
12
|
+
// `error`.
|
|
13
|
+
const body = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
|
14
|
+
const serverMessage =
|
|
15
|
+
typeof body.message === "string"
|
|
16
|
+
? body.message
|
|
17
|
+
: typeof body.detail === "string"
|
|
18
|
+
? body.detail
|
|
19
|
+
: typeof body.error === "string" && !looksLikeMachineCode(body.error)
|
|
20
|
+
? body.error
|
|
21
|
+
: null;
|
|
22
|
+
let message = serverMessage ?? `Dharta API error ${status}`;
|
|
23
|
+
if (status === 401) {
|
|
24
|
+
message += `${message.endsWith(".") ? "" : "."} Run \`dharta login\` to re-authenticate (or check DHARTA_API_KEY).`;
|
|
25
|
+
} else if (status === 403 && /scope/i.test(message)) {
|
|
26
|
+
message += `${message.endsWith(".") ? "" : "."} Run \`dharta auth status\` to inspect the active key, or \`dharta open api-keys\` to manage key scopes.`;
|
|
27
|
+
const sensitiveScopes = missingSensitiveScopes(message);
|
|
28
|
+
if (sensitiveScopes.length > 0) {
|
|
29
|
+
message += ` Sensitive scopes (${sensitiveScopes.join(", ")}) require a bounded key with --expires-at within 90 days.`;
|
|
30
|
+
}
|
|
31
|
+
} else if (status === 404 && (!serverMessage || isGenericNotFound(serverMessage))) {
|
|
32
|
+
message +=
|
|
33
|
+
`${message.endsWith(".") ? "" : "."} ` +
|
|
34
|
+
"Check the id or agent slug; for agent commands, run `dharta agent list`. " +
|
|
35
|
+
"Run `dharta auth status` to confirm the active host; DHARTA_BASE_URL must point at the Dharta control API.";
|
|
36
|
+
}
|
|
37
|
+
const err = new Error(message) as Error & { status?: number; body?: unknown };
|
|
38
|
+
err.status = status;
|
|
39
|
+
err.body = parsed;
|
|
40
|
+
return err;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function cliFormatNetworkError(url: string, error: unknown): Error {
|
|
44
|
+
const origin = new URL(url).origin;
|
|
45
|
+
return new Error(
|
|
46
|
+
`Could not reach Dharta at ${origin} (${formatCause(error)}). ` +
|
|
47
|
+
"Check your network and DHARTA_BASE_URL, then try again.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function cliFormatRawError(res: Response): Promise<Error> {
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
let parsed: unknown = null;
|
|
54
|
+
if (text.length > 0) {
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(text);
|
|
57
|
+
} catch {
|
|
58
|
+
parsed = text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return cliFormatError(res.status, parsed);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function missingSensitiveScopes(message: string): string[] {
|
|
65
|
+
const mentioned = new Set(mentionedScopes(message));
|
|
66
|
+
if (/one of required scopes/i.test(message) && Array.from(mentioned).some((scope) => !SENSITIVE_API_KEY_SCOPES.has(scope))) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return Array.from(SENSITIVE_API_KEY_SCOPES).filter((scope) => mentioned.has(scope));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mentionedScopes(message: string): string[] {
|
|
73
|
+
const required = message.match(/required scopes?:\s*([a-z0-9_:,\s-]+)/i)?.[1];
|
|
74
|
+
const source = required ?? message;
|
|
75
|
+
return source.match(/[a-z][a-z0-9_]*(?::[a-z][a-z0-9_]*)?/g) ?? [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatCause(error: unknown): string {
|
|
79
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function looksLikeMachineCode(value: string): boolean {
|
|
83
|
+
return /^[a-z][a-z0-9_:-]*$/.test(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isGenericNotFound(value: string): boolean {
|
|
87
|
+
return /^resource not found\.?$/i.test(value.trim()) || /^not found\.?$/i.test(value.trim());
|
|
88
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Best-effort browser launch; callers always print the URL too, so a
|
|
4
|
+
// headless box still works.
|
|
5
|
+
export function openBrowser(url: string): Promise<boolean> {
|
|
6
|
+
const cmd =
|
|
7
|
+
process.platform === "darwin"
|
|
8
|
+
? "open"
|
|
9
|
+
: process.platform === "win32"
|
|
10
|
+
? "explorer"
|
|
11
|
+
: "xdg-open";
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const proc = spawn(cmd, [url], { stdio: "ignore", detached: true });
|
|
14
|
+
proc.on("error", () => resolve(false));
|
|
15
|
+
proc.on("spawn", () => {
|
|
16
|
+
proc.unref();
|
|
17
|
+
resolve(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|