@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,117 @@
1
+ import { existsSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import TOML from "@iarna/toml";
4
+ import { closestChoice, normalizeNonBlank } from "./validators.js";
5
+
6
+ export const HARNESS_TYPES = ["claude-code", "opencode", "deepagents", "goose", "codex-cli"] as const;
7
+ export type HarnessType = (typeof HARNESS_TYPES)[number];
8
+
9
+ export const DEFAULT_CREATE_HARNESS: HarnessType = "claude-code";
10
+
11
+ const HARNESS_MARKERS: Record<HarnessType, string[]> = {
12
+ "claude-code": [".claude", "CLAUDE.md"],
13
+ opencode: [".opencode/opencode.jsonc", ".opencode/agents"],
14
+ deepagents: [".deepagents", ".deepagents/agents"],
15
+ goose: [".goose", ".goose/agents"],
16
+ "codex-cli": [".codex/config.toml", ".codex/agents"],
17
+ };
18
+
19
+ const NATIVE_TEMPLATE_MARKER: Partial<Record<HarnessType, { path: string; content: string }>> = {
20
+ opencode: { path: ".opencode/opencode.jsonc", content: "{}\n" },
21
+ deepagents: { path: ".deepagents/dharta.jsonc", content: "{}\n" },
22
+ goose: { path: ".goose/dharta.jsonc", content: "{}\n" },
23
+ "codex-cli": { path: ".codex/config.toml", content: "# Codex project config for this Dharta agent.\n" },
24
+ };
25
+
26
+ export const AGENT_MARKERS = [
27
+ "dharta.toml",
28
+ "CLAUDE.md",
29
+ ".claude",
30
+ ".opencode",
31
+ ".deepagents",
32
+ ".goose",
33
+ ".codex",
34
+ ] as const;
35
+
36
+ export function parseHarnessType(value: string | undefined, label = "--harness"): HarnessType | undefined {
37
+ if (value === undefined) return undefined;
38
+ const normalized = normalizeNonBlank(value, label);
39
+ if ((HARNESS_TYPES as readonly string[]).includes(normalized)) return normalized as HarnessType;
40
+ const suggestion = closestChoice(normalized, HARNESS_TYPES);
41
+ throw new Error(
42
+ `${label} must be one of ${HARNESS_TYPES.join(", ")}.` +
43
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
44
+ );
45
+ }
46
+
47
+ export function nativeInstructionFile(harness: HarnessType): "CLAUDE.md" | "AGENTS.md" {
48
+ return harness === "claude-code" ? "CLAUDE.md" : "AGENTS.md";
49
+ }
50
+
51
+ export function translateTemplateFilesForHarness(
52
+ files: Record<string, string>,
53
+ harness: HarnessType,
54
+ ): Record<string, string> {
55
+ if (harness === "claude-code") return { ...files };
56
+
57
+ const translated: Record<string, string> = {};
58
+ for (const [rel, content] of Object.entries(files)) {
59
+ if (rel !== "CLAUDE.md") translated[rel] = content;
60
+ }
61
+ if (translated["AGENTS.md"] === undefined && files["CLAUDE.md"] !== undefined) {
62
+ translated["AGENTS.md"] = files["CLAUDE.md"];
63
+ }
64
+ const marker = NATIVE_TEMPLATE_MARKER[harness];
65
+ if (marker && translated[marker.path] === undefined) {
66
+ translated[marker.path] = marker.content;
67
+ }
68
+ return translated;
69
+ }
70
+
71
+ export function hasAgentMarker(dir: string): boolean {
72
+ return AGENT_MARKERS.some((marker) => existsSync(path.join(dir, marker)));
73
+ }
74
+
75
+ export function markerFolderRel(filePath: string): string | null {
76
+ const parts = filePath.split(/[\\/]+/).filter(Boolean);
77
+ if (parts.includes("node_modules")) return null;
78
+ const last = parts.at(-1);
79
+ if (last === "dharta.toml" || last === "CLAUDE.md") {
80
+ return parts.slice(0, -1).join(path.sep) || ".";
81
+ }
82
+ for (const marker of [".claude", ".opencode", ".deepagents", ".goose", ".codex"]) {
83
+ const index = parts.indexOf(marker);
84
+ if (index >= 0) return parts.slice(0, index).join(path.sep) || ".";
85
+ }
86
+ return null;
87
+ }
88
+
89
+ export function sniffHarnessType(dir: string): HarnessType {
90
+ if (presentIn(dir, "opencode")) return "opencode";
91
+ if (presentIn(dir, "deepagents")) return "deepagents";
92
+ if (presentIn(dir, "goose")) return "goose";
93
+ if (presentIn(dir, "claude-code")) return "claude-code";
94
+ if (presentIn(dir, "codex-cli")) return "codex-cli";
95
+ return "opencode";
96
+ }
97
+
98
+ export function readDeclaredHarnessType(manifestText: string): HarnessType | undefined {
99
+ let parsed: unknown;
100
+ try {
101
+ parsed = TOML.parse(manifestText);
102
+ } catch {
103
+ return undefined;
104
+ }
105
+ const table = (parsed as Record<string, unknown>).harness;
106
+ if (table === null || typeof table !== "object") return undefined;
107
+ const type = (table as Record<string, unknown>).type;
108
+ try {
109
+ return typeof type === "string" ? parseHarnessType(type, "[harness] type") : undefined;
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ function presentIn(dir: string, harness: HarnessType): boolean {
116
+ return HARNESS_MARKERS[harness].some((marker) => existsSync(path.join(dir, marker)));
117
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { main } from "./cli.js";
2
+ export { buildContext } from "./context.js";
3
+ export type { CliContext, CtxOverrides } from "./context.js";
@@ -0,0 +1,49 @@
1
+ // RFC 0024 DEP-9 / D5: `dharta setup` is fully functional logged-out - it
2
+ // writes local config only and reads no logged-in state. If you are not
3
+ // logged in, it offers login ONCE at the end as the bridge to `dharta deploy`
4
+ // (which does need auth). --login forces it; otherwise we only prompt an
5
+ // interactive terminal. It never blocks setup.
6
+
7
+ import { activeProfile } from "./config.js";
8
+ import { login } from "./commands/login.js";
9
+
10
+ export interface OfferLoginOptions {
11
+ force?: boolean; // --login
12
+ ask?: (q: string) => Promise<string>; // present only in an interactive TTY
13
+ log: (line: string) => void;
14
+ // seams (tests)
15
+ isLoggedIn?: () => Promise<boolean>;
16
+ runLogin?: () => Promise<void>;
17
+ }
18
+
19
+ export type OfferLoginOutcome = "logged-in" | "skipped" | "completed";
20
+
21
+ export async function offerLogin(opts: OfferLoginOptions): Promise<OfferLoginOutcome> {
22
+ // Non-interactive and not forced: stay silent (don't even read the config).
23
+ if (!opts.force && !opts.ask) return "skipped";
24
+
25
+ const loggedIn = opts.isLoggedIn ?? defaultIsLoggedIn;
26
+ if (await loggedIn()) return "logged-in";
27
+
28
+ let go = opts.force ?? false;
29
+ if (!go && opts.ask) {
30
+ const a = (await opts.ask("You are not logged into Dharta. Log in now? (optional) [y/N] ")).trim().toLowerCase();
31
+ go = a === "y" || a === "yes";
32
+ }
33
+ if (!go) {
34
+ opts.log("Setup is complete. You're not logged in - run `dharta login`, then `dharta deploy`.");
35
+ return "skipped";
36
+ }
37
+
38
+ await (opts.runLogin ?? (() => login({ log: opts.log }).then(() => undefined)))();
39
+ return "completed";
40
+ }
41
+
42
+ async function defaultIsLoggedIn(): Promise<boolean> {
43
+ try {
44
+ await activeProfile();
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
@@ -0,0 +1,153 @@
1
+ import TOML from "@iarna/toml";
2
+ import { DEFAULT_CREATE_HARNESS, type HarnessType } from "./harnesses.js";
3
+
4
+ export type DeployMethod = "git" | "folder";
5
+
6
+ const DEPLOY_METHOD_PLACEHOLDER = '# deploy_method = "git" # set by `dharta setup`';
7
+
8
+ // The scaffolded dharta.toml is self-documenting (RFC 0024 DEP-1/DEP-4):
9
+ // every field is commented with its purpose and its alternatives, which
10
+ // is the whole DX fix. TOML.stringify drops comments, so a fresh manifest
11
+ // is written from this string template, and an edit to an existing one is
12
+ // a surgical top-level-scalar patch that leaves the comments and [tables]
13
+ // intact.
14
+
15
+ export function renderManifest(o: {
16
+ name: string;
17
+ deploy: boolean;
18
+ deployMethod?: DeployMethod;
19
+ harness?: HarnessType;
20
+ harnessVersion?: string;
21
+ }): string {
22
+ const harness = o.harness ?? DEFAULT_CREATE_HARNESS;
23
+ const versionExample = harnessVersionExample(harness);
24
+ const versionLine = o.harnessVersion
25
+ ? `version = ${JSON.stringify(o.harnessVersion)}`
26
+ : `# version = ${JSON.stringify(versionExample)}`;
27
+ return [
28
+ "# --- Identity -------------------------------------------------------------",
29
+ "# Display name (1-100 characters). Dharta derives the deploy slug from this",
30
+ "# name (for example, Support Bot -> support-bot); that slug must be unique",
31
+ "# within your org. Changing the name so its slug changes targets a different agent.",
32
+ "# Each deploy creates a new immutable release; earlier releases remain available.",
33
+ `name = ${JSON.stringify(o.name)}`,
34
+ "",
35
+ "# Deploy gate. Only agents with deploy = true ship on `dharta deploy` /",
36
+ "# `git push dharta`. Absent or false = configured but not deployed.",
37
+ `deploy = ${o.deploy}`,
38
+ "",
39
+ "# How this agent is deployed. `git` pushes the containing repo while",
40
+ "# selecting this folder; `folder` uploads this folder directly.",
41
+ o.deployMethod
42
+ ? `deploy_method = ${JSON.stringify(o.deployMethod)}`
43
+ : DEPLOY_METHOD_PLACEHOLDER,
44
+ "",
45
+ "# --- Build ----------------------------------------------------------------",
46
+ "# OMIT this whole section for a harness-only agent (for example CLAUDE.md,",
47
+ "# AGENTS.md + .opencode/, .deepagents/, .goose/, or .codex/) - Dharta serves it as-is,",
48
+ "# no build step.",
49
+ '# "python" - Python project; Dharta builds it (set entry_point below)',
50
+ '# "node" - Node project; Dharta builds it',
51
+ '# buildpack = "python"',
52
+ "",
53
+ '# Import path "module:attribute" to your app object. Required ONLY when',
54
+ "# buildpack is set; omit for harness-only agents.",
55
+ '# entry_point = "app:app"',
56
+ "",
57
+ "# --- Harness ---------------------------------------------------------------",
58
+ "# The agent runtime this agent runs on. `type` is fixed for the life of the",
59
+ "# agent (switching harness = a new agent). Supported types today:",
60
+ "# claude-code, opencode, deepagents, goose, codex-cli.",
61
+ "# `version` is optional: a range resolves to the latest matching version Dharta",
62
+ "# supports at deploy time; omit it for the curated default; pin an exact",
63
+ "# version only when you need determinism (`dharta deploy` checks it).",
64
+ "[harness]",
65
+ `type = ${JSON.stringify(harness)}`,
66
+ versionLine,
67
+ "",
68
+ "# --- Environment (declare-or-bind) -----------------------------------------",
69
+ "# Names this agent needs. Bind locally in .env, in prod via `dharta env set`.",
70
+ "# A required var with no prod binding fails the build.",
71
+ "[env]",
72
+ '# SUPPORT_DB_URL = { required = false, description = "Support database DSN" }',
73
+ "",
74
+ "# --- Environment setup -----------------------------------------------------",
75
+ "# A Bash setup script run once per cold start, before the agent, to install",
76
+ "# what the base image lacks. Use user-space installs: `pip install` and",
77
+ "# `npm i -g` are auto-configured (pip --user) to persist under ~/.local, so",
78
+ "# no --user flag is needed. apt/system packages are NOT available",
79
+ "# here (they need a custom image - a higher tier). The script is validated at",
80
+ "# deploy (`bash -n`); a syntax error fails the deploy, not your users.",
81
+ "# network: none | trusted | full (default) | custom. Only `full` is enforced",
82
+ "# today (every agent has open egress); the others arrive with a later phase.",
83
+ "# [environment]",
84
+ '# setup = "scripts/setup.sh"',
85
+ '# network = "full"',
86
+ "",
87
+ ].join("\n");
88
+ }
89
+
90
+ function harnessVersionExample(harness: HarnessType): string {
91
+ switch (harness) {
92
+ case "claude-code":
93
+ return "2";
94
+ case "opencode":
95
+ return "1";
96
+ case "deepagents":
97
+ return "0.1.28";
98
+ case "goose":
99
+ return "1.39.0";
100
+ case "codex-cli":
101
+ return "0.133.0";
102
+ }
103
+ }
104
+
105
+ // Flip or insert a top-level scalar WITHOUT disturbing comments or
106
+ // [tables]: replace it in place if present (anchored to a line, so a
107
+ // commented `# name = ...` is never matched), else insert before the
108
+ // first table so it stays a top-level key rather than nesting under a
109
+ // section. `key` is always a literal we control
110
+ // ("name"/"deploy"/"deploy_method").
111
+ export function patchScalar(src: string, key: string, literal: string): string {
112
+ const re = new RegExp(`^(\\s*${key}\\s*=).*$`, "m");
113
+ const tbl = src.search(/^\s*\[/m);
114
+ const top = tbl >= 0 ? src.slice(0, tbl) : src;
115
+ const rest = tbl >= 0 ? src.slice(tbl) : "";
116
+ if (re.test(top)) return top.replace(re, `$1 ${literal}`) + rest;
117
+ const line = `${key} = ${literal}\n`;
118
+ if (tbl >= 0) return src.slice(0, tbl) + line + src.slice(tbl);
119
+ return (src.endsWith("\n") ? src : src + "\n") + line;
120
+ }
121
+
122
+ function hasTopLevelScalar(src: string, key: string): boolean {
123
+ const tbl = src.search(/^\s*\[/m);
124
+ const top = tbl >= 0 ? src.slice(0, tbl) : src;
125
+ return new RegExp(`^\\s*${key}\\s*=`, "m").test(top);
126
+ }
127
+
128
+ // Set identity/deploy configuration on an existing manifest, preserving its
129
+ // comments and tables, then round-trip through the parser so a broken edit
130
+ // fails loud instead of writing an invalid manifest.
131
+ export function patchManifest(
132
+ src: string,
133
+ o: { name?: string; deploy?: boolean; deployMethod?: DeployMethod },
134
+ ): string {
135
+ let out = src;
136
+ if (o.name !== undefined) out = patchScalar(out, "name", JSON.stringify(o.name));
137
+ if (o.deploy !== undefined) out = patchScalar(out, "deploy", String(o.deploy));
138
+ if (o.deployMethod !== undefined) {
139
+ const literal = JSON.stringify(o.deployMethod);
140
+ // `dharta create` leaves this decision visibly unset for setup. Activate
141
+ // that generated placeholder in place so the field stays next to its
142
+ // explanation; arbitrary user comments remain untouched.
143
+ if (hasTopLevelScalar(out, "deploy_method")) {
144
+ out = patchScalar(out, "deploy_method", literal);
145
+ } else if (out.includes(DEPLOY_METHOD_PLACEHOLDER)) {
146
+ out = out.replace(DEPLOY_METHOD_PLACEHOLDER, `deploy_method = ${literal}`);
147
+ } else {
148
+ out = patchScalar(out, "deploy_method", literal);
149
+ }
150
+ }
151
+ TOML.parse(out); // throws on an invalid result
152
+ return out;
153
+ }
package/src/money.ts ADDED
@@ -0,0 +1,15 @@
1
+ export function parseDollarAmountToCents(value: string, flag: string): number {
2
+ const trimmed = value.trim();
3
+ const match = trimmed.match(/^(?:(\d+)(?:\.(\d{1,2}))?|\.(\d{1,2}))$/);
4
+ if (!match) throw amountError(flag);
5
+
6
+ const dollars = match[1] ? Number(match[1]) : 0;
7
+ const centsPart = match[2] ?? match[3] ?? "";
8
+ const cents = dollars * 100 + Number(centsPart.padEnd(2, "0"));
9
+ if (!Number.isSafeInteger(cents) || cents <= 0) throw amountError(flag);
10
+ return cents;
11
+ }
12
+
13
+ function amountError(flag: string): Error {
14
+ return new Error(`${flag} must be a dollar amount greater than 0 with at most two decimal places.`);
15
+ }
package/src/output.ts ADDED
@@ -0,0 +1,22 @@
1
+ // Output helpers. Tiny on purpose — the CLI's UX is "narrate one line
2
+ // per row + occasional table"; pulling in cli-table or chalk costs
3
+ // more than it buys at this scale.
4
+
5
+ export function table(rows: Array<Record<string, string>>): string {
6
+ if (rows.length === 0) return "";
7
+ const cols = Object.keys(rows[0]);
8
+ const widths = cols.map((c) =>
9
+ Math.max(c.length, ...rows.map((r) => (r[c] ?? "").length)),
10
+ );
11
+ const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - s.length));
12
+ const header = cols.map((c, i) => pad(c, widths[i])).join(" ");
13
+ const divider = widths.map((w) => "-".repeat(w)).join(" ");
14
+ const body = rows
15
+ .map((r) => cols.map((c, i) => pad(r[c] ?? "", widths[i])).join(" "))
16
+ .join("\n");
17
+ return `${header}\n${divider}\n${body}`;
18
+ }
19
+
20
+ export function fmtTs(ts: string | null | undefined): string {
21
+ return ts ?? "—";
22
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,127 @@
1
+ // Runtime bootstrap for `dharta dev` (RFC 0013 CLI-3, decision D2): the
2
+ // harness-adapter kernel lives in the Python `dharta-runtime` package,
3
+ // single-sourced with production. The CLI resolves it via uv with a
4
+ // version pin that ships inside each CLI release, so CLI and runtime
5
+ // version as a pair. DHARTA_RUNTIME is the escape hatch (point it at a
6
+ // checkout or an executable). Zero Python is ever visible on the happy
7
+ // path; every failure prints the one remedial command.
8
+
9
+ import { promises as fs } from "node:fs";
10
+ import * as path from "node:path";
11
+ import { spawn } from "node:child_process";
12
+
13
+ // Ships with the CLI release; bump together with dharta-runtime.
14
+ export const DHARTA_RUNTIME_PIN = "0.6.77";
15
+
16
+ export const UV_INSTALL_HINT =
17
+ "Install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh (or: brew install uv)\n" +
18
+ "Then re-run dharta dev. To use a local checkout instead, set DHARTA_RUNTIME=/path/to/dharta-runtime.";
19
+
20
+ export interface RuntimeCommand {
21
+ // argv prefix that launches dharta-runtime; callers append subcommand
22
+ // args (e.g. ["serve", "--port", "4001"]).
23
+ command: string[];
24
+ source: "env-executable" | "env-checkout" | "uv";
25
+ description: string;
26
+ }
27
+
28
+ export interface RuntimeResolveOptions {
29
+ env?: NodeJS.ProcessEnv;
30
+ commandExists?: (cmd: string) => Promise<boolean>;
31
+ }
32
+
33
+ // Resolve how to launch dharta-runtime. Throws with remedial guidance —
34
+ // never a stack trace — when the bootstrap can't work.
35
+ export async function resolveRuntime(
36
+ opts: RuntimeResolveOptions = {},
37
+ ): Promise<RuntimeCommand> {
38
+ const env = opts.env ?? process.env;
39
+ const exists = opts.commandExists ?? commandOnPath;
40
+
41
+ const override = env.DHARTA_RUNTIME;
42
+ if (override) {
43
+ const stat = await fs.stat(override).catch(() => null);
44
+ if (!stat) {
45
+ throw new Error(
46
+ `DHARTA_RUNTIME points at ${override}, which does not exist. ` +
47
+ "Unset it to use the pinned runtime, or point it at a dharta-runtime checkout or executable.",
48
+ );
49
+ }
50
+ if (stat.isDirectory()) {
51
+ if (!(await exists("uv"))) {
52
+ throw new Error(`DHARTA_RUNTIME is a checkout, which needs uv to run.\n${UV_INSTALL_HINT}`);
53
+ }
54
+ return {
55
+ command: ["uv", "run", "--project", override, "dharta-runtime"],
56
+ source: "env-checkout",
57
+ description: `checkout at ${override} (DHARTA_RUNTIME)`,
58
+ };
59
+ }
60
+ return {
61
+ command: [override],
62
+ source: "env-executable",
63
+ description: `executable ${override} (DHARTA_RUNTIME)`,
64
+ };
65
+ }
66
+
67
+ // Check for the binary we actually launch (uvx ships with uv, but a
68
+ // partial install would otherwise surface as a raw spawn ENOENT).
69
+ if (!(await exists("uvx"))) {
70
+ throw new Error(`dharta dev needs uv to bootstrap the agent runtime.\n${UV_INSTALL_HINT}`);
71
+ }
72
+ return {
73
+ // uvx resolves and caches the pinned package on first use — the
74
+ // "install" step the user never sees.
75
+ command: ["uvx", "--from", `dharta-runtime==${DHARTA_RUNTIME_PIN}`, "dharta-runtime"],
76
+ source: "uv",
77
+ description: `dharta-runtime ${DHARTA_RUNTIME_PIN} via uv`,
78
+ };
79
+ }
80
+
81
+ // `which`-equivalent: scan PATH for an executable, returning every hit
82
+ // in order. Used both for uv detection and the doctor's PATH-shadow
83
+ // check (a stale pip-installed `dharta` shadowing this CLI). On Windows
84
+ // the PATHEXT suffixes apply and the executable bit is meaningless.
85
+ export async function whichAll(
86
+ cmd: string,
87
+ env: NodeJS.ProcessEnv = process.env,
88
+ ): Promise<string[]> {
89
+ const dirs = (env.PATH ?? "").split(path.delimiter).filter(Boolean);
90
+ const win = process.platform === "win32";
91
+ const suffixes = win
92
+ ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((s) => s.toLowerCase())
93
+ : [""];
94
+ const hits: string[] = [];
95
+ for (const dir of dirs) {
96
+ for (const suffix of suffixes) {
97
+ const candidate = path.join(dir, cmd + suffix);
98
+ try {
99
+ await fs.access(candidate, win ? fs.constants.F_OK : fs.constants.X_OK);
100
+ hits.push(candidate);
101
+ break;
102
+ } catch {
103
+ // not here
104
+ }
105
+ }
106
+ }
107
+ return hits;
108
+ }
109
+
110
+ async function commandOnPath(cmd: string): Promise<boolean> {
111
+ return (await whichAll(cmd)).length > 0;
112
+ }
113
+
114
+ // Spawn the runtime with the resolved command + args, inheriting stdio
115
+ // by default. The error path names the escape hatch.
116
+ export function spawnRuntime(
117
+ runtime: RuntimeCommand,
118
+ args: string[],
119
+ options: { cwd?: string; env?: NodeJS.ProcessEnv; stdio?: "inherit" | "pipe" } = {},
120
+ ): ReturnType<typeof spawn> {
121
+ const [cmd, ...prefix] = runtime.command;
122
+ return spawn(cmd, [...prefix, ...args], {
123
+ cwd: options.cwd,
124
+ env: options.env ?? process.env,
125
+ stdio: options.stdio ?? "inherit",
126
+ });
127
+ }
@@ -0,0 +1,102 @@
1
+ // Template scaffolding, extracted from the old `dharta init` (RFC 0024 DEP-4c,
2
+ // B.1): `dharta setup` reuses this to lay down a starter agent in a brand-new
3
+ // folder. Unlike init it does NOT git-init or commit (setup prompts for git,
4
+ // `dharta deploy` makes the first commit), does NOT require an empty directory
5
+ // (setup configures folders that may already have files), never clobbers an
6
+ // existing file, and skips dharta.toml - setup writes the self-documenting one.
7
+
8
+ import { existsSync, promises as fs } from "node:fs";
9
+ import * as path from "node:path";
10
+ import { resolveTemplates } from "./templates.js";
11
+ import { closestChoice, normalizeNonBlank } from "./validators.js";
12
+ import {
13
+ DEFAULT_CREATE_HARNESS,
14
+ nativeInstructionFile,
15
+ translateTemplateFilesForHarness,
16
+ type HarnessType,
17
+ } from "./harnesses.js";
18
+
19
+ export interface ScaffoldOptions {
20
+ template?: string; // default "blank"
21
+ harness?: HarnessType; // default "claude-code"
22
+ baseUrl: string;
23
+ fetchImpl?: typeof fetch;
24
+ log?: (line: string) => void;
25
+ }
26
+
27
+ export interface ScaffoldResult {
28
+ template: string;
29
+ harness: HarnessType;
30
+ instructionFile: "CLAUDE.md" | "AGENTS.md";
31
+ files: string[]; // newly written, relative to target
32
+ source: "control-plane" | "bundled";
33
+ }
34
+
35
+ export async function scaffoldTemplate(target: string, opts: ScaffoldOptions): Promise<ScaffoldResult> {
36
+ const log = opts.log ?? ((line: string) => console.log(line));
37
+ const templateKey = opts.template === undefined ? "blank" : normalizeNonBlank(opts.template, "--template");
38
+ const harness = opts.harness ?? DEFAULT_CREATE_HARNESS;
39
+
40
+ const resolved = await resolveTemplates(opts.baseUrl, opts.fetchImpl);
41
+ if (resolved.source === "bundled") {
42
+ log("Could not reach the template service - using the bundled template set.");
43
+ }
44
+
45
+ const template = resolved.templates.find((t) => t.key === templateKey);
46
+ if (!template) {
47
+ const keys = resolved.templates.map((t) => t.key);
48
+ const suggestion = closestChoice(templateKey, keys);
49
+ throw new Error(
50
+ `Unknown template '${templateKey}'. Available: ${keys.join(", ")}.` +
51
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
52
+ );
53
+ }
54
+
55
+ await fs.mkdir(target, { recursive: true });
56
+ const written: string[] = [];
57
+ const files = translateTemplateFilesForHarness(template.files, harness);
58
+ for (const [rel, content] of Object.entries(files)) {
59
+ if (rel === "dharta.toml") continue; // setup owns identity (the commented manifest)
60
+ // Template files come from server JSON - never let a hostile path
61
+ // (../..) or non-string content escape the target directory.
62
+ const dest = path.resolve(target, rel);
63
+ if (dest !== target && !dest.startsWith(target + path.sep)) {
64
+ throw new Error(
65
+ `Template file path escapes the target directory: ${rel}. ` +
66
+ "Re-run with a bundled template (--template blank) and report this.",
67
+ );
68
+ }
69
+ if (typeof content !== "string") {
70
+ throw new Error(`Template file ${rel} has non-text content. Re-run with --template blank and report this.`);
71
+ }
72
+ if (existsSync(dest)) continue; // never clobber the user's own file
73
+ await fs.mkdir(path.dirname(dest), { recursive: true });
74
+ await fs.writeFile(dest, content);
75
+ written.push(rel);
76
+ }
77
+
78
+ // .env binds the [env] declarations locally and must never be committed.
79
+ await ensureGitignoreHasDotEnv(target);
80
+
81
+ return {
82
+ template: template.key,
83
+ harness,
84
+ instructionFile: nativeInstructionFile(harness),
85
+ files: written,
86
+ source: resolved.source,
87
+ };
88
+ }
89
+
90
+ async function ensureGitignoreHasDotEnv(target: string): Promise<void> {
91
+ const file = path.join(target, ".gitignore");
92
+ let existing = "";
93
+ try {
94
+ existing = await fs.readFile(file, "utf8");
95
+ } catch {
96
+ // no .gitignore yet
97
+ }
98
+ const lines = existing.split("\n").map((l) => l.trim());
99
+ if (lines.includes(".env")) return;
100
+ const next = existing.length > 0 && !existing.endsWith("\n") ? `${existing}\n.env\n` : `${existing}.env\n`;
101
+ await fs.writeFile(file, next);
102
+ }