@dharta/cli 0.6.77 → 0.6.78

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 (293) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +215 -11
  3. package/dist/agent.js +3 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/api-error.js +6 -6
  6. package/dist/api-error.js.map +1 -1
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +860 -149
  9. package/dist/cli.js.map +1 -1
  10. package/dist/command-tree.d.ts +3 -0
  11. package/dist/command-tree.js +10 -0
  12. package/dist/command-tree.js.map +1 -0
  13. package/dist/commands/agents.d.ts +54 -17
  14. package/dist/commands/agents.js +246 -136
  15. package/dist/commands/agents.js.map +1 -1
  16. package/dist/commands/analytics.js +6 -5
  17. package/dist/commands/analytics.js.map +1 -1
  18. package/dist/commands/api.js +71 -86
  19. package/dist/commands/api.js.map +1 -1
  20. package/dist/commands/appearance.js +5 -6
  21. package/dist/commands/appearance.js.map +1 -1
  22. package/dist/commands/artifact.js +7 -6
  23. package/dist/commands/artifact.js.map +1 -1
  24. package/dist/commands/audit-log.js +2 -1
  25. package/dist/commands/audit-log.js.map +1 -1
  26. package/dist/commands/auth.js +26 -6
  27. package/dist/commands/auth.js.map +1 -1
  28. package/dist/commands/caps.js +11 -15
  29. package/dist/commands/caps.js.map +1 -1
  30. package/dist/commands/deploy.js +25 -10
  31. package/dist/commands/deploy.js.map +1 -1
  32. package/dist/commands/dev.d.ts +10 -1
  33. package/dist/commands/dev.js +56 -25
  34. package/dist/commands/dev.js.map +1 -1
  35. package/dist/commands/doctor.d.ts +2 -0
  36. package/dist/commands/doctor.js +60 -0
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/embed-keys.js +4 -13
  39. package/dist/commands/embed-keys.js.map +1 -1
  40. package/dist/commands/env.js +4 -7
  41. package/dist/commands/env.js.map +1 -1
  42. package/dist/commands/files.js +3 -4
  43. package/dist/commands/files.js.map +1 -1
  44. package/dist/commands/fleet.d.ts +129 -0
  45. package/dist/commands/fleet.js +408 -0
  46. package/dist/commands/fleet.js.map +1 -0
  47. package/dist/commands/genui.d.ts +2 -0
  48. package/dist/commands/genui.js +53 -0
  49. package/dist/commands/genui.js.map +1 -0
  50. package/dist/commands/git-credential.js +2 -3
  51. package/dist/commands/git-credential.js.map +1 -1
  52. package/dist/commands/kartas.d.ts +9 -0
  53. package/dist/commands/kartas.js +33 -11
  54. package/dist/commands/kartas.js.map +1 -1
  55. package/dist/commands/keys.d.ts +17 -18
  56. package/dist/commands/keys.js +108 -19
  57. package/dist/commands/keys.js.map +1 -1
  58. package/dist/commands/login.js +2 -1
  59. package/dist/commands/login.js.map +1 -1
  60. package/dist/commands/members.d.ts +10 -0
  61. package/dist/commands/members.js +20 -6
  62. package/dist/commands/members.js.map +1 -1
  63. package/dist/commands/model-connections.d.ts +44 -0
  64. package/dist/commands/model-connections.js +435 -0
  65. package/dist/commands/model-connections.js.map +1 -0
  66. package/dist/commands/open.d.ts +2 -2
  67. package/dist/commands/open.js +4 -3
  68. package/dist/commands/open.js.map +1 -1
  69. package/dist/commands/org.d.ts +9 -0
  70. package/dist/commands/org.js +42 -0
  71. package/dist/commands/org.js.map +1 -0
  72. package/dist/commands/organization.d.ts +16 -0
  73. package/dist/commands/organization.js +118 -0
  74. package/dist/commands/organization.js.map +1 -0
  75. package/dist/commands/parity.js +1 -3
  76. package/dist/commands/parity.js.map +1 -1
  77. package/dist/commands/repos.d.ts +4 -0
  78. package/dist/commands/repos.js +17 -0
  79. package/dist/commands/repos.js.map +1 -0
  80. package/dist/commands/rollback.js +2 -3
  81. package/dist/commands/rollback.js.map +1 -1
  82. package/dist/commands/schedules.js +26 -21
  83. package/dist/commands/schedules.js.map +1 -1
  84. package/dist/commands/scopes.js +8 -1
  85. package/dist/commands/scopes.js.map +1 -1
  86. package/dist/commands/sessions.js +9 -3
  87. package/dist/commands/sessions.js.map +1 -1
  88. package/dist/commands/setup.js +5 -4
  89. package/dist/commands/setup.js.map +1 -1
  90. package/dist/commands/update.d.ts +1 -0
  91. package/dist/commands/update.js +24 -11
  92. package/dist/commands/update.js.map +1 -1
  93. package/dist/commands/webhooks.d.ts +4 -1
  94. package/dist/commands/webhooks.js +21 -16
  95. package/dist/commands/webhooks.js.map +1 -1
  96. package/dist/completions.js +24 -13
  97. package/dist/completions.js.map +1 -1
  98. package/dist/config.d.ts +12 -0
  99. package/dist/config.js +121 -7
  100. package/dist/config.js.map +1 -1
  101. package/dist/context.d.ts +2 -0
  102. package/dist/context.js +23 -7
  103. package/dist/context.js.map +1 -1
  104. package/dist/docs/cli-reference.d.ts +2 -0
  105. package/dist/docs/cli-reference.js +191 -0
  106. package/dist/docs/cli-reference.js.map +1 -0
  107. package/dist/exit-codes.d.ts +25 -0
  108. package/dist/exit-codes.js +107 -0
  109. package/dist/exit-codes.js.map +1 -0
  110. package/dist/genui.d.ts +178 -0
  111. package/dist/genui.js +1130 -0
  112. package/dist/genui.js.map +1 -0
  113. package/dist/harnesses.js +2 -1
  114. package/dist/harnesses.js.map +1 -1
  115. package/dist/jq.d.ts +4 -0
  116. package/dist/jq.js +91 -0
  117. package/dist/jq.js.map +1 -0
  118. package/dist/login-offer.js +6 -2
  119. package/dist/login-offer.js.map +1 -1
  120. package/dist/manifest.d.ts +12 -0
  121. package/dist/manifest.js +126 -0
  122. package/dist/manifest.js.map +1 -1
  123. package/dist/model-auth.d.ts +29 -0
  124. package/dist/model-auth.js +117 -0
  125. package/dist/model-auth.js.map +1 -0
  126. package/dist/model-settings.d.ts +23 -0
  127. package/dist/model-settings.js +186 -0
  128. package/dist/model-settings.js.map +1 -0
  129. package/dist/money.js +2 -1
  130. package/dist/money.js.map +1 -1
  131. package/dist/output.d.ts +6 -0
  132. package/dist/output.js +54 -0
  133. package/dist/output.js.map +1 -1
  134. package/dist/runtime.d.ts +1 -1
  135. package/dist/runtime.js +1 -1
  136. package/dist/scaffold.js +2 -1
  137. package/dist/scaffold.js.map +1 -1
  138. package/dist/validators.js +17 -11
  139. package/dist/validators.js.map +1 -1
  140. package/dist/version-check.d.ts +1 -1
  141. package/dist/version-check.js +1 -1
  142. package/dist/version-check.js.map +1 -1
  143. package/node_modules/@dharta/contracts/LICENSE +21 -0
  144. package/node_modules/@dharta/contracts/README.md +45 -0
  145. package/node_modules/@dharta/contracts/artifact-runtime.json +750 -0
  146. package/node_modules/@dharta/contracts/dist/activity.d.ts +62 -0
  147. package/node_modules/@dharta/contracts/dist/activity.d.ts.map +1 -0
  148. package/node_modules/@dharta/contracts/dist/activity.js +178 -0
  149. package/node_modules/@dharta/contracts/dist/activity.js.map +1 -0
  150. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts +20 -0
  151. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts.map +1 -0
  152. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js +53 -0
  153. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js.map +1 -0
  154. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts +37 -0
  155. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts.map +1 -0
  156. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js +5107 -0
  157. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js.map +1 -0
  158. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts +154 -0
  159. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts.map +1 -0
  160. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts +1670 -0
  161. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts.map +1 -0
  162. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js +1903 -0
  163. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js.map +1 -0
  164. package/node_modules/@dharta/contracts/dist/artifact-runtime.js +377 -0
  165. package/node_modules/@dharta/contracts/dist/artifact-runtime.js.map +1 -0
  166. package/node_modules/@dharta/contracts/dist/index.d.ts +6 -0
  167. package/node_modules/@dharta/contracts/dist/index.d.ts.map +1 -0
  168. package/node_modules/@dharta/contracts/dist/index.js +6 -0
  169. package/node_modules/@dharta/contracts/dist/index.js.map +1 -0
  170. package/node_modules/@dharta/contracts/dist/model-auth.d.ts +31 -0
  171. package/node_modules/@dharta/contracts/dist/model-auth.d.ts.map +1 -0
  172. package/node_modules/@dharta/contracts/dist/model-auth.js +45 -0
  173. package/node_modules/@dharta/contracts/dist/model-auth.js.map +1 -0
  174. package/node_modules/@dharta/contracts/dist/tasks.d.ts +44 -0
  175. package/node_modules/@dharta/contracts/dist/tasks.d.ts.map +1 -0
  176. package/node_modules/@dharta/contracts/dist/tasks.js +127 -0
  177. package/node_modules/@dharta/contracts/dist/tasks.js.map +1 -0
  178. package/node_modules/@dharta/contracts/dist/wire-events.d.ts +71 -0
  179. package/node_modules/@dharta/contracts/dist/wire-events.d.ts.map +1 -0
  180. package/node_modules/@dharta/contracts/dist/wire-events.js +48 -0
  181. package/node_modules/@dharta/contracts/dist/wire-events.js.map +1 -0
  182. package/node_modules/@dharta/contracts/model-auth-capabilities-v1.json +168 -0
  183. package/node_modules/@dharta/contracts/model-auth-reasons-v1.json +38 -0
  184. package/node_modules/@dharta/contracts/package.json +44 -0
  185. package/node_modules/@dharta/contracts/src/activity.ts +215 -0
  186. package/node_modules/@dharta/contracts/src/artifact-runtime-formats.ts +54 -0
  187. package/node_modules/@dharta/contracts/src/artifact-runtime-validators.generated.ts +29 -0
  188. package/node_modules/@dharta/contracts/src/artifact-runtime.generated.ts +2093 -0
  189. package/node_modules/@dharta/contracts/src/artifact-runtime.ts +520 -0
  190. package/node_modules/@dharta/contracts/src/index.ts +5 -0
  191. package/node_modules/@dharta/contracts/src/model-auth.ts +82 -0
  192. package/node_modules/@dharta/contracts/src/tasks.ts +142 -0
  193. package/node_modules/@dharta/contracts/src/wire-events.ts +150 -0
  194. package/node_modules/@dharta/contracts/wire-events.json +29 -0
  195. package/node_modules/@dharta/sdk/LICENSE +1 -1
  196. package/node_modules/@dharta/sdk/README.md +22 -8
  197. package/node_modules/@dharta/sdk/dist/errors.d.ts +9 -7
  198. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -1
  199. package/node_modules/@dharta/sdk/dist/errors.js +36 -18
  200. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -1
  201. package/node_modules/@dharta/sdk/dist/fleet.d.ts +69 -0
  202. package/node_modules/@dharta/sdk/dist/fleet.d.ts.map +1 -0
  203. package/node_modules/@dharta/sdk/dist/fleet.js +240 -0
  204. package/node_modules/@dharta/sdk/dist/fleet.js.map +1 -0
  205. package/node_modules/@dharta/sdk/dist/http.d.ts +6 -2
  206. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -1
  207. package/node_modules/@dharta/sdk/dist/http.js +71 -12
  208. package/node_modules/@dharta/sdk/dist/http.js.map +1 -1
  209. package/node_modules/@dharta/sdk/dist/idempotency.d.ts +12 -0
  210. package/node_modules/@dharta/sdk/dist/idempotency.d.ts.map +1 -0
  211. package/node_modules/@dharta/sdk/dist/idempotency.js +72 -0
  212. package/node_modules/@dharta/sdk/dist/idempotency.js.map +1 -0
  213. package/node_modules/@dharta/sdk/dist/index.d.ts +20 -6
  214. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -1
  215. package/node_modules/@dharta/sdk/dist/index.js +27 -4
  216. package/node_modules/@dharta/sdk/dist/index.js.map +1 -1
  217. package/node_modules/@dharta/sdk/dist/resources.d.ts +108 -13
  218. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -1
  219. package/node_modules/@dharta/sdk/dist/resources.js +403 -57
  220. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -1
  221. package/node_modules/@dharta/sdk/dist/types.d.ts +436 -16
  222. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -1
  223. package/node_modules/@dharta/sdk/dist/types.js +31 -2
  224. package/node_modules/@dharta/sdk/dist/types.js.map +1 -1
  225. package/node_modules/@dharta/sdk/package.json +9 -3
  226. package/node_modules/@dharta/sdk/src/errors.ts +41 -18
  227. package/node_modules/@dharta/sdk/src/fleet.ts +339 -0
  228. package/node_modules/@dharta/sdk/src/http.ts +110 -17
  229. package/node_modules/@dharta/sdk/src/idempotency.ts +101 -0
  230. package/node_modules/@dharta/sdk/src/index.ts +50 -11
  231. package/node_modules/@dharta/sdk/src/resources.ts +549 -68
  232. package/node_modules/@dharta/sdk/src/types.ts +629 -20
  233. package/package.json +16 -6
  234. package/src/agent.ts +3 -2
  235. package/src/api-error.ts +6 -5
  236. package/src/cli.ts +978 -163
  237. package/src/command-tree.ts +13 -0
  238. package/src/commands/agents.ts +365 -167
  239. package/src/commands/analytics.ts +6 -5
  240. package/src/commands/api.ts +73 -92
  241. package/src/commands/appearance.ts +5 -6
  242. package/src/commands/artifact.ts +7 -6
  243. package/src/commands/audit-log.ts +2 -1
  244. package/src/commands/auth.ts +25 -5
  245. package/src/commands/caps.ts +11 -15
  246. package/src/commands/deploy.ts +18 -4
  247. package/src/commands/dev.ts +86 -25
  248. package/src/commands/doctor.ts +69 -0
  249. package/src/commands/embed-keys.ts +4 -14
  250. package/src/commands/env.ts +4 -7
  251. package/src/commands/files.ts +3 -4
  252. package/src/commands/fleet.ts +672 -0
  253. package/src/commands/genui.ts +59 -0
  254. package/src/commands/git-credential.ts +2 -3
  255. package/src/commands/kartas.ts +45 -11
  256. package/src/commands/keys.ts +152 -35
  257. package/src/commands/login.ts +2 -1
  258. package/src/commands/members.ts +31 -6
  259. package/src/commands/model-connections.ts +547 -0
  260. package/src/commands/open.ts +5 -4
  261. package/src/commands/org.ts +57 -0
  262. package/src/commands/organization.ts +153 -0
  263. package/src/commands/parity.ts +1 -3
  264. package/src/commands/repos.ts +23 -0
  265. package/src/commands/rollback.ts +2 -3
  266. package/src/commands/schedules.ts +33 -22
  267. package/src/commands/scopes.ts +8 -1
  268. package/src/commands/sessions.ts +9 -3
  269. package/src/commands/setup.ts +5 -4
  270. package/src/commands/update.ts +28 -12
  271. package/src/commands/webhooks.ts +30 -19
  272. package/src/completions.ts +25 -13
  273. package/src/config.ts +141 -9
  274. package/src/context.ts +34 -7
  275. package/src/docs/cli-reference.ts +206 -0
  276. package/src/exit-codes.ts +122 -0
  277. package/src/genui.ts +1398 -0
  278. package/src/harnesses.ts +2 -1
  279. package/src/jq.ts +109 -0
  280. package/src/login-offer.ts +6 -2
  281. package/src/manifest.ts +134 -0
  282. package/src/model-auth.ts +173 -0
  283. package/src/model-settings.ts +222 -0
  284. package/src/money.ts +5 -1
  285. package/src/output.ts +60 -0
  286. package/src/runtime.ts +1 -1
  287. package/src/scaffold.ts +2 -1
  288. package/src/validators.ts +18 -11
  289. package/src/version-check.ts +1 -1
  290. package/dist/commands/model-keys.d.ts +0 -22
  291. package/dist/commands/model-keys.js +0 -222
  292. package/dist/commands/model-keys.js.map +0 -1
  293. package/src/commands/model-keys.ts +0 -278
