@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,94 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { CliContext } from "../context.js";
|
|
3
|
+
import { findGitRoot, scanAgentFolders } from "../setup-scan.js";
|
|
4
|
+
import { deriveSlug, validateAgentSelectorOptions } from "../agent.js";
|
|
5
|
+
import { table } from "../output.js";
|
|
6
|
+
|
|
7
|
+
// `dharta status` (CLI surface spec): the repo-wide "what's here" view - every
|
|
8
|
+
// agent declared in this repo (resolved from the git root down, like `git
|
|
9
|
+
// status`) joined to its live release on the active profile. Works before the
|
|
10
|
+
// first deploy (the local columns come from dharta.toml; the remote ones fill
|
|
11
|
+
// in once live). Absorbs the old `deploy list`. For one agent's detail, see
|
|
12
|
+
// `dharta agent show`.
|
|
13
|
+
|
|
14
|
+
export interface StatusRow {
|
|
15
|
+
name: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
enabled: boolean; // deploy = true locally
|
|
18
|
+
deployMethod: "git" | "folder" | null;
|
|
19
|
+
liveVersion: number | null; // null = no active release on this profile
|
|
20
|
+
url: string | null; // live hosted page
|
|
21
|
+
remoteChecked: boolean; // false when status is local-only because auth is unavailable
|
|
22
|
+
remoteUnavailableReason: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AgentRemote {
|
|
26
|
+
// GET /api/agents/:slug reports only the active release as `live_version`
|
|
27
|
+
// (null before the first successful deploy). It does not expose the newest
|
|
28
|
+
// deployment attempt, so this command deliberately describes live state.
|
|
29
|
+
live_version: number | null;
|
|
30
|
+
agent_url: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function statusData(
|
|
34
|
+
ctx: CliContext | null,
|
|
35
|
+
opts: { dir?: string; authMessage?: string } = {},
|
|
36
|
+
): Promise<StatusRow[]> {
|
|
37
|
+
validateAgentSelectorOptions(opts);
|
|
38
|
+
const dir = path.resolve(opts.dir ?? ".");
|
|
39
|
+
const gitRoot = findGitRoot(dir);
|
|
40
|
+
const folders = await scanAgentFolders(gitRoot ?? dir, { isGitRepo: !!gitRoot });
|
|
41
|
+
|
|
42
|
+
const rows: StatusRow[] = [];
|
|
43
|
+
for (const f of folders) {
|
|
44
|
+
if (!f.name) continue; // configured agents only (a dharta.toml with a name)
|
|
45
|
+
const slug = deriveSlug(f.name);
|
|
46
|
+
let remote: AgentRemote | null = null;
|
|
47
|
+
if (ctx) {
|
|
48
|
+
try {
|
|
49
|
+
remote = await ctx.http.request<AgentRemote>({
|
|
50
|
+
path: `/api/agents/${encodeURIComponent(slug)}`,
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// 404 = not deployed on the active profile (not an error); anything else
|
|
54
|
+
// (auth, network) is real and must surface.
|
|
55
|
+
if ((err as { status?: number }).status !== 404) throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
rows.push({
|
|
59
|
+
name: f.name,
|
|
60
|
+
slug,
|
|
61
|
+
enabled: f.deployEnabled,
|
|
62
|
+
deployMethod: f.deployMethod ?? null,
|
|
63
|
+
liveVersion: remote?.live_version ?? null,
|
|
64
|
+
url: remote?.agent_url ?? null,
|
|
65
|
+
remoteChecked: !!ctx,
|
|
66
|
+
remoteUnavailableReason: ctx
|
|
67
|
+
? null
|
|
68
|
+
: opts.authMessage ?? "Run `dharta login` to check live versions and URLs.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return rows;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function status(ctx: CliContext | null, opts: { dir?: string; authMessage?: string } = {}): Promise<string> {
|
|
75
|
+
const rows = await statusData(ctx, opts);
|
|
76
|
+
if (rows.length === 0) {
|
|
77
|
+
return "No agents here (no dharta.toml with a name). Create one with `dharta create`, or run `dharta setup` inside an existing agent folder.";
|
|
78
|
+
}
|
|
79
|
+
const rendered = table(
|
|
80
|
+
rows.map((r) => ({
|
|
81
|
+
agent: r.name,
|
|
82
|
+
enabled: r.enabled ? "yes" : "no",
|
|
83
|
+
method: r.deployMethod ?? "-",
|
|
84
|
+
live:
|
|
85
|
+
r.liveVersion == null
|
|
86
|
+
? "-"
|
|
87
|
+
: `v${r.liveVersion}`,
|
|
88
|
+
url: r.url ?? "-",
|
|
89
|
+
})),
|
|
90
|
+
);
|
|
91
|
+
if (ctx) return rendered;
|
|
92
|
+
const message = opts.authMessage ?? "Run `dharta login` to check live versions and URLs.";
|
|
93
|
+
return `${rendered}\n\nRemote live release unavailable: ${message}`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// `dharta update` (RFC 0013 CLI-11): self-update on the STANDALONE
|
|
2
|
+
// channel only - npm installs are updated by npm, and the two must
|
|
3
|
+
// never fight over the same files. A standalone install is identified
|
|
4
|
+
// by the `.dharta-standalone` marker the tarball build writes next to
|
|
5
|
+
// the package root.
|
|
6
|
+
|
|
7
|
+
import { promises as fs } from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { checkNpmLatestVersion } from "../version-check.js";
|
|
14
|
+
|
|
15
|
+
// Where the standalone tarballs live; overridable for staging/self-host.
|
|
16
|
+
export const DOWNLOAD_BASE =
|
|
17
|
+
process.env.DHARTA_CLI_DOWNLOAD_BASE ?? "https://cli.dharta.ai";
|
|
18
|
+
|
|
19
|
+
export interface UpdateOptions {
|
|
20
|
+
installRoot?: string; // test seam (default: the package root)
|
|
21
|
+
currentVersion?: string;
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
latestUrl?: string;
|
|
24
|
+
platform?: string; // test seam (default: process.platform)
|
|
25
|
+
arch?: string; // test seam (default: process.arch)
|
|
26
|
+
log?: (line: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function packageRoot(): string {
|
|
30
|
+
// src/commands/update.ts -> dist/commands/update.js; the package
|
|
31
|
+
// root is two levels up either way.
|
|
32
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function installChannel(root: string): Promise<"standalone" | "npm"> {
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(path.join(root, ".dharta-standalone"));
|
|
38
|
+
return "standalone";
|
|
39
|
+
} catch {
|
|
40
|
+
return "npm";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function tarballName(platform: string, arch: string): string {
|
|
45
|
+
return `karta-${platform}-${arch}.tar.gz`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function update(opts: UpdateOptions = {}): Promise<string> {
|
|
49
|
+
const log = opts.log ?? ((line: string) => console.log(line));
|
|
50
|
+
const root = opts.installRoot ?? packageRoot();
|
|
51
|
+
|
|
52
|
+
if ((await installChannel(root)) === "npm") {
|
|
53
|
+
const version = await checkNpmLatestVersion({
|
|
54
|
+
root,
|
|
55
|
+
currentVersion: opts.currentVersion,
|
|
56
|
+
fetchImpl: opts.fetchImpl,
|
|
57
|
+
latestUrl: opts.latestUrl,
|
|
58
|
+
});
|
|
59
|
+
if (version.status === "current") return `karta is already current (${version.current}).`;
|
|
60
|
+
if (version.status === "ahead") {
|
|
61
|
+
return `dharta ${version.current} is newer than the latest published npm version (${version.latest}).`;
|
|
62
|
+
}
|
|
63
|
+
const suffix =
|
|
64
|
+
version.status === "outdated"
|
|
65
|
+
? `\nInstalled: ${version.current}\nLatest: ${version.latest}`
|
|
66
|
+
: `\nCould not check the latest npm version (${version.reason}).`;
|
|
67
|
+
return (
|
|
68
|
+
"This karta was installed via npm - update it with:\n" +
|
|
69
|
+
" npm install -g @dharta/cli@latest" +
|
|
70
|
+
suffix
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const platform = opts.platform ?? process.platform;
|
|
75
|
+
const arch = opts.arch ?? process.arch;
|
|
76
|
+
const url = `${DOWNLOAD_BASE.replace(/\/+$/, "")}/latest/${tarballName(platform, arch)}`;
|
|
77
|
+
const fetchImpl = (opts.fetchImpl ?? globalThis.fetch).bind(globalThis);
|
|
78
|
+
|
|
79
|
+
log(`Downloading ${url}…`);
|
|
80
|
+
const res = await fetchUpdate(url, fetchImpl);
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Could not download the update (HTTP ${res.status} from ${url}). ` +
|
|
84
|
+
"Check your network, or reinstall with the install script.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
88
|
+
|
|
89
|
+
// Integrity: the build publishes a .sha256 sidecar next to every
|
|
90
|
+
// tarball; a mismatch means a truncated or tampered download and
|
|
91
|
+
// must never reach the extract step. A missing sidecar is tolerated
|
|
92
|
+
// (older releases) but logged.
|
|
93
|
+
const sumRes = await fetchImpl(`${url}.sha256`).catch(() => null);
|
|
94
|
+
if (sumRes?.ok) {
|
|
95
|
+
const expected = (await sumRes.text()).trim().split(/\s+/)[0];
|
|
96
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
97
|
+
if (expected && expected !== actual) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Update checksum mismatch - the download is corrupt or tampered. " +
|
|
100
|
+
"Nothing was changed; retry, or reinstall with the install script.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
log("No checksum published for this build - skipping verification.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const staging = await fs.mkdtemp(path.join(os.tmpdir(), "dharta-update-"));
|
|
108
|
+
const tarball = path.join(staging, "update.tar.gz");
|
|
109
|
+
await fs.writeFile(tarball, bytes);
|
|
110
|
+
await untar(tarball, staging);
|
|
111
|
+
|
|
112
|
+
// The tarball contains a single karta/ directory; its contents
|
|
113
|
+
// replace the install root. Extract fully BEFORE touching the live
|
|
114
|
+
// install so a failed download never leaves a half-updated CLI.
|
|
115
|
+
const extracted = path.join(staging, "dharta");
|
|
116
|
+
await fs.access(path.join(extracted, ".dharta-standalone"));
|
|
117
|
+
|
|
118
|
+
await replaceContents(extracted, root);
|
|
119
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
120
|
+
return "Updated. Run `karta --version` to confirm.";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function fetchUpdate(url: string, fetchImpl: typeof fetch): Promise<Response> {
|
|
124
|
+
try {
|
|
125
|
+
return await fetchImpl(url);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Could not download the update from ${url}: ${(error as Error).message}. ` +
|
|
129
|
+
"Check your network, or reinstall with the install script.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function replaceContents(from: string, to: string): Promise<void> {
|
|
135
|
+
for (const entry of await fs.readdir(from)) {
|
|
136
|
+
const target = path.join(to, entry);
|
|
137
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
138
|
+
await fs.cp(path.join(from, entry), target, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function untar(tarball: string, dest: string): Promise<void> {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const proc = spawn("tar", ["-xzf", tarball, "-C", dest], { stdio: "ignore" });
|
|
145
|
+
proc.on("error", () => reject(new Error("Could not extract the update (is `tar` installed?).")));
|
|
146
|
+
proc.on("close", (code) =>
|
|
147
|
+
code === 0
|
|
148
|
+
? resolve()
|
|
149
|
+
: reject(
|
|
150
|
+
new Error(
|
|
151
|
+
`tar exited ${code} extracting the update. Re-run \`dharta update\`; ` +
|
|
152
|
+
"reinstall with the install script if it persists.",
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CliContext } from "../context.js";
|
|
2
|
+
|
|
3
|
+
export async function usageData(ctx: CliContext) {
|
|
4
|
+
return ctx.sdk.usage.summary();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function usage(ctx: CliContext): Promise<string> {
|
|
8
|
+
const summary = await usageData(ctx);
|
|
9
|
+
const t = summary.totals ?? {};
|
|
10
|
+
const lines: string[] = [];
|
|
11
|
+
lines.push("Usage totals:");
|
|
12
|
+
const entries = Object.entries(t).filter(([, v]) => v !== undefined);
|
|
13
|
+
if (entries.length === 0) {
|
|
14
|
+
lines.push(" no usage recorded yet");
|
|
15
|
+
lines.push(" Generate usage with: dharta send \"hello\" --agent <slug>");
|
|
16
|
+
}
|
|
17
|
+
for (const [k, v] of entries) {
|
|
18
|
+
if (v === undefined) continue;
|
|
19
|
+
lines.push(` ${formatUsageKey(k)}: ${formatUsageValue(k, v)}`);
|
|
20
|
+
}
|
|
21
|
+
if (summary.budget) {
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push("Budget:");
|
|
24
|
+
lines.push(` period: ${summary.budget.period_start} → ${summary.budget.period_end}`);
|
|
25
|
+
const cap = summary.budget.monthly_cap_cents;
|
|
26
|
+
lines.push(` monthly_cap: ${cap == null ? "unlimited" : formatCents(cap)}`);
|
|
27
|
+
}
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatUsageKey(key: string): string {
|
|
32
|
+
return key.endsWith("_cents") ? key.slice(0, -"_cents".length) : key;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatUsageValue(key: string, value: number): string {
|
|
36
|
+
return key.endsWith("_cents") ? formatCents(value) : String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatCents(cents: number): string {
|
|
40
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { CliContext } from "../context.js";
|
|
3
|
+
import { WEBHOOK_EVENT_TYPES } from "@dharta/sdk";
|
|
4
|
+
import { fmtTs, table } from "../output.js";
|
|
5
|
+
import { closestChoice, normalizeNonBlank } from "../validators.js";
|
|
6
|
+
|
|
7
|
+
export interface WebhookEndpoint {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
url: string;
|
|
11
|
+
event_types: string[];
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
disabled_at: string | null;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ListEnvelope<T> {
|
|
19
|
+
data: T[];
|
|
20
|
+
event_types?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EndpointEnvelope {
|
|
24
|
+
webhook_endpoint: WebhookEndpoint;
|
|
25
|
+
secret?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WebhookOptions {
|
|
29
|
+
event?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function webhooksListData(ctx: CliContext): Promise<WebhookEndpoint[]> {
|
|
33
|
+
const res = await ctx.http.request<ListEnvelope<WebhookEndpoint>>({ path: "/api/webhook_endpoints" });
|
|
34
|
+
return res.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function webhooksList(ctx: CliContext): Promise<string> {
|
|
38
|
+
const data = await webhooksListData(ctx);
|
|
39
|
+
if (data.length === 0) return "No webhook endpoints. Create one with: dharta webhooks create <name> <url>";
|
|
40
|
+
return table(data.map((endpoint) => ({
|
|
41
|
+
id: endpoint.id,
|
|
42
|
+
name: endpoint.name,
|
|
43
|
+
url: endpoint.url,
|
|
44
|
+
state: endpoint.enabled ? "enabled" : "disabled",
|
|
45
|
+
events: endpoint.event_types.join(",") || "all",
|
|
46
|
+
created: fmtTs(endpoint.created_at),
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function webhooksCreateData(
|
|
51
|
+
ctx: CliContext,
|
|
52
|
+
name: string,
|
|
53
|
+
url: string,
|
|
54
|
+
opts: WebhookOptions,
|
|
55
|
+
): Promise<EndpointEnvelope> {
|
|
56
|
+
validateWebhooksCreateInput(name, url, opts);
|
|
57
|
+
const normalizedName = normalizeRequired(name, "Webhook name");
|
|
58
|
+
const normalizedUrl = normalizeWebhookUrl(url);
|
|
59
|
+
return ctx.http.request<EndpointEnvelope>({
|
|
60
|
+
method: "POST",
|
|
61
|
+
path: "/api/webhook_endpoints",
|
|
62
|
+
headers: idempotencyHeader(),
|
|
63
|
+
body: { name: normalizedName, url: normalizedUrl, event_types: parseEvents(opts.event) },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function webhooksCreate(ctx: CliContext, name: string, url: string, opts: WebhookOptions): Promise<string> {
|
|
68
|
+
const res = await webhooksCreateData(ctx, name, url, opts);
|
|
69
|
+
return [
|
|
70
|
+
`Created webhook endpoint ${res.webhook_endpoint.id}.`,
|
|
71
|
+
"Secret (shown once - configure your verifier now):",
|
|
72
|
+
res.secret ?? "",
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function webhooksUpdateData(
|
|
77
|
+
ctx: CliContext,
|
|
78
|
+
id: string,
|
|
79
|
+
opts: { name?: string; url?: string; event?: string[] },
|
|
80
|
+
): Promise<EndpointEnvelope> {
|
|
81
|
+
validateWebhooksUpdateInput(id, opts);
|
|
82
|
+
const normalizedId = normalizeWebhookId(id);
|
|
83
|
+
const body: Record<string, unknown> = {};
|
|
84
|
+
if (opts.name !== undefined) body.name = normalizeRequired(opts.name, "Webhook name");
|
|
85
|
+
if (opts.url !== undefined) body.url = normalizeWebhookUrl(opts.url);
|
|
86
|
+
if (opts.event !== undefined) body.event_types = parseEvents(opts.event);
|
|
87
|
+
if (Object.keys(body).length === 0) {
|
|
88
|
+
throw new Error("Pass at least one update option: --name, --url, or --event.");
|
|
89
|
+
}
|
|
90
|
+
return ctx.http.request<EndpointEnvelope>({
|
|
91
|
+
method: "PATCH",
|
|
92
|
+
path: `/api/webhook_endpoints/${encodeURIComponent(normalizedId)}`,
|
|
93
|
+
headers: idempotencyHeader(),
|
|
94
|
+
body,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function webhooksUpdate(ctx: CliContext, id: string, opts: { name?: string; url?: string; event?: string[] }): Promise<string> {
|
|
99
|
+
const res = await webhooksUpdateData(ctx, id, opts);
|
|
100
|
+
return `Updated webhook endpoint ${res.webhook_endpoint.id}.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function validateWebhooksCreateInput(name: string, url: string, opts: WebhookOptions): void {
|
|
104
|
+
normalizeRequired(name, "Webhook name");
|
|
105
|
+
normalizeWebhookUrl(url);
|
|
106
|
+
parseEvents(opts.event);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function validateWebhooksUpdateInput(id: string, opts: { name?: string; url?: string; event?: string[] }): void {
|
|
110
|
+
normalizeWebhookId(id);
|
|
111
|
+
const hasName = opts.name !== undefined;
|
|
112
|
+
const hasUrl = opts.url !== undefined;
|
|
113
|
+
const hasEvent = opts.event !== undefined;
|
|
114
|
+
if (hasName) normalizeRequired(opts.name!, "Webhook name");
|
|
115
|
+
if (hasUrl) normalizeWebhookUrl(opts.url!);
|
|
116
|
+
if (hasEvent) parseEvents(opts.event);
|
|
117
|
+
if (!hasName && !hasUrl && !hasEvent) {
|
|
118
|
+
throw new Error("Pass at least one update option: --name, --url, or --event.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function webhooksEnableData(ctx: CliContext, id: string): Promise<EndpointEnvelope> {
|
|
123
|
+
const normalizedId = normalizeWebhookId(id);
|
|
124
|
+
return ctx.http.request<EndpointEnvelope>({
|
|
125
|
+
method: "POST",
|
|
126
|
+
path: `/api/webhook_endpoints/${encodeURIComponent(normalizedId)}/enable`,
|
|
127
|
+
headers: idempotencyHeader(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function webhooksEnable(ctx: CliContext, id: string): Promise<string> {
|
|
132
|
+
const res = await webhooksEnableData(ctx, id);
|
|
133
|
+
return `Enabled webhook endpoint ${res.webhook_endpoint.id}.`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function webhooksDeleteData(ctx: CliContext, id: string): Promise<{ id: string; deleted: boolean }> {
|
|
137
|
+
const normalizedId = normalizeWebhookId(id);
|
|
138
|
+
await ctx.http.request<void>({
|
|
139
|
+
method: "DELETE",
|
|
140
|
+
path: `/api/webhook_endpoints/${encodeURIComponent(normalizedId)}`,
|
|
141
|
+
headers: idempotencyHeader(),
|
|
142
|
+
});
|
|
143
|
+
return { id: normalizedId, deleted: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function webhooksDelete(ctx: CliContext, id: string): Promise<string> {
|
|
147
|
+
const res = await webhooksDeleteData(ctx, id);
|
|
148
|
+
return `Deleted webhook endpoint ${res.id}.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function parseEvents(input: string[] | undefined): string[] {
|
|
152
|
+
if (input === undefined) return [];
|
|
153
|
+
const events = input
|
|
154
|
+
.flatMap((value) => value.split(/[,\s]+/))
|
|
155
|
+
.map((value) => value.trim())
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
if (events.length === 0) {
|
|
158
|
+
throw new Error("--event must include at least one event type, e.g. --event turn.completed.");
|
|
159
|
+
}
|
|
160
|
+
const allowed = new Set<string>(WEBHOOK_EVENT_TYPES);
|
|
161
|
+
const unknown = events.filter((event) => !allowed.has(event));
|
|
162
|
+
if (unknown.length > 0) {
|
|
163
|
+
const suggestion = unknown.length === 1 ? closestChoice(unknown[0]!, WEBHOOK_EVENT_TYPES) : null;
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Unknown --event ${unknown.join(", ")}. Allowed events: ${WEBHOOK_EVENT_TYPES.join(", ")}.` +
|
|
166
|
+
(suggestion ? ` Did you mean ${suggestion}?` : ""),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return events;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function idempotencyHeader(): Record<string, string> {
|
|
173
|
+
return { "Idempotency-Key": randomUUID() };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeRequired(value: string, label: string): string {
|
|
177
|
+
const normalized = value.trim();
|
|
178
|
+
if (!normalized) {
|
|
179
|
+
throw new Error(`${label} cannot be blank.`);
|
|
180
|
+
}
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeWebhookUrl(value: string): string {
|
|
185
|
+
const normalized = normalizeRequired(value, "Webhook URL");
|
|
186
|
+
let parsed: URL;
|
|
187
|
+
try {
|
|
188
|
+
parsed = new URL(normalized);
|
|
189
|
+
} catch {
|
|
190
|
+
throw new Error("Webhook URL must be an absolute http(s) URL.");
|
|
191
|
+
}
|
|
192
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
193
|
+
throw new Error("Webhook URL must be an absolute http(s) URL.");
|
|
194
|
+
}
|
|
195
|
+
return normalized;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeWebhookId(id: string): string {
|
|
199
|
+
return normalizeNonBlank(id, "Webhook endpoint id");
|
|
200
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CliContext } from "../context.js";
|
|
2
|
+
import type { Me } from "./auth.js";
|
|
3
|
+
import { formatApiKeyExpiry, formatScopes } from "./scopes.js";
|
|
4
|
+
|
|
5
|
+
export interface WhoamiData extends Me {
|
|
6
|
+
host: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function whoamiData(ctx: CliContext): Promise<WhoamiData> {
|
|
10
|
+
const me = await ctx.http.request<Me>({ path: "/me" });
|
|
11
|
+
return { ...me, host: ctx.baseUrl };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function whoami(ctx: CliContext): Promise<string> {
|
|
15
|
+
const me = await whoamiData(ctx);
|
|
16
|
+
const lines = [
|
|
17
|
+
`Organization: ${me.organization.name} (${me.organization.slug})`,
|
|
18
|
+
`Key: ${me.api_key.name} · ${me.api_key.prefix}…`,
|
|
19
|
+
`Scopes: ${formatScopes(me.api_key.scopes)}`,
|
|
20
|
+
`Expiry: ${formatApiKeyExpiry(me.api_key.expires_at)}`,
|
|
21
|
+
`Host: ${me.host}`,
|
|
22
|
+
];
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// `dharta completion <bash|zsh>` (RFC 0013 CLI-12): shell completion
|
|
2
|
+
// scripts generated statically by walking the commander program - no
|
|
3
|
+
// runtime dependency, no shelling out. The output is a snapshot of this
|
|
4
|
+
// CLI version's command surface; regenerate after updating.
|
|
5
|
+
|
|
6
|
+
import type { Argument, Command, Option } from "commander";
|
|
7
|
+
|
|
8
|
+
interface CommandSpec {
|
|
9
|
+
name: string;
|
|
10
|
+
aliases: string[];
|
|
11
|
+
description: string;
|
|
12
|
+
// Long option flags (--json, --dir, ...) plus --help.
|
|
13
|
+
options: string[];
|
|
14
|
+
// Positional-argument choices (e.g. completion's bash|zsh) - offered
|
|
15
|
+
// as completion words alongside the options.
|
|
16
|
+
argChoices: string[];
|
|
17
|
+
subs: CommandSpec[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function specFromCommand(cmd: Command): CommandSpec {
|
|
21
|
+
return {
|
|
22
|
+
name: cmd.name(),
|
|
23
|
+
aliases: commandAliases(cmd),
|
|
24
|
+
description: sanitize(cmd.description()),
|
|
25
|
+
options: [
|
|
26
|
+
...cmd.options.filter((o: Option) => !o.hidden && o.long).map((o: Option) => o.long as string),
|
|
27
|
+
"--help",
|
|
28
|
+
],
|
|
29
|
+
argChoices: cmd.registeredArguments.flatMap((a: Argument) => a.argChoices ?? []),
|
|
30
|
+
subs: cmd.commands
|
|
31
|
+
.filter((c) => !(c as unknown as { _hidden?: boolean })._hidden)
|
|
32
|
+
.map((c) => specFromCommand(c as Command)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// First sentence, single line. Shell-specific escaping happens when rendering
|
|
37
|
+
// so meaningful tokens like audit_log:export keep their punctuation.
|
|
38
|
+
function sanitize(description: string): string {
|
|
39
|
+
const firstSentence = description.replace(/\s+/g, " ").trim().split(". ")[0];
|
|
40
|
+
return firstSentence.replace(/\.$/, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function words(spec: CommandSpec): string {
|
|
44
|
+
return [...subcommandWords(spec), ...spec.argChoices, ...spec.options].join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function subcommandWords(spec: CommandSpec): string[] {
|
|
48
|
+
return spec.subs.flatMap((sub) => commandNames(sub));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function commandNames(spec: CommandSpec): string[] {
|
|
52
|
+
return [spec.name, ...spec.aliases];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function commandAliases(cmd: Command): string[] {
|
|
56
|
+
const maybeAliases = (cmd as unknown as { aliases?: () => string[] }).aliases;
|
|
57
|
+
return typeof maybeAliases === "function" ? maybeAliases.call(cmd) : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CompletionEntry {
|
|
61
|
+
path: string;
|
|
62
|
+
words: string;
|
|
63
|
+
describedCommands: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function completionEntries(root: CommandSpec): CompletionEntry[] {
|
|
67
|
+
const entries: CompletionEntry[] = [];
|
|
68
|
+
const visit = (spec: CommandSpec, paths: string[]): void => {
|
|
69
|
+
for (const path of paths) {
|
|
70
|
+
entries.push({
|
|
71
|
+
path,
|
|
72
|
+
words: words(spec),
|
|
73
|
+
describedCommands: describedSubcommands(spec),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const child of spec.subs) {
|
|
78
|
+
const childPaths = expandPaths(paths, commandNames(child));
|
|
79
|
+
visit(child, childPaths);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
visit(root, [""]);
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function expandPaths(prefixes: string[], names: string[]): string[] {
|
|
88
|
+
return prefixes.flatMap((prefix) => names.map((name) => (prefix ? `${prefix} ${name}` : name)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function describedSubcommands(spec: CommandSpec): string {
|
|
92
|
+
return spec.subs.flatMap((sub) => [
|
|
93
|
+
`'${escapeZshDescription(sub.name)}:${escapeZshDescription(sub.description)}'`,
|
|
94
|
+
...sub.aliases.map((alias) => `'${escapeZshDescription(alias)}:Alias of ${escapeZshDescription(sub.name)}'`),
|
|
95
|
+
]).join(" ");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function escapeBashCase(value: string): string {
|
|
99
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function escapeZshCase(value: string): string {
|
|
103
|
+
return value.replace(/\\/g, "\\\\").replace(/ /g, "\\ ").replace(/\)/g, "\\)");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function zshCasePattern(value: string): string {
|
|
107
|
+
return value === "" ? '""' : escapeZshCase(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function escapeZshDescription(value: string): string {
|
|
111
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "'\\''").replace(/:/g, "\\:");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function bashCompletion(program: Command): string {
|
|
115
|
+
const spec = specFromCommand(program);
|
|
116
|
+
const entries = completionEntries(spec);
|
|
117
|
+
const lines: string[] = [
|
|
118
|
+
"# bash completion for karta - generated by `dharta completion bash`",
|
|
119
|
+
"# Load with: source <(dharta completion bash) (or append to ~/.bash_completion)",
|
|
120
|
+
"_karta() {",
|
|
121
|
+
' local cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
122
|
+
' local path="${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}"',
|
|
123
|
+
' local words=""',
|
|
124
|
+
' case "$path" in',
|
|
125
|
+
];
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
lines.push(` "${escapeBashCase(entry.path)}") words="${escapeBashCase(entry.words)}" ;;`);
|
|
128
|
+
}
|
|
129
|
+
lines.push(
|
|
130
|
+
" esac",
|
|
131
|
+
' COMPREPLY=( $(compgen -W "$words" -- "$cur") )',
|
|
132
|
+
"}",
|
|
133
|
+
"complete -F _karta karta",
|
|
134
|
+
);
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function zshCompletion(program: Command): string {
|
|
139
|
+
const spec = specFromCommand(program);
|
|
140
|
+
const entries = completionEntries(spec);
|
|
141
|
+
|
|
142
|
+
const lines: string[] = [
|
|
143
|
+
"#compdef karta",
|
|
144
|
+
"# zsh completion for karta - generated by `dharta completion zsh`",
|
|
145
|
+
"# Install: dharta completion zsh > ~/.zfunc/_dharta (with fpath+=(~/.zfunc) before compinit)",
|
|
146
|
+
"",
|
|
147
|
+
"_karta() {",
|
|
148
|
+
" local -a reply",
|
|
149
|
+
' local path=""',
|
|
150
|
+
" if (( CURRENT > 2 )); then",
|
|
151
|
+
' path="${(j: :)words[2,$((CURRENT - 1))]}"',
|
|
152
|
+
" fi",
|
|
153
|
+
' case "$path" in',
|
|
154
|
+
];
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
lines.push(
|
|
157
|
+
` ${zshCasePattern(entry.path)})`,
|
|
158
|
+
entry.describedCommands
|
|
159
|
+
? ` reply=( ${entry.describedCommands} ); _describe -t commands 'dharta command' reply`
|
|
160
|
+
: " true",
|
|
161
|
+
entry.words ? ` compadd -- ${entry.words}` : " true",
|
|
162
|
+
" return",
|
|
163
|
+
" ;;",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
lines.push(" esac", "}", "", '_karta "$@"');
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|