@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,369 @@
|
|
|
1
|
+
// `dharta env list|set|unset|pull|diff` (RFC 0013 CLI-8): the prod half
|
|
2
|
+
// of declare-or-bind. Declarations live in dharta.toml [env]; .env binds
|
|
3
|
+
// locally; these verbs manage the prod bindings and report drift.
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import TOML from "@iarna/toml";
|
|
9
|
+
import type { CliContext } from "../context.js";
|
|
10
|
+
import { inferAgentSlug, validateAgentSelectorOptions, type AgentInferOptions } from "../agent.js";
|
|
11
|
+
import { table } from "../output.js";
|
|
12
|
+
import { normalizeNonBlank } from "../validators.js";
|
|
13
|
+
|
|
14
|
+
export interface EnvOptions extends AgentInferOptions {
|
|
15
|
+
force?: boolean; // pull: overwrite an existing .env
|
|
16
|
+
includeSecrets?: boolean; // pull: fetch secret-marked values (env:secrets:read)
|
|
17
|
+
secret?: boolean; // set: mark the value secret
|
|
18
|
+
reveal?: boolean; // list: print full values instead of masking them
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Env values routinely hold credentials the setter never flagged `--secret`
|
|
22
|
+
// (a GitHub PAT, a webhook signing key). `list` therefore masks EVERY value by
|
|
23
|
+
// default - a routine listing must never dump a live secret to the terminal or
|
|
24
|
+
// CI logs. A short tail is kept so an operator can still tell two bindings
|
|
25
|
+
// apart (matching how `whoami`/model keys show only a tail); `--reveal` prints
|
|
26
|
+
// the full value for the rare time it is actually needed.
|
|
27
|
+
export function maskEnvValue(v: { secret: boolean; value?: string | null }, reveal: boolean): string {
|
|
28
|
+
if (reveal) return v.value ?? "";
|
|
29
|
+
if (v.secret) return "•••••• (secret)";
|
|
30
|
+
const val = v.value ?? "";
|
|
31
|
+
if (val === "") return "";
|
|
32
|
+
return val.length > 4 ? `…${val.slice(-4)}` : "••••";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EnvEntry {
|
|
36
|
+
name: string;
|
|
37
|
+
secret: boolean;
|
|
38
|
+
value?: string;
|
|
39
|
+
withheld?: boolean;
|
|
40
|
+
updated_at?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function envListData(
|
|
44
|
+
ctx: CliContext,
|
|
45
|
+
opts: EnvOptions = {},
|
|
46
|
+
): Promise<{ slug: string; env: EnvEntry[] }> {
|
|
47
|
+
const slug = await inferAgentSlug(opts);
|
|
48
|
+
const body = await ctx.http.request<{ env: EnvEntry[] }>({
|
|
49
|
+
path: `/api/agents/${encodeURIComponent(slug)}/env`,
|
|
50
|
+
});
|
|
51
|
+
return { slug, env: body.env };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function envList(ctx: CliContext, opts: EnvOptions = {}): Promise<string> {
|
|
55
|
+
const { slug, env } = await envListData(ctx, opts);
|
|
56
|
+
if (env.length === 0) {
|
|
57
|
+
return `No env vars set for ${slug}. Bind one with: dharta env set NAME=value`;
|
|
58
|
+
}
|
|
59
|
+
return table(
|
|
60
|
+
env.map((v) => ({
|
|
61
|
+
name: v.name,
|
|
62
|
+
value: maskEnvValue(v, opts.reveal === true),
|
|
63
|
+
updated: v.updated_at ?? "",
|
|
64
|
+
})),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function envSetData(
|
|
69
|
+
ctx: CliContext,
|
|
70
|
+
assignment: string,
|
|
71
|
+
opts: EnvOptions = {},
|
|
72
|
+
): Promise<{ slug: string; name: string; secret: boolean }> {
|
|
73
|
+
const { name, value } = parseEnvAssignment(assignment);
|
|
74
|
+
const slug = await inferAgentSlug(opts);
|
|
75
|
+
const body: Record<string, unknown> = { value };
|
|
76
|
+
if (opts.secret) body.secret = true;
|
|
77
|
+
await ctx.http.request({
|
|
78
|
+
method: "PUT",
|
|
79
|
+
path: `/api/agents/${encodeURIComponent(slug)}/env/${encodeURIComponent(name)}`,
|
|
80
|
+
body,
|
|
81
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
82
|
+
});
|
|
83
|
+
return { slug, name, secret: !!opts.secret };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validateEnvAssignment(assignment: string): void {
|
|
87
|
+
parseEnvAssignment(assignment);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateEnvName(name: string): void {
|
|
91
|
+
normalizeEnvName(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseEnvAssignment(assignment: string): { name: string; value: string } {
|
|
95
|
+
const eq = assignment.indexOf("=");
|
|
96
|
+
if (eq <= 0) {
|
|
97
|
+
throw new Error("Usage: dharta env set NAME=value [--secret]");
|
|
98
|
+
}
|
|
99
|
+
const name = normalizeEnvName(assignment.slice(0, eq));
|
|
100
|
+
const value = assignment.slice(eq + 1);
|
|
101
|
+
return { name, value };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function envSet(
|
|
105
|
+
ctx: CliContext,
|
|
106
|
+
assignment: string,
|
|
107
|
+
opts: EnvOptions = {},
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
const { slug, name, secret } = await envSetData(ctx, assignment, opts);
|
|
110
|
+
return (
|
|
111
|
+
`Set ${name}${secret ? " (secret)" : ""} on ${slug}. ` +
|
|
112
|
+
"The next build picks it up; redeploy with git push dharta main if needed."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function envUnsetData(
|
|
117
|
+
ctx: CliContext,
|
|
118
|
+
name: string,
|
|
119
|
+
opts: EnvOptions = {},
|
|
120
|
+
): Promise<{ slug: string; name: string; removed: boolean }> {
|
|
121
|
+
const normalizedName = normalizeEnvName(name);
|
|
122
|
+
const slug = await inferAgentSlug(opts);
|
|
123
|
+
await ctx.http.request({
|
|
124
|
+
method: "DELETE",
|
|
125
|
+
path: `/api/agents/${encodeURIComponent(slug)}/env/${encodeURIComponent(normalizedName)}`,
|
|
126
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
127
|
+
});
|
|
128
|
+
return { slug, name: normalizedName, removed: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function envUnset(
|
|
132
|
+
ctx: CliContext,
|
|
133
|
+
name: string,
|
|
134
|
+
opts: EnvOptions = {},
|
|
135
|
+
): Promise<string> {
|
|
136
|
+
const { slug, name: removed } = await envUnsetData(ctx, name, opts);
|
|
137
|
+
return `Removed ${removed} from ${slug}.`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Hydrate .env from prod (D6): secret-marked values are excluded unless
|
|
141
|
+
// --include-secrets (bounded env:secrets:read key);
|
|
142
|
+
// every pull is audited server-side.
|
|
143
|
+
export async function envPullData(
|
|
144
|
+
ctx: CliContext,
|
|
145
|
+
opts: EnvOptions = {},
|
|
146
|
+
): Promise<{ slug: string; path: string; written: string[]; withheld: string[] }> {
|
|
147
|
+
const dir = path.resolve(opts.dir ?? process.cwd());
|
|
148
|
+
const target = path.join(dir, ".env");
|
|
149
|
+
|
|
150
|
+
await validateEnvPullOptions(opts);
|
|
151
|
+
const slug = await inferAgentSlug(opts);
|
|
152
|
+
|
|
153
|
+
const body = await ctx.http.request<{ env: EnvEntry[] }>({
|
|
154
|
+
method: "POST",
|
|
155
|
+
path: `/api/agents/${encodeURIComponent(slug)}/env/pull`,
|
|
156
|
+
body: { include_secrets: !!opts.includeSecrets },
|
|
157
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const bound = body.env.filter((v) => !v.withheld);
|
|
161
|
+
const lines = bound.map((v) => `${v.name}=${quoteIfNeeded(v.value ?? "")}`);
|
|
162
|
+
const withheld = body.env.filter((v) => v.withheld).map((v) => v.name);
|
|
163
|
+
|
|
164
|
+
await writeEnvFile(
|
|
165
|
+
target,
|
|
166
|
+
`# Pulled from ${slug} by dharta env pull - do not commit.\n${lines.join("\n")}\n`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return { slug, path: target, written: bound.map((v) => v.name), withheld };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function envPull(ctx: CliContext, opts: EnvOptions = {}): Promise<string> {
|
|
173
|
+
const res = await envPullData(ctx, opts);
|
|
174
|
+
const n = res.written.length;
|
|
175
|
+
let out = `Wrote ${n} value${n === 1 ? "" : "s"} to ${res.path}.`;
|
|
176
|
+
if (res.withheld.length > 0) {
|
|
177
|
+
out +=
|
|
178
|
+
`\n${res.withheld.length} secret value${res.withheld.length === 1 ? "" : "s"} withheld (${res.withheld.join(", ")}). ` +
|
|
179
|
+
"Re-run with --include-secrets using a key with env:secrets:read to fetch them (audited).";
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type DriftKind =
|
|
185
|
+
| "prod-only"
|
|
186
|
+
| "value-differs"
|
|
187
|
+
| "secret"
|
|
188
|
+
| "local-only"
|
|
189
|
+
| "declared-only";
|
|
190
|
+
|
|
191
|
+
export interface DriftEntry {
|
|
192
|
+
kind: DriftKind;
|
|
193
|
+
name: string;
|
|
194
|
+
secret?: boolean;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Local (.env) vs prod drift, plus the dharta.toml [env] declarations -
|
|
198
|
+
// the report half of declare-or-bind (dev warns, build fails, diff
|
|
199
|
+
// explains).
|
|
200
|
+
export async function envDiffData(
|
|
201
|
+
ctx: CliContext,
|
|
202
|
+
opts: EnvOptions = {},
|
|
203
|
+
): Promise<{ slug: string; drift: DriftEntry[] }> {
|
|
204
|
+
const dir = path.resolve(opts.dir ?? process.cwd());
|
|
205
|
+
await validateEnvDiffOptions(opts);
|
|
206
|
+
const slug = await inferAgentSlug(opts);
|
|
207
|
+
const local = parseDotenvFile(await readOptionalLocalFile(path.join(dir, ".env"), ".env"));
|
|
208
|
+
const declared = await declaredEnvNames(dir);
|
|
209
|
+
|
|
210
|
+
const body = await ctx.http.request<{ env: EnvEntry[] }>({
|
|
211
|
+
path: `/api/agents/${encodeURIComponent(slug)}/env`,
|
|
212
|
+
});
|
|
213
|
+
const prod = new Map(body.env.map((v) => [v.name, v]));
|
|
214
|
+
|
|
215
|
+
const drift: DriftEntry[] = [];
|
|
216
|
+
for (const [name, entry] of prod) {
|
|
217
|
+
if (!(name in local)) {
|
|
218
|
+
drift.push({ kind: "prod-only", name, secret: entry.secret });
|
|
219
|
+
} else if (!entry.secret && entry.value !== local[name]) {
|
|
220
|
+
drift.push({ kind: "value-differs", name });
|
|
221
|
+
} else if (entry.secret) {
|
|
222
|
+
drift.push({ kind: "secret", name, secret: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const name of Object.keys(local)) {
|
|
226
|
+
if (!prod.has(name)) drift.push({ kind: "local-only", name });
|
|
227
|
+
}
|
|
228
|
+
for (const name of declared) {
|
|
229
|
+
if (!prod.has(name) && !(name in local)) drift.push({ kind: "declared-only", name });
|
|
230
|
+
}
|
|
231
|
+
return { slug, drift };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function envDiff(ctx: CliContext, opts: EnvOptions = {}): Promise<string> {
|
|
235
|
+
const { slug, drift } = await envDiffData(ctx, opts);
|
|
236
|
+
if (drift.length === 0) return `No drift: .env and ${slug}'s prod bindings agree.`;
|
|
237
|
+
return drift.map(formatDrift).join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatDrift(d: DriftEntry): string {
|
|
241
|
+
switch (d.kind) {
|
|
242
|
+
case "prod-only":
|
|
243
|
+
return `prod only ${d.name}${d.secret ? " (secret)" : ""} - add to .env (dharta env pull)`;
|
|
244
|
+
case "value-differs":
|
|
245
|
+
return `value differs ${d.name} - local .env and prod disagree`;
|
|
246
|
+
case "secret":
|
|
247
|
+
return `secret ${d.name} - prod value not comparable`;
|
|
248
|
+
case "local-only":
|
|
249
|
+
return `local only ${d.name} - bind in prod with: dharta env set ${d.name}=...`;
|
|
250
|
+
case "declared-only":
|
|
251
|
+
return `declared only ${d.name} - in dharta.toml [env] but bound nowhere`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function declaredEnvNames(dir: string): Promise<string[]> {
|
|
256
|
+
const raw = await readOptionalLocalFile(path.join(dir, "dharta.toml"), "dharta.toml");
|
|
257
|
+
if (!raw) return [];
|
|
258
|
+
try {
|
|
259
|
+
const parsed = TOML.parse(raw) as Record<string, unknown>;
|
|
260
|
+
const env = parsed.env;
|
|
261
|
+
return env && typeof env === "object" ? Object.keys(env) : [];
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function parseDotenvFile(content: string | null): Record<string, string> {
|
|
268
|
+
const vars: Record<string, string> = {};
|
|
269
|
+
if (!content) return vars;
|
|
270
|
+
for (const raw of content.split("\n")) {
|
|
271
|
+
let line = raw.trim();
|
|
272
|
+
if (line.startsWith("export ")) line = line.slice("export ".length).trim();
|
|
273
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
274
|
+
const idx = line.indexOf("=");
|
|
275
|
+
const name = line.slice(0, idx).trim();
|
|
276
|
+
let value = line.slice(idx + 1).trim();
|
|
277
|
+
if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.endsWith(value[0])) {
|
|
278
|
+
value = value.slice(1, -1);
|
|
279
|
+
} else {
|
|
280
|
+
value = value.split(" #", 1)[0].trim();
|
|
281
|
+
}
|
|
282
|
+
vars[name] = value;
|
|
283
|
+
}
|
|
284
|
+
return vars;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function quoteIfNeeded(value: string): string {
|
|
288
|
+
return /[\s#"']/.test(value) ? JSON.stringify(value) : value;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeEnvName(name: string): string {
|
|
292
|
+
return normalizeNonBlank(name, "env var name");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function validateEnvPullOptions(opts: EnvOptions = {}): Promise<void> {
|
|
296
|
+
validateAgentSelectorOptions({ agent: opts.agent, project: opts.project, dir: opts.dir });
|
|
297
|
+
const dir = path.resolve(opts.dir ?? process.cwd());
|
|
298
|
+
await validateEnvPullTarget(dir, path.join(dir, ".env"), !!opts.force);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function validateEnvDiffOptions(opts: EnvOptions = {}): Promise<void> {
|
|
302
|
+
validateAgentSelectorOptions({ agent: opts.agent, project: opts.project, dir: opts.dir });
|
|
303
|
+
await validateEnvReadDir(path.resolve(opts.dir ?? process.cwd()));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function readOptionalLocalFile(file: string, label: string): Promise<string | null> {
|
|
307
|
+
try {
|
|
308
|
+
return await fs.readFile(file, "utf8");
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
311
|
+
if (code === "ENOENT") return null;
|
|
312
|
+
if (code === "EISDIR") throw new Error(`Cannot read ${label} at ${file}: expected a file, got a directory.`);
|
|
313
|
+
throw new Error(`Cannot read ${label} at ${file}: ${(error as Error).message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function validateEnvReadDir(dir: string): Promise<void> {
|
|
318
|
+
let stat;
|
|
319
|
+
try {
|
|
320
|
+
stat = await fs.stat(dir);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
323
|
+
if (code === "ENOENT") throw new Error(`Cannot read env files in ${dir}: directory does not exist.`);
|
|
324
|
+
throw new Error(`Cannot read env files in ${dir}: ${(error as Error).message}`);
|
|
325
|
+
}
|
|
326
|
+
if (!stat.isDirectory()) {
|
|
327
|
+
throw new Error(`Cannot read env files in ${dir}: expected a directory.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function validateEnvPullTarget(dir: string, target: string, force: boolean): Promise<void> {
|
|
332
|
+
let dirStat;
|
|
333
|
+
try {
|
|
334
|
+
dirStat = await fs.stat(dir);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
337
|
+
if (code === "ENOENT") throw new Error(`Cannot write .env in ${dir}: directory does not exist.`);
|
|
338
|
+
throw new Error(`Cannot write .env in ${dir}: ${(error as Error).message}`);
|
|
339
|
+
}
|
|
340
|
+
if (!dirStat.isDirectory()) {
|
|
341
|
+
throw new Error(`Cannot write .env in ${dir}: expected a directory.`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const targetStat = await fs.stat(target);
|
|
346
|
+
if (targetStat.isDirectory()) {
|
|
347
|
+
throw new Error(`Cannot write .env to ${target}: expected a file, got a directory.`);
|
|
348
|
+
}
|
|
349
|
+
if (!force) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`${target} already exists. Re-run with --force to overwrite it (local-only keys are lost).`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function writeEnvFile(target: string, content: string): Promise<void> {
|
|
361
|
+
try {
|
|
362
|
+
await fs.writeFile(target, content, { mode: 0o600 });
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
365
|
+
if (code === "ENOENT") throw new Error(`Cannot write .env to ${target}: parent directory does not exist.`);
|
|
366
|
+
if (code === "EISDIR") throw new Error(`Cannot write .env to ${target}: expected a file, got a directory.`);
|
|
367
|
+
throw new Error(`Cannot write .env to ${target}: ${(error as Error).message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { HttpClient } from "@dharta/sdk";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import type { CliContext } from "../context.js";
|
|
5
|
+
import { cliFormatError, cliFormatNetworkError, cliFormatRawError } from "../api-error.js";
|
|
6
|
+
import { table } from "../output.js";
|
|
7
|
+
import { validateAgentSlug } from "../agent.js";
|
|
8
|
+
|
|
9
|
+
// `dharta files ls|get <agent> <karta>` - the owner file-read channel (RFC 0060 D3/P3).
|
|
10
|
+
//
|
|
11
|
+
// Mints a short-lived `workspace_read` token OUT OF BAND from the control plane (the
|
|
12
|
+
// same mint as the git-credential helper, `scope: "workspace_read"`), then GETs the
|
|
13
|
+
// DATA-PLANE files routes directly with it - so file bytes flow only from the data plane,
|
|
14
|
+
// never through the control plane (the BYOC residency property, D3/D8). The mint response
|
|
15
|
+
// carries `files_url`, the data-plane base for this dharta.
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceEntry {
|
|
18
|
+
path: string;
|
|
19
|
+
kind: string;
|
|
20
|
+
size: number;
|
|
21
|
+
mode: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkspaceListing {
|
|
25
|
+
snapshot: string;
|
|
26
|
+
entries: WorkspaceEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FilesMint {
|
|
30
|
+
token: string;
|
|
31
|
+
files_url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function mintFilesToken(ctx: CliContext, agent: string, instance: string): Promise<FilesMint> {
|
|
35
|
+
const res = await ctx.http.request<{ token?: string; files_url?: string }>({
|
|
36
|
+
method: "POST",
|
|
37
|
+
path: `/api/agents/${encodeURIComponent(agent)}/workspace_tokens`,
|
|
38
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
39
|
+
body: { instance, scope: "workspace_read" },
|
|
40
|
+
});
|
|
41
|
+
if (!res.token) {
|
|
42
|
+
throw new Error("The Dharta server did not return a workspace token.");
|
|
43
|
+
}
|
|
44
|
+
if (!res.files_url) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"This Dharta server does not expose the files API (files_url missing). Upgrade the server (RFC 0060).",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return { token: res.token, files_url: res.files_url };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A data-plane HttpClient bound to the files host, authenticated with the minted token.
|
|
53
|
+
// Returns the client plus the base path from `files_url` (`/workspace/<agent>/<karta>`).
|
|
54
|
+
function filesClient(ctx: CliContext, mint: FilesMint): { http: HttpClient; basePath: string } {
|
|
55
|
+
const url = new URL(mint.files_url);
|
|
56
|
+
const http = new HttpClient({
|
|
57
|
+
apiKey: mint.token,
|
|
58
|
+
baseUrl: url.origin,
|
|
59
|
+
fetch: ctx.fetch,
|
|
60
|
+
formatError: cliFormatError,
|
|
61
|
+
formatNetworkError: cliFormatNetworkError,
|
|
62
|
+
});
|
|
63
|
+
return { http, basePath: url.pathname };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeInstance(instance: string): string {
|
|
67
|
+
const trimmed = instance.trim();
|
|
68
|
+
if (!trimmed) throw new Error("A dharta id is required.");
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function filesLsData(
|
|
73
|
+
ctx: CliContext,
|
|
74
|
+
agent: string,
|
|
75
|
+
instance: string,
|
|
76
|
+
): Promise<WorkspaceListing> {
|
|
77
|
+
const slug = validateAgentSlug(agent, "agent slug");
|
|
78
|
+
const karta = normalizeInstance(instance);
|
|
79
|
+
const mint = await mintFilesToken(ctx, slug, karta);
|
|
80
|
+
const { http, basePath } = filesClient(ctx, mint);
|
|
81
|
+
const listing = await http.request<WorkspaceListing>({ path: basePath });
|
|
82
|
+
return {
|
|
83
|
+
snapshot: typeof listing.snapshot === "string" ? listing.snapshot : "",
|
|
84
|
+
entries: Array.isArray(listing.entries) ? listing.entries : [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function filesLs(ctx: CliContext, agent: string, instance: string): Promise<string> {
|
|
89
|
+
const listing = await filesLsData(ctx, agent, instance);
|
|
90
|
+
if (listing.entries.length === 0) {
|
|
91
|
+
return `No files in ${agent}/${instance} (snapshot ${listing.snapshot || "—"}).`;
|
|
92
|
+
}
|
|
93
|
+
const rows = [...listing.entries]
|
|
94
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
95
|
+
.map((e) => ({ path: e.path, size: String(e.size) }));
|
|
96
|
+
return table(rows);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fetch one file's bytes. Writes to `out` (a status line is returned) or, when `out` is
|
|
100
|
+
// omitted, streams the raw bytes to stdout (returns null so nothing else is printed - a
|
|
101
|
+
// trailing newline would corrupt binary output).
|
|
102
|
+
export async function filesGet(
|
|
103
|
+
ctx: CliContext,
|
|
104
|
+
agent: string,
|
|
105
|
+
instance: string,
|
|
106
|
+
path: string,
|
|
107
|
+
out?: string,
|
|
108
|
+
): Promise<string | null> {
|
|
109
|
+
const slug = validateAgentSlug(agent, "agent slug");
|
|
110
|
+
const karta = normalizeInstance(instance);
|
|
111
|
+
const filePath = path.trim();
|
|
112
|
+
if (!filePath) throw new Error("A file path is required.");
|
|
113
|
+
const mint = await mintFilesToken(ctx, slug, karta);
|
|
114
|
+
const { http, basePath } = filesClient(ctx, mint);
|
|
115
|
+
const res = await http.raw(`${basePath}/file?path=${encodeURIComponent(filePath)}`);
|
|
116
|
+
if (!res.ok) throw await cliFormatRawError(res);
|
|
117
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
118
|
+
if (out) {
|
|
119
|
+
await writeFile(out, bytes);
|
|
120
|
+
return `Wrote ${bytes.length} bytes to ${out}.`;
|
|
121
|
+
}
|
|
122
|
+
process.stdout.write(bytes);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { HttpClient } from "@dharta/sdk";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { activeProfile, type Profile } from "../config.js";
|
|
4
|
+
|
|
5
|
+
// `dharta git-credential <operation>` - a git credential helper in the protocol sense,
|
|
6
|
+
// wired by `dharta login` via `credential.<git host>.helper = !dharta git-credential`.
|
|
7
|
+
//
|
|
8
|
+
// Two hosts, two auth models (RFC 0043 D8):
|
|
9
|
+
// - the RFC 0011 PUSH host (git_base_url): answer with the stored API key (the server
|
|
10
|
+
// ignores the username, "dharta" by convention).
|
|
11
|
+
// - the workspace CLONE host (workspace_git_base_url, the DATA PLANE): mint a per-clone
|
|
12
|
+
// read-only token OUT OF BAND (a metadata call to the control plane) and answer with
|
|
13
|
+
// it, so the clone bytes flow only from the data plane and never through the control
|
|
14
|
+
// plane. `dharta login` sets `useHttpPath=true` for this host so git passes the repo
|
|
15
|
+
// path, from which we read the karta id.
|
|
16
|
+
//
|
|
17
|
+
// store/erase are no-ops: the API key already lives in ~/.dharta/config.toml and the
|
|
18
|
+
// minted token is short-lived (git never persists this secret).
|
|
19
|
+
export async function gitCredential(
|
|
20
|
+
operation: string,
|
|
21
|
+
stdin: string,
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
if (operation !== "get") return "";
|
|
24
|
+
|
|
25
|
+
const profile = await activeProfile();
|
|
26
|
+
|
|
27
|
+
const attrs = new Map<string, string>();
|
|
28
|
+
for (const line of stdin.split("\n")) {
|
|
29
|
+
const idx = line.indexOf("=");
|
|
30
|
+
if (idx > 0) attrs.set(line.slice(0, idx), line.slice(idx + 1));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const protocol = attrs.get("protocol");
|
|
34
|
+
if (protocol !== "https" && protocol !== "http") return "";
|
|
35
|
+
const host = attrs.get("host");
|
|
36
|
+
if (!host) return "";
|
|
37
|
+
|
|
38
|
+
const wsBase = profile.workspace_git_base_url;
|
|
39
|
+
if (wsBase && host === new URL(wsBase).host) {
|
|
40
|
+
const token = await mintWorkspaceToken(profile, attrs.get("path") ?? "");
|
|
41
|
+
return token ? `username=karta\npassword=${token}\n` : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (profile.git_base_url && host === new URL(profile.git_base_url).host) {
|
|
45
|
+
return `username=karta\npassword=${profile.api_key}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mint a read-only workspace token for the karta named by the request path. With
|
|
52
|
+
// `useHttpPath=true`, git passes a path like `git/<agent>/<karta>` (no leading slash,
|
|
53
|
+
// possibly with extra segments we ignore). Returns null on any failure - git then falls
|
|
54
|
+
// back to its normal prompt/deny, never a crash.
|
|
55
|
+
async function mintWorkspaceToken(
|
|
56
|
+
profile: Profile,
|
|
57
|
+
path: string,
|
|
58
|
+
): Promise<string | null> {
|
|
59
|
+
const m = path.replace(/^\/+/, "").match(/^git\/([^/]+)\/([^/]+)/);
|
|
60
|
+
if (!m) return null;
|
|
61
|
+
const [, agent, instance] = m;
|
|
62
|
+
|
|
63
|
+
const http = new HttpClient({
|
|
64
|
+
apiKey: profile.api_key,
|
|
65
|
+
baseUrl: profile.base_url,
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
const res = await http.request<{ token?: string }>({
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: `/api/agents/${encodeURIComponent(agent)}/workspace_tokens`,
|
|
71
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
72
|
+
body: { instance },
|
|
73
|
+
});
|
|
74
|
+
return res.token ?? null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|