@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.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/agent.d.ts +17 -0
  4. package/dist/agent.js +134 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-error.d.ts +3 -0
  7. package/dist/api-error.js +80 -0
  8. package/dist/api-error.js.map +1 -0
  9. package/dist/browser.d.ts +1 -0
  10. package/dist/browser.js +19 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/cli.d.ts +5 -0
  13. package/dist/cli.js +1978 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/commands/agents.d.ts +131 -0
  16. package/dist/commands/agents.js +439 -0
  17. package/dist/commands/agents.js.map +1 -0
  18. package/dist/commands/analytics.d.ts +25 -0
  19. package/dist/commands/analytics.js +166 -0
  20. package/dist/commands/analytics.js.map +1 -0
  21. package/dist/commands/api.d.ts +19 -0
  22. package/dist/commands/api.js +278 -0
  23. package/dist/commands/api.js.map +1 -0
  24. package/dist/commands/appearance.d.ts +19 -0
  25. package/dist/commands/appearance.js +63 -0
  26. package/dist/commands/appearance.js.map +1 -0
  27. package/dist/commands/artifact.d.ts +33 -0
  28. package/dist/commands/artifact.js +235 -0
  29. package/dist/commands/artifact.js.map +1 -0
  30. package/dist/commands/audit-log.d.ts +32 -0
  31. package/dist/commands/audit-log.js +102 -0
  32. package/dist/commands/audit-log.js.map +1 -0
  33. package/dist/commands/auth.d.ts +47 -0
  34. package/dist/commands/auth.js +117 -0
  35. package/dist/commands/auth.js.map +1 -0
  36. package/dist/commands/caps.d.ts +82 -0
  37. package/dist/commands/caps.js +213 -0
  38. package/dist/commands/caps.js.map +1 -0
  39. package/dist/commands/create.d.ts +22 -0
  40. package/dist/commands/create.js +76 -0
  41. package/dist/commands/create.js.map +1 -0
  42. package/dist/commands/deploy.d.ts +45 -0
  43. package/dist/commands/deploy.js +892 -0
  44. package/dist/commands/deploy.js.map +1 -0
  45. package/dist/commands/dev.d.ts +33 -0
  46. package/dist/commands/dev.js +655 -0
  47. package/dist/commands/dev.js.map +1 -0
  48. package/dist/commands/directories.d.ts +60 -0
  49. package/dist/commands/directories.js +83 -0
  50. package/dist/commands/directories.js.map +1 -0
  51. package/dist/commands/doctor.d.ts +13 -0
  52. package/dist/commands/doctor.js +123 -0
  53. package/dist/commands/doctor.js.map +1 -0
  54. package/dist/commands/embed-keys.d.ts +61 -0
  55. package/dist/commands/embed-keys.js +165 -0
  56. package/dist/commands/embed-keys.js.map +1 -0
  57. package/dist/commands/env.d.ts +59 -0
  58. package/dist/commands/env.js +300 -0
  59. package/dist/commands/env.js.map +1 -0
  60. package/dist/commands/files.d.ts +14 -0
  61. package/dist/commands/files.js +84 -0
  62. package/dist/commands/files.js.map +1 -0
  63. package/dist/commands/git-credential.d.ts +1 -0
  64. package/dist/commands/git-credential.js +70 -0
  65. package/dist/commands/git-credential.js.map +1 -0
  66. package/dist/commands/kartas.d.ts +101 -0
  67. package/dist/commands/kartas.js +289 -0
  68. package/dist/commands/kartas.js.map +1 -0
  69. package/dist/commands/keys.d.ts +33 -0
  70. package/dist/commands/keys.js +141 -0
  71. package/dist/commands/keys.js.map +1 -0
  72. package/dist/commands/login.d.ts +22 -0
  73. package/dist/commands/login.js +78 -0
  74. package/dist/commands/login.js.map +1 -0
  75. package/dist/commands/logs.d.ts +14 -0
  76. package/dist/commands/logs.js +89 -0
  77. package/dist/commands/logs.js.map +1 -0
  78. package/dist/commands/members.d.ts +32 -0
  79. package/dist/commands/members.js +77 -0
  80. package/dist/commands/members.js.map +1 -0
  81. package/dist/commands/model-keys.d.ts +22 -0
  82. package/dist/commands/model-keys.js +222 -0
  83. package/dist/commands/model-keys.js.map +1 -0
  84. package/dist/commands/open.d.ts +29 -0
  85. package/dist/commands/open.js +119 -0
  86. package/dist/commands/open.js.map +1 -0
  87. package/dist/commands/parity.d.ts +15 -0
  88. package/dist/commands/parity.js +73 -0
  89. package/dist/commands/parity.js.map +1 -0
  90. package/dist/commands/rollback.d.ts +21 -0
  91. package/dist/commands/rollback.js +26 -0
  92. package/dist/commands/rollback.js.map +1 -0
  93. package/dist/commands/schedules.d.ts +52 -0
  94. package/dist/commands/schedules.js +231 -0
  95. package/dist/commands/schedules.js.map +1 -0
  96. package/dist/commands/scopes.d.ts +3 -0
  97. package/dist/commands/scopes.js +21 -0
  98. package/dist/commands/scopes.js.map +1 -0
  99. package/dist/commands/sessions.d.ts +53 -0
  100. package/dist/commands/sessions.js +142 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/setup.d.ts +28 -0
  103. package/dist/commands/setup.js +949 -0
  104. package/dist/commands/setup.js.map +1 -0
  105. package/dist/commands/status.d.ts +19 -0
  106. package/dist/commands/status.js +63 -0
  107. package/dist/commands/status.js.map +1 -0
  108. package/dist/commands/update.d.ts +14 -0
  109. package/dist/commands/update.js +120 -0
  110. package/dist/commands/update.js.map +1 -0
  111. package/dist/commands/usage.d.ts +3 -0
  112. package/dist/commands/usage.js +37 -0
  113. package/dist/commands/usage.js.map +1 -0
  114. package/dist/commands/webhooks.d.ts +47 -0
  115. package/dist/commands/webhooks.js +156 -0
  116. package/dist/commands/webhooks.js.map +1 -0
  117. package/dist/commands/whoami.d.ts +7 -0
  118. package/dist/commands/whoami.js +17 -0
  119. package/dist/commands/whoami.js.map +1 -0
  120. package/dist/completions.d.ts +13 -0
  121. package/dist/completions.js +120 -0
  122. package/dist/completions.js.map +1 -0
  123. package/dist/config.d.ts +20 -0
  124. package/dist/config.js +98 -0
  125. package/dist/config.js.map +1 -0
  126. package/dist/context.d.ts +14 -0
  127. package/dist/context.js +38 -0
  128. package/dist/context.js.map +1 -0
  129. package/dist/device-flow.d.ts +9 -0
  130. package/dist/device-flow.js +127 -0
  131. package/dist/device-flow.js.map +1 -0
  132. package/dist/harness-preflight.d.ts +24 -0
  133. package/dist/harness-preflight.js +252 -0
  134. package/dist/harness-preflight.js.map +1 -0
  135. package/dist/harnesses.d.ts +11 -0
  136. package/dist/harnesses.js +112 -0
  137. package/dist/harnesses.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.js +3 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/login-offer.d.ts +9 -0
  142. package/dist/login-offer.js +36 -0
  143. package/dist/login-offer.js.map +1 -0
  144. package/dist/manifest.d.ts +15 -0
  145. package/dist/manifest.js +142 -0
  146. package/dist/manifest.js.map +1 -0
  147. package/dist/money.d.ts +1 -0
  148. package/dist/money.js +16 -0
  149. package/dist/money.js.map +1 -0
  150. package/dist/output.d.ts +2 -0
  151. package/dist/output.js +20 -0
  152. package/dist/output.js.map +1 -0
  153. package/dist/runtime.d.ts +19 -0
  154. package/dist/runtime.js +95 -0
  155. package/dist/runtime.js.map +1 -0
  156. package/dist/scaffold.d.ts +16 -0
  157. package/dist/scaffold.js +74 -0
  158. package/dist/scaffold.js.map +1 -0
  159. package/dist/setup-scan.d.ts +21 -0
  160. package/dist/setup-scan.js +127 -0
  161. package/dist/setup-scan.js.map +1 -0
  162. package/dist/templates.d.ts +14 -0
  163. package/dist/templates.js +65 -0
  164. package/dist/templates.js.map +1 -0
  165. package/dist/validators.d.ts +10 -0
  166. package/dist/validators.js +116 -0
  167. package/dist/validators.js.map +1 -0
  168. package/dist/version-check.d.ts +25 -0
  169. package/dist/version-check.js +68 -0
  170. package/dist/version-check.js.map +1 -0
  171. package/node_modules/@dharta/sdk/LICENSE +21 -0
  172. package/node_modules/@dharta/sdk/README.md +87 -0
  173. package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
  174. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
  175. package/node_modules/@dharta/sdk/dist/errors.js +51 -0
  176. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
  177. package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
  178. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
  179. package/node_modules/@dharta/sdk/dist/http.js +136 -0
  180. package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
  181. package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
  182. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
  183. package/node_modules/@dharta/sdk/dist/index.js +21 -0
  184. package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
  185. package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
  186. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
  187. package/node_modules/@dharta/sdk/dist/resources.js +150 -0
  188. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
  189. package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
  190. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
  191. package/node_modules/@dharta/sdk/dist/types.js +59 -0
  192. package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
  193. package/node_modules/@dharta/sdk/package.json +32 -0
  194. package/node_modules/@dharta/sdk/src/errors.ts +58 -0
  195. package/node_modules/@dharta/sdk/src/http.ts +166 -0
  196. package/node_modules/@dharta/sdk/src/index.ts +36 -0
  197. package/node_modules/@dharta/sdk/src/resources.ts +197 -0
  198. package/node_modules/@dharta/sdk/src/types.ts +179 -0
  199. package/package.json +38 -0
  200. package/src/agent.ts +147 -0
  201. package/src/api-error.ts +88 -0
  202. package/src/browser.ts +20 -0
  203. package/src/cli.ts +2339 -0
  204. package/src/commands/agents.ts +635 -0
  205. package/src/commands/analytics.ts +181 -0
  206. package/src/commands/api.ts +297 -0
  207. package/src/commands/appearance.ts +79 -0
  208. package/src/commands/artifact.ts +331 -0
  209. package/src/commands/audit-log.ts +125 -0
  210. package/src/commands/auth.ts +186 -0
  211. package/src/commands/caps.ts +302 -0
  212. package/src/commands/create.ts +114 -0
  213. package/src/commands/deploy.ts +1122 -0
  214. package/src/commands/dev.ts +792 -0
  215. package/src/commands/directories.ts +136 -0
  216. package/src/commands/doctor.ts +138 -0
  217. package/src/commands/embed-keys.ts +237 -0
  218. package/src/commands/env.ts +369 -0
  219. package/src/commands/files.ts +124 -0
  220. package/src/commands/git-credential.ts +78 -0
  221. package/src/commands/kartas.ts +439 -0
  222. package/src/commands/keys.ts +206 -0
  223. package/src/commands/login.ts +115 -0
  224. package/src/commands/logs.ts +111 -0
  225. package/src/commands/members.ts +109 -0
  226. package/src/commands/model-keys.ts +278 -0
  227. package/src/commands/open.ts +173 -0
  228. package/src/commands/parity.ts +96 -0
  229. package/src/commands/rollback.ts +45 -0
  230. package/src/commands/schedules.ts +290 -0
  231. package/src/commands/scopes.ts +22 -0
  232. package/src/commands/sessions.ts +225 -0
  233. package/src/commands/setup.ts +1176 -0
  234. package/src/commands/status.ts +94 -0
  235. package/src/commands/update.ts +157 -0
  236. package/src/commands/usage.ts +41 -0
  237. package/src/commands/webhooks.ts +200 -0
  238. package/src/commands/whoami.ts +24 -0
  239. package/src/completions.ts +168 -0
  240. package/src/config.ts +143 -0
  241. package/src/context.ts +59 -0
  242. package/src/device-flow.ts +174 -0
  243. package/src/harness-preflight.ts +293 -0
  244. package/src/harnesses.ts +117 -0
  245. package/src/index.ts +3 -0
  246. package/src/login-offer.ts +49 -0
  247. package/src/manifest.ts +153 -0
  248. package/src/money.ts +15 -0
  249. package/src/output.ts +22 -0
  250. package/src/runtime.ts +127 -0
  251. package/src/scaffold.ts +102 -0
  252. package/src/setup-scan.ts +154 -0
  253. package/src/templates.ts +85 -0
  254. package/src/validators.ts +124 -0
  255. package/src/version-check.ts +75 -0
  256. package/templates/blank/CLAUDE.md +21 -0
  257. package/templates/blank/dharta.toml +11 -0
  258. package/templates/faq-agent/CLAUDE.md +51 -0
  259. package/templates/faq-agent/dharta.toml +11 -0