@@ -1,11 +1,14 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import type { CliContext } from "../context.js";
3
- import { WEBHOOK_EVENT_TYPES } from "@dharta/sdk";
2
+ import { WEBHOOK_EVENT_TYPES, type WebhookDelivery } from "@dharta/sdk";
4
3
  import { fmtTs, table } from "../output.js";
5
4
  import { closestChoice, normalizeNonBlank } from "../validators.js";
5
+ import { CliUsageError } from "../exit-codes.js";
6
6
 
7
+ // The shape `Api::WebhookEndpointsController#serialize` returns. `id` is the
8
+ // integer primary key; it read `string` here, in the SDK types and in the
9
+ // OpenAPI document, and the controller has always emitted a number.
7
10
  export interface WebhookEndpoint {
8
- id: string;
11
+ id: number;
9
12
  name: string;
10
13
  url: string;
11
14
  event_types: string[];
@@ -38,7 +41,7 @@ export async function webhooksList(ctx: CliContext): Promise<string> {
38
41
  const data = await webhooksListData(ctx);
39
42
  if (data.length === 0) return "No webhook endpoints. Create one with: dharta webhooks create <name> <url>";
40
43
  return table(data.map((endpoint) => ({
41
- id: endpoint.id,
44
+ id: String(endpoint.id),
42
45
  name: endpoint.name,
43
46
  url: endpoint.url,
44
47
  state: endpoint.enabled ? "enabled" : "disabled",
@@ -59,7 +62,6 @@ export async function webhooksCreateData(
59
62
  return ctx.http.request<EndpointEnvelope>({
60
63
  method: "POST",
61
64
  path: "/api/webhook_endpoints",
62
- headers: idempotencyHeader(),
63
65
  body: { name: normalizedName, url: normalizedUrl, event_types: parseEvents(opts.event) },
64
66
  });
65
67
  }
@@ -85,12 +87,11 @@ export async function webhooksUpdateData(
85
87
  if (opts.url !== undefined) body.url = normalizeWebhookUrl(opts.url);
86
88
  if (opts.event !== undefined) body.event_types = parseEvents(opts.event);
87
89
  if (Object.keys(body).length === 0) {
88
- throw new Error("Pass at least one update option: --name, --url, or --event.");
90
+ throw new CliUsageError("Pass at least one update option: --name, --url, or --event.");
89
91
  }
90
92
  return ctx.http.request<EndpointEnvelope>({
91
93
  method: "PATCH",
92
94
  path: `/api/webhook_endpoints/${encodeURIComponent(normalizedId)}`,
93
- headers: idempotencyHeader(),
94
95
  body,
95
96
  });
96
97
  }
@@ -115,7 +116,7 @@ export function validateWebhooksUpdateInput(id: string, opts: { name?: string; u
115
116
  if (hasUrl) normalizeWebhookUrl(opts.url!);
116
117
  if (hasEvent) parseEvents(opts.event);
117
118
  if (!hasName && !hasUrl && !hasEvent) {
118
- throw new Error("Pass at least one update option: --name, --url, or --event.");
119
+ throw new CliUsageError("Pass at least one update option: --name, --url, or --event.");
119
120
  }
120
121
  }
121
122
 
@@ -124,7 +125,6 @@ export async function webhooksEnableData(ctx: CliContext, id: string): Promise<E
124
125
  return ctx.http.request<EndpointEnvelope>({
125
126
  method: "POST",
126
127
  path: `/api/webhook_endpoints/${encodeURIComponent(normalizedId)}/enable`,
127
- headers: idempotencyHeader(),
128
128
  });
129
129
  }
130
130
 
@@ -133,12 +133,27 @@ export async function webhooksEnable(ctx: CliContext, id: string): Promise<strin
133
133
  return `Enabled webhook endpoint ${res.webhook_endpoint.id}.`;
134
134
  }
135
135
 
136
+ // Queues one probe through the production delivery path: the same delivery
137
+ // row, the same signature, the same retry schedule. The answer is the QUEUED
138
+ // delivery, not its outcome - read that from `dharta webhooks list` and the
139
+ // deliveries the endpoint records.
140
+ export async function webhooksTestData(ctx: CliContext, id: string): Promise<WebhookDelivery> {
141
+ return ctx.sdk.webhookEndpoints.test(normalizeWebhookId(id));
142
+ }
143
+
144
+ export async function webhooksTest(ctx: CliContext, id: string): Promise<string> {
145
+ const delivery = await webhooksTestData(ctx, id);
146
+ return (
147
+ `Queued ${delivery.event_type} delivery ${delivery.id} (event ${delivery.event_id}) ` +
148
+ "to the endpoint. Its outcome lands on the delivery record, not here."
149
+ );
150
+ }
151
+
136
152
  export async function webhooksDeleteData(ctx: CliContext, id: string): Promise<{ id: string; deleted: boolean }> {
137
153
  const normalizedId = normalizeWebhookId(id);
138
154
  await ctx.http.request<void>({
139
155
  method: "DELETE",
140
156
  path: `/api/webhook_endpoints/${encodeURIComponent(normalizedId)}`,
141
- headers: idempotencyHeader(),
142
157
  });
143
158
  return { id: normalizedId, deleted: true };
144
159
  }
@@ -155,13 +170,13 @@ export function parseEvents(input: string[] | undefined): string[] {
155
170
  .map((value) => value.trim())
156
171
  .filter(Boolean);
157
172
  if (events.length === 0) {
158
- throw new Error("--event must include at least one event type, e.g. --event turn.completed.");
173
+ throw new CliUsageError("--event must include at least one event type, e.g. --event turn.completed.");
159
174
  }
160
175
  const allowed = new Set<string>(WEBHOOK_EVENT_TYPES);
161
176
  const unknown = events.filter((event) => !allowed.has(event));
162
177
  if (unknown.length > 0) {
163
178
  const suggestion = unknown.length === 1 ? closestChoice(unknown[0]!, WEBHOOK_EVENT_TYPES) : null;
164
- throw new Error(
179
+ throw new CliUsageError(
165
180
  `Unknown --event ${unknown.join(", ")}. Allowed events: ${WEBHOOK_EVENT_TYPES.join(", ")}.` +
166
181
  (suggestion ? ` Did you mean ${suggestion}?` : ""),
167
182
  );
@@ -169,14 +184,10 @@ export function parseEvents(input: string[] | undefined): string[] {
169
184
  return events;
170
185
  }
171
186
 
172
- function idempotencyHeader(): Record<string, string> {
173
- return { "Idempotency-Key": randomUUID() };
174
- }
175
-
176
187
  function normalizeRequired(value: string, label: string): string {
177
188
  const normalized = value.trim();
178
189
  if (!normalized) {
179
- throw new Error(`${label} cannot be blank.`);
190
+ throw new CliUsageError(`${label} cannot be blank.`);
180
191
  }
181
192
  return normalized;
182
193
  }
@@ -187,10 +198,10 @@ function normalizeWebhookUrl(value: string): string {
187
198
  try {
188
199
  parsed = new URL(normalized);
189
200
  } catch {
190
- throw new Error("Webhook URL must be an absolute http(s) URL.");
201
+ throw new CliUsageError("Webhook URL must be an absolute http(s) URL.");
191
202
  }
192
203
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
193
- throw new Error("Webhook URL must be an absolute http(s) URL.");
204
+ throw new CliUsageError("Webhook URL must be an absolute http(s) URL.");
194
205
  }
195
206
  return normalized;
196
207
  }
@@ -4,6 +4,7 @@
4
4
  // CLI version's command surface; regenerate after updating.
5
5
 
6
6
  import type { Argument, Command, Option } from "commander";
7
+ import { visibleSubcommands } from "./command-tree.js";
7
8
 
8
9
  interface CommandSpec {
9
10
  name: string;
@@ -17,6 +18,12 @@ interface CommandSpec {
17
18
  subs: CommandSpec[];
18
19
  }
19
20
 
21
+ // zsh looks up `_<command>` by convention and bash needs a legal shell
22
+ // identifier, so non-word characters in the binary name become underscores.
23
+ function completionFunctionName(bin: string): string {
24
+ return `_${bin.replace(/[^A-Za-z0-9_]/g, "_")}`;
25
+ }
26
+
20
27
  export function specFromCommand(cmd: Command): CommandSpec {
21
28
  return {
22
29
  name: cmd.name(),
@@ -27,9 +34,7 @@ export function specFromCommand(cmd: Command): CommandSpec {
27
34
  "--help",
28
35
  ],
29
36
  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)),
37
+ subs: visibleSubcommands(cmd).map((c) => specFromCommand(c)),
33
38
  };
34
39
  }
35
40
 
@@ -114,10 +119,15 @@ function escapeZshDescription(value: string): string {
114
119
  export function bashCompletion(program: Command): string {
115
120
  const spec = specFromCommand(program);
116
121
  const entries = completionEntries(spec);
122
+ // The binary name comes from the program, never a literal: these scripts
123
+ // bound `karta` for the whole life of the renamed CLI, so `complete -F`
124
+ // attached to a command that no longer exists and completion was dead.
125
+ const bin = program.name();
126
+ const fn = completionFunctionName(bin);
117
127
  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() {",
128
+ `# bash completion for ${bin} - generated by \`${bin} completion bash\``,
129
+ `# Load with: source <(${bin} completion bash) (or append to ~/.bash_completion)`,
130
+ `${fn}() {`,
121
131
  ' local cur="${COMP_WORDS[COMP_CWORD]}"',
122
132
  ' local path="${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}"',
123
133
  ' local words=""',
@@ -130,7 +140,7 @@ export function bashCompletion(program: Command): string {
130
140
  " esac",
131
141
  ' COMPREPLY=( $(compgen -W "$words" -- "$cur") )',
132
142
  "}",
133
- "complete -F _karta karta",
143
+ `complete -F ${fn} ${bin}`,
134
144
  );
135
145
  return lines.join("\n");
136
146
  }
@@ -138,13 +148,15 @@ export function bashCompletion(program: Command): string {
138
148
  export function zshCompletion(program: Command): string {
139
149
  const spec = specFromCommand(program);
140
150
  const entries = completionEntries(spec);
151
+ const bin = program.name();
152
+ const fn = completionFunctionName(bin);
141
153
 
142
154
  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)",
155
+ `#compdef ${bin}`,
156
+ `# zsh completion for ${bin} - generated by \`${bin} completion zsh\``,
157
+ `# Install: ${bin} completion zsh > ~/.zfunc/${fn} (with fpath+=(~/.zfunc) before compinit)`,
146
158
  "",
147
- "_karta() {",
159
+ `${fn}() {`,
148
160
  " local -a reply",
149
161
  ' local path=""',
150
162
  " if (( CURRENT > 2 )); then",
@@ -156,13 +168,13 @@ export function zshCompletion(program: Command): string {
156
168
  lines.push(
157
169
  ` ${zshCasePattern(entry.path)})`,
158
170
  entry.describedCommands
159
- ? ` reply=( ${entry.describedCommands} ); _describe -t commands 'dharta command' reply`
171
+ ? ` reply=( ${entry.describedCommands} ); _describe -t commands '${bin} command' reply`
160
172
  : " true",
161
173
  entry.words ? ` compadd -- ${entry.words}` : " true",
162
174
  " return",
163
175
  " ;;",
164
176
  );
165
177
  }
166
- lines.push(" esac", "}", "", '_karta "$@"');
178
+ lines.push(" esac", "}", "", `${fn} "$@"`);
167
179
  return lines.join("\n");
168
180
  }
package/src/config.ts CHANGED
@@ -1,7 +1,11 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { promises as fs } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import * as path from "node:path";
4
5
  import TOML from "@iarna/toml";
6
+ import { assertIdempotencyKey, MAX_IDEMPOTENCY_KEY_LENGTH } from "@dharta/sdk";
7
+ import { CliUsageError } from "./exit-codes.js";
8
+ import { normalizeNonBlank } from "./validators.js";
5
9
 
6
10
  // One host schema (RFC 0013 CLI-7, executing RFC 0011 HOST-5's CLI
7
11
  // note). D11's coordinate system has three hosts, each with one auth
@@ -37,6 +41,115 @@ export function setHomeForTesting(home: string | undefined): void {
37
41
  homeOverride = home;
38
42
  }
39
43
 
44
+ // The profile named by the global `--profile` flag, recorded once per run by
45
+ // the root preAction hook in cli.ts. Resolution order for every command is:
46
+ // explicit --profile, then DHARTA_PROFILE, then the config's default_profile
47
+ // (the aws --profile shape, RFC 0048 D2).
48
+ let requestedProfile: string | undefined;
49
+
50
+ export function setRequestedProfile(name: string | undefined): void {
51
+ requestedProfile = name === undefined ? undefined : normalizeNonBlank(name, "--profile");
52
+ }
53
+
54
+ // The Idempotency-Key named by the global `--idempotency-key` flag, recorded
55
+ // once per run by the same preAction hook. It rides here rather than in each
56
+ // command because it is a property of the RUN, not of one call: a CI job that
57
+ // reruns after an ambiguous failure has to present the key the first run used,
58
+ // and every curated command mints a fresh one per process otherwise.
59
+ //
60
+ // What a key may contain is the SDK's rule (`assertIdempotencyKey`), written
61
+ // down once in sdks/typescript/src/idempotency.ts and enforced by the client on
62
+ // every key it is handed. The CLI checks it here as well, so a bad flag value is
63
+ // a usage error before any request goes out rather than a failure part way
64
+ // through a run.
65
+
66
+ // A command that performs more than one mutation cannot send the root key on
67
+ // both: the server binds a key to the first request it saw and answers the
68
+ // second with a 422. Such a command derives one key per operation instead (see
69
+ // mintIdempotencyKey), which keeps the writes apart from each other and stable
70
+ // across reruns. The cap applies to the WHOLE header value, so the root has to
71
+ // leave room for the longest suffix we append.
72
+ export const MAX_OPERATION_SUFFIX_LENGTH = 55;
73
+ export { MAX_IDEMPOTENCY_KEY_LENGTH };
74
+ export const MAX_ROOT_IDEMPOTENCY_KEY_LENGTH =
75
+ MAX_IDEMPOTENCY_KEY_LENGTH - MAX_OPERATION_SUFFIX_LENGTH;
76
+
77
+ let requestedIdempotencyKey: string | undefined;
78
+
79
+ export function setRequestedIdempotencyKey(key: string | undefined): void {
80
+ if (key === undefined) {
81
+ requestedIdempotencyKey = undefined;
82
+ return;
83
+ }
84
+ const normalized = normalizeNonBlank(key, "--idempotency-key");
85
+ try {
86
+ assertIdempotencyKey(normalized, "--idempotency-key");
87
+ } catch (error) {
88
+ // The SDK owns what a key may contain; the CLI owns what a rejected flag
89
+ // value exits with. Re-wrapping keeps both true and keeps the wording in
90
+ // one place.
91
+ throw new CliUsageError((error as Error).message);
92
+ }
93
+ if (normalized.length > MAX_ROOT_IDEMPOTENCY_KEY_LENGTH) {
94
+ throw new CliUsageError(
95
+ `--idempotency-key must be at most ${MAX_ROOT_IDEMPOTENCY_KEY_LENGTH} characters. ` +
96
+ `The server accepts ${MAX_IDEMPOTENCY_KEY_LENGTH}; the difference is reserved for the ` +
97
+ "per-operation suffix a multi-mutation command appends.",
98
+ );
99
+ }
100
+ requestedIdempotencyKey = normalized;
101
+ }
102
+
103
+ export function requestedIdempotencyKeyValue(): string | undefined {
104
+ return requestedIdempotencyKey;
105
+ }
106
+
107
+ // Dotted lowercase, so a derived key reads as `<root>.sessions.turn.2` and the
108
+ // operation cannot smuggle in whitespace or a separator of its own.
109
+ const IDEMPOTENCY_OPERATION_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
110
+
111
+ // The Idempotency-Key for ONE mutation. With no root key this is a fresh UUID,
112
+ // which is what every command did before the flag existed. With a root key it
113
+ // is `<root>.<operation>`: two mutations in the same run get different keys, and
114
+ // the same mutation in a later run with the same root gets the same key, which
115
+ // is what makes the rerun replay instead of repeating the work.
116
+ //
117
+ // `operation` is written by us, never by the caller, so a bad one is a
118
+ // programming error and throws a plain Error (exit 1), not a usage error.
119
+ export function mintIdempotencyKey(operation: string): string {
120
+ if (!IDEMPOTENCY_OPERATION_PATTERN.test(operation)) {
121
+ throw new Error(
122
+ `Idempotency operation ${JSON.stringify(operation)} is not a dotted lowercase token.`,
123
+ );
124
+ }
125
+ const suffix = `.${operation}`;
126
+ if (suffix.length > MAX_OPERATION_SUFFIX_LENGTH) {
127
+ throw new Error(
128
+ `Idempotency operation ${JSON.stringify(operation)} exceeds the ` +
129
+ `${MAX_OPERATION_SUFFIX_LENGTH}-character reserve.`,
130
+ );
131
+ }
132
+ return requestedIdempotencyKey === undefined
133
+ ? randomUUID()
134
+ : `${requestedIdempotencyKey}${suffix}`;
135
+ }
136
+
137
+ // The profile named by the flag alone, ignoring DHARTA_PROFILE. An explicit
138
+ // flag is a deliberate act and outranks an ambient credential in the
139
+ // environment; the env var does not.
140
+ export function explicitProfileName(): string | undefined {
141
+ return requestedProfile;
142
+ }
143
+
144
+ // The explicitly requested profile, or undefined to mean "the default one".
145
+ // Commands that operate ON a profile (login, auth logout) read this rather
146
+ // than the resolved name, so omitting the flag keeps their existing meaning.
147
+ export function requestedProfileName(): string | undefined {
148
+ if (requestedProfile !== undefined) return requestedProfile;
149
+ const fromEnv = process.env.DHARTA_PROFILE?.trim();
150
+ return fromEnv ? fromEnv : undefined;
151
+ }
152
+
40
153
  export function configPath(): string {
41
154
  const home = homeOverride ?? process.env.HOME ?? homedir();
42
155
  return path.join(home, ".dharta", "config.toml");
@@ -109,17 +222,21 @@ export async function removeProfile(name: string): Promise<boolean> {
109
222
 
110
223
  export async function activeProfile(): Promise<Profile> {
111
224
  const cfg = await loadConfig();
225
+ const requested = requestedProfileName();
112
226
  if (!cfg) {
113
227
  throw new Error(
114
- "Not logged in. Run `dharta login` to store an API key in ~/.dharta/config.toml.",
228
+ `Not logged in. Run \`dharta login${requested ? ` --profile ${requested}` : ""}\` ` +
229
+ "to store an API key in ~/.dharta/config.toml.",
115
230
  );
116
231
  }
117
- const profile = cfg.profiles[cfg.default_profile];
232
+ const name = requested ?? cfg.default_profile;
233
+ const profile = cfg.profiles[name];
118
234
  if (!profile) {
119
- throw new Error(
120
- `Default profile '${cfg.default_profile}' not found in ${configPath()}. ` +
121
- "Run `dharta login` to recreate it.",
122
- );
235
+ throw requested
236
+ ? profileNotFoundError(name)
237
+ : new CliUsageError(
238
+ `Default profile '${name}' not found in ${configPath()}. Run \`dharta login\` to recreate it.`,
239
+ );
123
240
  }
124
241
  return {
125
242
  api_key: profile.api_key,
@@ -129,13 +246,28 @@ export async function activeProfile(): Promise<Profile> {
129
246
  };
130
247
  }
131
248
 
249
+ // One message for "you named a profile that is not stored", shared by
250
+ // activeProfile, `auth token`, and `auth logout` so the three agree on what
251
+ // happened and on the exit code (2, a usage error).
252
+ export function profileNotFoundError(name: string): CliUsageError {
253
+ return new CliUsageError(
254
+ `Profile '${name}' not found in ${configPath()}. ` +
255
+ `Run \`dharta login --profile ${name}\` to create it, or \`dharta auth status\` to list profiles.`,
256
+ );
257
+ }
258
+
132
259
  // Base URL for commands that work without a credential (init's template
133
- // fetch): env override, then the stored profile, then the default.
260
+ // fetch): env override, then the stored profile, then the default. An explicit
261
+ // --profile pins the host to that profile, the same rule buildContext applies.
134
262
  export async function resolveBaseUrl(): Promise<string> {
135
- if (process.env.DHARTA_BASE_URL) return process.env.DHARTA_BASE_URL;
263
+ const pinnedToProfile = explicitProfileName() !== undefined;
264
+ if (process.env.DHARTA_BASE_URL && !pinnedToProfile) return process.env.DHARTA_BASE_URL;
136
265
  try {
137
266
  return (await activeProfile()).base_url;
138
- } catch {
267
+ } catch (err) {
268
+ // A profile the user named and that does not exist is their mistake, not a
269
+ // missing credential: say so instead of quietly using the default host.
270
+ if (pinnedToProfile) throw err;
139
271
  return DEFAULT_BASE_URL;
140
272
  }
141
273
  }
package/src/context.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Dharta, HttpClient } from "@dharta/sdk";
2
- import { activeProfile, DEFAULT_BASE_URL } from "./config.js";
2
+ import {
3
+ activeProfile,
4
+ explicitProfileName,
5
+ requestedIdempotencyKeyValue,
6
+ DEFAULT_BASE_URL,
7
+ } from "./config.js";
3
8
  import { cliFormatError, cliFormatNetworkError } from "./api-error.js";
4
9
 
5
10
  export interface CliContext {
@@ -13,6 +18,10 @@ export interface CliContext {
13
18
  // `dharta files` can build a second HttpClient for the DATA-PLANE files host (a
14
19
  // different origin than the control-plane base_url) while staying testable.
15
20
  fetch?: typeof fetch;
21
+ // The run-wide `--idempotency-key`, already resolved. A command that builds a
22
+ // second control-plane client hands it over so the flag reaches that client's
23
+ // writes too; the two clients then behave identically.
24
+ idempotencyKey?: string;
16
25
  }
17
26
 
18
27
  // Allow tests to inject a stub fetch + base config without touching disk.
@@ -20,6 +29,9 @@ export interface CtxOverrides {
20
29
  apiKey?: string;
21
30
  baseUrl?: string;
22
31
  fetch?: typeof fetch;
32
+ // Normally read off the global `--idempotency-key` flag. Tests that never go
33
+ // through the program's preAction hook pass it here.
34
+ idempotencyKey?: string;
23
35
  }
24
36
 
25
37
  export async function buildContext(
@@ -27,11 +39,17 @@ export async function buildContext(
27
39
  ): Promise<CliContext> {
28
40
  let apiKey = over.apiKey;
29
41
  let baseUrl = over.baseUrl;
30
- // Env overrides config everywhere (the GH_TOKEN pattern, RFC 0013
31
- // §2); explicit per-call overrides beat both. DHARTA_BASE_URL applies
32
- // regardless of where the key came from, matching resolveBaseUrl.
33
- baseUrl = baseUrl ?? process.env.DHARTA_BASE_URL;
34
- if (!apiKey && process.env.DHARTA_API_KEY) {
42
+ // An explicit `--profile` outranks BOTH ambient settings: the key and the
43
+ // host are one credential. Without this, a shell that exports a key would
44
+ // silently send `--profile staging` at whatever org that key belongs to, and
45
+ // an exported DHARTA_BASE_URL would send the profile's key to a host it was
46
+ // never issued for. DHARTA_PROFILE does NOT get this treatment: two ambient
47
+ // settings, and the environment still wins.
48
+ const pinnedToProfile = explicitProfileName() !== undefined;
49
+ // Env overrides config everywhere (the GH_TOKEN pattern, RFC 0013 §2);
50
+ // explicit per-call overrides beat both.
51
+ if (!pinnedToProfile) baseUrl = baseUrl ?? process.env.DHARTA_BASE_URL;
52
+ if (!apiKey && process.env.DHARTA_API_KEY && !pinnedToProfile) {
35
53
  apiKey = process.env.DHARTA_API_KEY;
36
54
  }
37
55
  if (!apiKey) {
@@ -41,12 +59,20 @@ export async function buildContext(
41
59
  }
42
60
  if (!baseUrl) baseUrl = DEFAULT_BASE_URL;
43
61
  const fetchImpl = over.fetch;
62
+ // The global `--idempotency-key`, applied once here rather than threaded
63
+ // through every curated command. The client sends it on every request that
64
+ // does not name a key of its own, which is what makes a CI rerun after an
65
+ // ambiguous failure replay the first response instead of performing the
66
+ // mutation a second time. A command that writes more than once names a
67
+ // derived key per write, and those survive.
68
+ const idempotencyKey = over.idempotencyKey ?? requestedIdempotencyKeyValue();
44
69
  const sdk = new Dharta({
45
70
  apiKey,
46
71
  baseUrl,
47
72
  fetch: fetchImpl,
48
73
  formatError: cliFormatError,
49
74
  formatNetworkError: cliFormatNetworkError,
75
+ idempotencyKey,
50
76
  });
51
77
  const http = new HttpClient({
52
78
  apiKey,
@@ -54,6 +80,7 @@ export async function buildContext(
54
80
  fetch: fetchImpl,
55
81
  formatError: cliFormatError,
56
82
  formatNetworkError: cliFormatNetworkError,
83
+ idempotencyKey,
57
84
  });
58
- return { sdk, http, baseUrl, apiKey, fetch: fetchImpl };
85
+ return { sdk, http, baseUrl, apiKey, fetch: fetchImpl, idempotencyKey };
59
86
  }