@@ -0,0 +1,173 @@
1
+ // `dharta open [target]` (RFC 0013 CLI-5): open the hosted chat page,
2
+ // the agent console, or account handoff pages. URLs come from the
3
+ // control plane so the CLI never re-derives the host scheme.
4
+
5
+ import type { CliContext } from "../context.js";
6
+ import { openBrowser } from "../browser.js";
7
+ import { inferAgentSlug, type AgentInferOptions } from "../agent.js";
8
+
9
+ export interface OpenOptions extends AgentInferOptions {
10
+ openBrowserImpl?: (url: string) => Promise<boolean>;
11
+ }
12
+
13
+ interface ProjectShow {
14
+ agent_url: string;
15
+ console_url: string;
16
+ live_version: number | null;
17
+ project: { slug: string };
18
+ }
19
+
20
+ export interface OpenData {
21
+ slug?: string;
22
+ target: "agent" | "console" | "billing" | "security" | "api-keys" | "model-keys" | "webhooks" | "connected-accounts";
23
+ url: string;
24
+ // null until the first deploy; the agent target falls back to the
25
+ // console rather than opening a dead chat page.
26
+ live_version: number | null;
27
+ redirected_to_console: boolean;
28
+ }
29
+
30
+ const HANDOFF_TARGETS = {
31
+ billing: "/billing",
32
+ security: "/account/security",
33
+ "api-keys": "/api_keys",
34
+ "model-keys": "/model_keys",
35
+ webhooks: "/webhook_endpoints",
36
+ "connected-accounts": "/account/connected_accounts",
37
+ } as const;
38
+
39
+ export const OPEN_TARGETS_HELP = ["console", ...Object.keys(HANDOFF_TARGETS)].join(", ");
40
+ const OPEN_TARGETS = ["console", ...Object.keys(HANDOFF_TARGETS)];
41
+
42
+ export function isHandoffTarget(target: string | undefined): target is keyof typeof HANDOFF_TARGETS {
43
+ return target !== undefined && target in HANDOFF_TARGETS;
44
+ }
45
+
46
+ export function normalizeOpenTarget(target: string | undefined): string | undefined {
47
+ return target === undefined ? undefined : target.trim();
48
+ }
49
+
50
+ export function validateOpenTarget(target: string | undefined): void {
51
+ const normalizedTarget = normalizeOpenTarget(target);
52
+ if (target !== undefined && normalizedTarget === "") {
53
+ throw new Error("target cannot be blank.");
54
+ }
55
+ if (normalizedTarget !== undefined && normalizedTarget !== "console" && !isHandoffTarget(normalizedTarget)) {
56
+ const suggestion = closestTarget(normalizedTarget);
57
+ throw new Error(
58
+ `Unknown target '${normalizedTarget}'. Use one of: ${OPEN_TARGETS_HELP}.` +
59
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""),
60
+ );
61
+ }
62
+ }
63
+
64
+ function closestTarget(input: string): string | null {
65
+ let best: { target: string; distance: number } | null = null;
66
+ for (const target of OPEN_TARGETS) {
67
+ const distance = levenshtein(input, target);
68
+ if (distance <= 2 && (!best || distance < best.distance)) {
69
+ best = { target, distance };
70
+ }
71
+ }
72
+ return best?.target ?? null;
73
+ }
74
+
75
+ function levenshtein(a: string, b: string): number {
76
+ const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
77
+ const current = Array.from({ length: b.length + 1 }, () => 0);
78
+ for (let i = 1; i <= a.length; i += 1) {
79
+ current[0] = i;
80
+ for (let j = 1; j <= b.length; j += 1) {
81
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
82
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);
83
+ }
84
+ previous.splice(0, previous.length, ...current);
85
+ }
86
+ return previous[b.length];
87
+ }
88
+
89
+ export function openHandoffData(
90
+ baseUrl: string,
91
+ target: keyof typeof HANDOFF_TARGETS,
92
+ ): OpenData {
93
+ return {
94
+ target,
95
+ url: absoluteUrl(baseUrl, HANDOFF_TARGETS[target]),
96
+ live_version: null,
97
+ redirected_to_console: false,
98
+ };
99
+ }
100
+
101
+ export async function openHandoff(
102
+ baseUrl: string,
103
+ target: keyof typeof HANDOFF_TARGETS,
104
+ opts: Pick<OpenOptions, "openBrowserImpl"> = {},
105
+ ): Promise<string> {
106
+ const data = openHandoffData(baseUrl, target);
107
+ const opened = await (opts.openBrowserImpl ?? openBrowser)(data.url);
108
+ return opened ? `Opening ${data.url}` : `Open this in your browser: ${data.url}`;
109
+ }
110
+
111
+ // Resolve the URL without opening anything - the --json path prints
112
+ // this object instead of launching a browser.
113
+ export async function openData(
114
+ ctx: CliContext,
115
+ target: string | undefined,
116
+ opts: AgentInferOptions = {},
117
+ ): Promise<OpenData> {
118
+ const normalizedTarget = normalizeOpenTarget(target);
119
+ if (isHandoffTarget(normalizedTarget)) {
120
+ return openHandoffData(ctx.baseUrl, normalizedTarget);
121
+ }
122
+ validateOpenTarget(normalizedTarget);
123
+ if (normalizedTarget === "console" && !hasAgentSelector(opts)) {
124
+ return {
125
+ target: "console",
126
+ url: absoluteUrl(ctx.baseUrl, "/agents"),
127
+ live_version: null,
128
+ redirected_to_console: false,
129
+ };
130
+ }
131
+ const slug = await inferAgentSlug(opts);
132
+ const shown = await ctx.http.request<ProjectShow>({
133
+ path: `/api/agents/${encodeURIComponent(slug)}`,
134
+ });
135
+ const wantsAgent = normalizedTarget !== "console";
136
+ const noRelease = shown.live_version == null;
137
+ const redirected = wantsAgent && noRelease;
138
+ return {
139
+ slug,
140
+ target: wantsAgent && !redirected ? "agent" : "console",
141
+ url: wantsAgent && !redirected ? shown.agent_url : shown.console_url,
142
+ live_version: shown.live_version ?? null,
143
+ redirected_to_console: redirected,
144
+ };
145
+ }
146
+
147
+ export async function open(
148
+ ctx: CliContext,
149
+ target: string | undefined,
150
+ opts: OpenOptions = {},
151
+ ): Promise<string> {
152
+ const data = await openData(ctx, target, opts);
153
+ const opened = await (opts.openBrowserImpl ?? openBrowser)(data.url);
154
+ const prefix = data.redirected_to_console
155
+ ? `Nothing is deployed on ${data.slug} yet - deploy with: git push dharta main\n` +
156
+ "Opening the agent console instead.\n"
157
+ : "";
158
+ return prefix + (opened ? `Opening ${data.url}` : `Open this in your browser: ${data.url}`);
159
+ }
160
+
161
+ function hasAgentSelector(opts: AgentInferOptions): boolean {
162
+ return (
163
+ opts.agent !== undefined ||
164
+ opts.project !== undefined ||
165
+ opts.path !== undefined ||
166
+ opts.dir !== undefined ||
167
+ (opts.env ?? process.env).DHARTA_PROJECT !== undefined
168
+ );
169
+ }
170
+
171
+ function absoluteUrl(baseUrl: string, path: string): string {
172
+ return `${baseUrl.replace(/\/+$/, "")}${path}`;
173
+ }
@@ -0,0 +1,96 @@
1
+ // `dharta dev --release vN` / `--env remote` (RFC 0013 CLI-10): two
2
+ // orthogonal parity axes. --release runs the exact built tree prod
3
+ // serves (transcript-driven debugging: read the transcript with
4
+ // `dharta sessions show`, replay against the same code locally);
5
+ // --env remote binds prod env vars instead of .env. Honest limits:
6
+ // parity of code + config, not infrastructure.
7
+
8
+ import { promises as fs } from "node:fs";
9
+ import { randomUUID } from "node:crypto";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import { spawn } from "node:child_process";
13
+ import type { CliContext } from "../context.js";
14
+ import { inferAgentSlug, type AgentInferOptions } from "../agent.js";
15
+ import { parseReleaseVersion } from "../validators.js";
16
+
17
+ // Download + extract the release archive; returns the extracted dir.
18
+ export async function prepareReleaseDir(
19
+ ctx: CliContext,
20
+ releaseVersion: string,
21
+ opts: AgentInferOptions & { fetchImpl?: typeof fetch } = {},
22
+ ): Promise<{ dir: string; version: number; commitSha: string }> {
23
+ const version = parseReleaseVersion(releaseVersion, "--release");
24
+ const slug = await inferAgentSlug(opts);
25
+ const meta = await ctx.http.request<{ version: number; commit_sha: string; url: string }>({
26
+ path: `/api/agents/${encodeURIComponent(slug)}/releases/${encodeURIComponent(version)}/archive`,
27
+ });
28
+
29
+ const fetchImpl = (opts.fetchImpl ?? globalThis.fetch).bind(globalThis);
30
+ const res = await fetchReleaseArchive(meta.url, slug, fetchImpl);
31
+ if (!res.ok) {
32
+ throw new Error(
33
+ `Could not download the release archive (HTTP ${res.status}). ` +
34
+ `Check the version with \`dharta agent releases ${slug}\` and retry.`,
35
+ );
36
+ }
37
+ const bytes = Buffer.from(await res.arrayBuffer());
38
+
39
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), `karta-${slug}-v${meta.version}-`));
40
+ const tarball = path.join(dir, "release.tar.gz");
41
+ await fs.writeFile(tarball, bytes);
42
+ await untar(tarball, dir);
43
+ await fs.rm(tarball, { force: true });
44
+
45
+ return { dir, version: meta.version, commitSha: meta.commit_sha };
46
+ }
47
+
48
+ // Fetch prod env bindings for --env remote. Secrets ride the same D6
49
+ // rails as `dharta env pull --include-secrets`.
50
+ export async function fetchRemoteEnv(
51
+ ctx: CliContext,
52
+ opts: AgentInferOptions & { includeSecrets?: boolean } = {},
53
+ ): Promise<{ values: Record<string, string>; withheld: string[] }> {
54
+ const slug = await inferAgentSlug(opts);
55
+ const body = await ctx.http.request<{
56
+ env: Array<{ name: string; secret: boolean; value?: string; withheld?: boolean }>;
57
+ }>({
58
+ method: "POST",
59
+ path: `/api/agents/${encodeURIComponent(slug)}/env/pull`,
60
+ headers: { "Idempotency-Key": randomUUID() },
61
+ body: { include_secrets: !!opts.includeSecrets },
62
+ });
63
+
64
+ const values: Record<string, string> = {};
65
+ const withheld: string[] = [];
66
+ for (const entry of body.env) {
67
+ if (entry.withheld) withheld.push(entry.name);
68
+ else values[entry.name] = entry.value ?? "";
69
+ }
70
+ return { values, withheld };
71
+ }
72
+
73
+ async function fetchReleaseArchive(url: string, slug: string, fetchImpl: typeof fetch): Promise<Response> {
74
+ try {
75
+ return await fetchImpl(url);
76
+ } catch (error) {
77
+ throw new Error(
78
+ `Could not download the release archive from ${url}: ${(error as Error).message}. ` +
79
+ `Check the version with \`dharta agent releases ${slug}\` and retry.`,
80
+ );
81
+ }
82
+ }
83
+
84
+ function untar(tarball: string, dest: string): Promise<void> {
85
+ return new Promise((resolve, reject) => {
86
+ const proc = spawn("tar", ["-xzf", tarball, "-C", dest], { stdio: "ignore" });
87
+ proc.on("error", () =>
88
+ reject(new Error("Could not extract the release archive (is `tar` installed?).")),
89
+ );
90
+ proc.on("close", (code) =>
91
+ code === 0
92
+ ? resolve()
93
+ : reject(new Error(`tar exited ${code} extracting the release archive.`)),
94
+ );
95
+ });
96
+ }
@@ -0,0 +1,45 @@
1
+ // Re-deploy a previous release by pointer flip. (Forward deploys are
2
+ // `dharta deploy` / `git push dharta`; this is the rollback-only path.)
3
+ import type { CliContext } from "../context.js";
4
+ import { randomUUID } from "node:crypto";
5
+ import { validateAgentSlug } from "../agent.js";
6
+
7
+ export interface RollbackResponse {
8
+ agent: { id: string; slug: string; name: string };
9
+ release: { id: string; version: number; status: string; commit_sha: string };
10
+ previous_release: { id: string; version: number; status: string } | null;
11
+ }
12
+
13
+ export async function rollbackData(
14
+ ctx: CliContext,
15
+ slug: string,
16
+ toVersion: number,
17
+ ): Promise<RollbackResponse> {
18
+ const normalizedSlug = validateAgentSlug(slug, "agent slug");
19
+ const normalizedVersion = normalizeRollbackVersion(toVersion);
20
+ return ctx.http.request<RollbackResponse>({
21
+ method: "POST",
22
+ path: `/api/agents/${encodeURIComponent(normalizedSlug)}/rollback`,
23
+ headers: { "Idempotency-Key": randomUUID() },
24
+ body: { to_version: normalizedVersion },
25
+ });
26
+ }
27
+
28
+ export async function rollback(
29
+ ctx: CliContext,
30
+ slug: string,
31
+ toVersion: number,
32
+ ): Promise<string> {
33
+ const res = await rollbackData(ctx, slug, toVersion);
34
+ const prev = res.previous_release
35
+ ? ` (from v${res.previous_release.version})`
36
+ : "";
37
+ return `Rolled back ${res.agent.slug} to v${res.release.version}${prev}.`;
38
+ }
39
+
40
+ function normalizeRollbackVersion(toVersion: number): number {
41
+ if (!Number.isSafeInteger(toVersion) || toVersion <= 0) {
42
+ throw new Error("version must be a positive whole number.");
43
+ }
44
+ return toVersion;
45
+ }
@@ -0,0 +1,290 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { inferAgentSlug, type AgentInferOptions } from "../agent.js";
3
+ import type { CliContext } from "../context.js";
4
+ import { parseDollarAmountToCents } from "../money.js";
5
+ import { fmtTs, table } from "../output.js";
6
+ import { normalizeNonBlank, normalizeTimezone, parsePositiveInteger, parseReleaseVersion } from "../validators.js";
7
+
8
+ export interface Schedule {
9
+ id: number;
10
+ cron: string;
11
+ timezone: string;
12
+ initial_message: string;
13
+ release_version: number | null;
14
+ budget_cents: number | null;
15
+ end_user_id: string | null;
16
+ enabled: boolean;
17
+ next_fire_at: string | null;
18
+ last_fired_at: string | null;
19
+ created_at: string;
20
+ updated_at: string;
21
+ }
22
+
23
+ interface ListEnvelope<T> {
24
+ data: T[];
25
+ }
26
+
27
+ interface ScheduleEnvelope {
28
+ schedule: Schedule;
29
+ changed?: boolean;
30
+ queued?: boolean;
31
+ }
32
+
33
+ export interface ScheduleCreateOptions extends AgentInferOptions {
34
+ cron?: string;
35
+ timezone?: string;
36
+ message?: string;
37
+ budget?: string;
38
+ budgetCents?: string;
39
+ releaseVersion?: string;
40
+ endUserId?: string;
41
+ runNow?: boolean;
42
+ }
43
+
44
+ export async function schedulesListData(ctx: CliContext, opts: AgentInferOptions): Promise<{ slug: string; data: Schedule[] }> {
45
+ const slug = await inferAgentSlug(opts);
46
+ const res = await ctx.http.request<ListEnvelope<Schedule>>({
47
+ path: `/api/agents/${encodeURIComponent(slug)}/schedules`,
48
+ });
49
+ return { slug, data: res.data };
50
+ }
51
+
52
+ export async function schedulesList(ctx: CliContext, opts: AgentInferOptions): Promise<string> {
53
+ const { slug, data } = await schedulesListData(ctx, opts);
54
+ if (data.length === 0) return `No schedules for ${slug}. Create one with: dharta schedules create --agent ${slug} --cron '0 9 * * *' --message 'Daily check-in'`;
55
+ return table(data.map((schedule) => ({
56
+ id: String(schedule.id),
57
+ state: schedule.enabled ? "enabled" : "paused",
58
+ cron: schedule.cron,
59
+ timezone: schedule.timezone,
60
+ next: fmtTs(schedule.next_fire_at),
61
+ budget: formatBudget(schedule.budget_cents),
62
+ })));
63
+ }
64
+
65
+ export async function schedulesCreateData(
66
+ ctx: CliContext,
67
+ opts: ScheduleCreateOptions,
68
+ ): Promise<{ slug: string } & ScheduleEnvelope> {
69
+ validateSchedulesCreateInput(opts);
70
+ const cron = opts.cron as string;
71
+ const message = opts.message as string;
72
+ const slug = await inferAgentSlug(opts);
73
+ const body: Record<string, unknown> = {
74
+ cron: normalizeRequired(cron, "--cron"),
75
+ timezone: opts.timezone === undefined ? "UTC" : normalizeTimezone(opts.timezone, "--timezone"),
76
+ initial_message: normalizeRequired(message, "--message"),
77
+ };
78
+ if (opts.budget !== undefined && opts.budgetCents !== undefined) {
79
+ throw new Error("Use either --budget or --budget-cents, not both.");
80
+ }
81
+ if (opts.budget !== undefined) body.budget_cents = parseDollarAmountToCents(opts.budget, "--budget");
82
+ if (opts.budgetCents !== undefined) body.budget_cents = parsePositiveWholeNumber(opts.budgetCents, "--budget-cents");
83
+ if (opts.releaseVersion !== undefined) body.release_version = parseReleaseVersion(opts.releaseVersion, "--release-version");
84
+ if (opts.endUserId !== undefined) body.end_user_id = normalizeRequired(opts.endUserId, "--end-user-id");
85
+ const res = await ctx.http.request<ScheduleEnvelope>({
86
+ method: "POST",
87
+ path: `/api/agents/${encodeURIComponent(slug)}/schedules`,
88
+ headers: idempotencyHeader(),
89
+ body: { schedule: body },
90
+ });
91
+ if (opts.runNow) {
92
+ const run = await runScheduleNow(ctx, slug, String(res.schedule.id));
93
+ return { slug, ...res, queued: run.queued ?? true };
94
+ }
95
+ return { slug, ...res };
96
+ }
97
+
98
+ export async function schedulesCreate(ctx: CliContext, opts: ScheduleCreateOptions): Promise<string> {
99
+ const res = await schedulesCreateData(ctx, opts);
100
+ return `Created schedule ${res.schedule.id} for ${res.slug}${res.queued ? " and queued one run" : ""}.`;
101
+ }
102
+
103
+ export function validateSchedulesCreateInput(opts: ScheduleCreateOptions): void {
104
+ const missing = [
105
+ opts.cron === undefined ? "--cron <expr>" : null,
106
+ opts.message === undefined ? "--message <text>" : null,
107
+ ].filter((flag): flag is string => flag !== null);
108
+ if (missing.length > 0) {
109
+ throw new Error(
110
+ `Pass ${formatMissingFlags(missing)}. ` +
111
+ "Usage: dharta schedules create --cron '0 9 * * *' --message 'Daily check-in'",
112
+ );
113
+ }
114
+ const cron = opts.cron as string;
115
+ const message = opts.message as string;
116
+ validateCron(cron);
117
+ if (opts.timezone !== undefined) normalizeTimezone(opts.timezone, "--timezone");
118
+ normalizeRequired(message, "--message");
119
+ if (opts.budget !== undefined && opts.budgetCents !== undefined) {
120
+ throw new Error("Use either --budget or --budget-cents, not both.");
121
+ }
122
+ if (opts.budget !== undefined) parseDollarAmountToCents(opts.budget, "--budget");
123
+ if (opts.budgetCents !== undefined) parsePositiveWholeNumber(opts.budgetCents, "--budget-cents");
124
+ if (opts.releaseVersion !== undefined) parseReleaseVersion(opts.releaseVersion, "--release-version");
125
+ if (opts.endUserId !== undefined) normalizeRequired(opts.endUserId, "--end-user-id");
126
+ }
127
+
128
+ function formatMissingFlags(flags: string[]): string {
129
+ return flags.length === 1 ? flags[0] : `${flags.slice(0, -1).join(", ")} and ${flags[flags.length - 1]}`;
130
+ }
131
+
132
+ function validateCron(value: string): void {
133
+ const cron = normalizeRequired(value, "--cron");
134
+ const fields = cron.split(/\s+/);
135
+ if (fields.length !== 5 || fields.some((field) => field.length === 0)) {
136
+ throw new Error("--cron must be a standard 5-field cron expression, e.g. '0 9 * * *'.");
137
+ }
138
+ fields.forEach((field, index) => validateCronField(field, CRON_FIELD_SPECS[index]));
139
+ }
140
+
141
+ const CRON_FIELD_SPECS = [
142
+ { name: "minute", min: 0, max: 59, names: {} },
143
+ { name: "hour", min: 0, max: 23, names: {} },
144
+ { name: "day-of-month", min: 1, max: 31, names: {} },
145
+ {
146
+ name: "month",
147
+ min: 1,
148
+ max: 12,
149
+ names: { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 },
150
+ },
151
+ {
152
+ name: "day-of-week",
153
+ min: 0,
154
+ max: 7,
155
+ names: { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 },
156
+ },
157
+ ] as const;
158
+
159
+ type CronFieldSpec = (typeof CRON_FIELD_SPECS)[number];
160
+
161
+ function validateCronField(field: string, spec: CronFieldSpec): void {
162
+ for (const item of field.split(",")) {
163
+ if (!item) throw unsupportedCronField(spec);
164
+ const [base, step, extra] = item.split("/");
165
+ if (extra !== undefined || !base) throw unsupportedCronField(spec);
166
+ if (step !== undefined && !/^[1-9]\d*$/.test(step)) {
167
+ throw new Error(`--cron ${spec.name} field step must be a positive whole number.`);
168
+ }
169
+ if (base === "*") continue;
170
+
171
+ const [start, end, extraRange] = base.split("-");
172
+ if (extraRange !== undefined || !start) throw unsupportedCronField(spec);
173
+ if (end === undefined) {
174
+ parseCronToken(start, spec);
175
+ continue;
176
+ }
177
+ if (!end) throw unsupportedCronField(spec);
178
+ const rangeStart = parseCronToken(start, spec);
179
+ const rangeEnd = parseCronToken(end, spec);
180
+ if (rangeStart > rangeEnd) {
181
+ throw new Error(`--cron ${spec.name} field range must start before it ends.`);
182
+ }
183
+ }
184
+ }
185
+
186
+ function parseCronToken(token: string, spec: CronFieldSpec): number {
187
+ const normalized = token.toLowerCase();
188
+ const named = (spec.names as Record<string, number>)[normalized];
189
+ if (named !== undefined) return named;
190
+ if (!/^\d+$/.test(token)) throw unsupportedCronField(spec);
191
+ const parsed = Number(token);
192
+ if (parsed < spec.min || parsed > spec.max) {
193
+ throw new Error(`--cron ${spec.name} field must be ${spec.min}-${spec.max}.`);
194
+ }
195
+ return parsed;
196
+ }
197
+
198
+ function unsupportedCronField(spec: CronFieldSpec): Error {
199
+ return new Error(`--cron ${spec.name} field is not supported. Use numbers, *, ranges, lists, and steps.`);
200
+ }
201
+
202
+ export async function schedulesActionData(
203
+ ctx: CliContext,
204
+ id: string,
205
+ action: "pause" | "resume" | "run",
206
+ opts: AgentInferOptions,
207
+ ): Promise<{ slug: string } & ScheduleEnvelope> {
208
+ const normalizedId = normalizeScheduleId(id);
209
+ const slug = await inferAgentSlug(opts);
210
+ const res = action === "run"
211
+ ? await runScheduleNow(ctx, slug, normalizedId)
212
+ : await ctx.http.request<ScheduleEnvelope>({
213
+ method: "POST",
214
+ path: `/api/agents/${encodeURIComponent(slug)}/schedules/${encodeURIComponent(normalizedId)}/${action}`,
215
+ headers: idempotencyHeader(),
216
+ });
217
+ return { slug, ...res };
218
+ }
219
+
220
+ export async function schedulesAction(
221
+ ctx: CliContext,
222
+ id: string,
223
+ action: "pause" | "resume" | "run",
224
+ opts: AgentInferOptions,
225
+ ): Promise<string> {
226
+ const res = await schedulesActionData(ctx, id, action, opts);
227
+ if (action === "run") return `Queued schedule ${res.schedule.id} for ${res.slug}.`;
228
+ return `${action === "pause" ? "Paused" : "Resumed"} schedule ${res.schedule.id} for ${res.slug}${res.changed ? "" : " (unchanged)"}.`;
229
+ }
230
+
231
+ export async function schedulesDeleteData(
232
+ ctx: CliContext,
233
+ id: string,
234
+ opts: AgentInferOptions,
235
+ ): Promise<{ slug: string; id: string; deleted: boolean }> {
236
+ const normalizedId = normalizeScheduleId(id);
237
+ const slug = await inferAgentSlug(opts);
238
+ await ctx.http.request<void>({
239
+ method: "DELETE",
240
+ path: `/api/agents/${encodeURIComponent(slug)}/schedules/${encodeURIComponent(normalizedId)}`,
241
+ headers: idempotencyHeader(),
242
+ });
243
+ return { slug, id: normalizedId, deleted: true };
244
+ }
245
+
246
+ export async function schedulesDelete(ctx: CliContext, id: string, opts: AgentInferOptions): Promise<string> {
247
+ const res = await schedulesDeleteData(ctx, id, opts);
248
+ return `Deleted schedule ${res.id} for ${res.slug}.`;
249
+ }
250
+
251
+ function parsePositiveWholeNumber(value: string, flag: string): number {
252
+ const normalized = value.trim();
253
+ if (!/^\d+$/.test(normalized)) throw new Error(`${flag} must be a whole number.`);
254
+ const parsed = Number(normalized);
255
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
256
+ throw new Error(`${flag} must be a positive whole number.`);
257
+ }
258
+ return parsed;
259
+ }
260
+
261
+ function formatBudget(cents: number | null): string {
262
+ if (cents === null) return "—";
263
+ return `$${(cents / 100).toFixed(2)}`;
264
+ }
265
+
266
+ function idempotencyHeader(): Record<string, string> {
267
+ return { "Idempotency-Key": randomUUID() };
268
+ }
269
+
270
+ function runScheduleNow(ctx: CliContext, slug: string, id: string): Promise<ScheduleEnvelope> {
271
+ return ctx.http.request<ScheduleEnvelope>({
272
+ method: "POST",
273
+ path: `/api/agents/${encodeURIComponent(slug)}/schedules/${encodeURIComponent(id)}/run`,
274
+ headers: idempotencyHeader(),
275
+ });
276
+ }
277
+
278
+ function normalizeRequired(value: string, flag: string): string {
279
+ const normalized = value.trim();
280
+ if (!normalized) {
281
+ throw new Error(`${flag} cannot be blank.`);
282
+ }
283
+ return normalized;
284
+ }
285
+
286
+ function normalizeScheduleId(id: string): string {
287
+ const normalized = normalizeNonBlank(id, "Schedule id");
288
+ parsePositiveInteger(normalized, "Schedule id");
289
+ return normalized;
290
+ }
@@ -0,0 +1,22 @@
1
+ export const SENSITIVE_API_KEY_SCOPES = new Set([
2
+ "env:secrets:read",
3
+ "source_archives:read",
4
+ "workspace_tokens:create",
5
+ "workspace:read",
6
+ "audit_log:export",
7
+ "members:invite",
8
+ "members:write",
9
+ "members:owner_role:write",
10
+ "agents:delete",
11
+ "keys:write",
12
+ "model_keys:write",
13
+ ]);
14
+
15
+ export function formatScopes(scopes: readonly string[], separator = ", "): string {
16
+ return scopes.join(separator);
17
+ }
18
+
19
+ export function formatApiKeyExpiry(expiresAt: string | null | undefined): string {
20
+ const normalized = expiresAt?.trim();
21
+ return normalized ? normalized : "none - revocable on the dashboard keys page";
22
+ }