primitive-admin 1.0.61 → 1.0.62

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 (300) hide show
  1. package/README.md +125 -69
  2. package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
  3. package/dist/bin/primitive.js +25 -14
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +36 -16
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +49 -188
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/blob-buckets.js +13 -153
  10. package/dist/src/commands/blob-buckets.js.map +1 -1
  11. package/dist/src/commands/collection-type-configs.js +14 -100
  12. package/dist/src/commands/collection-type-configs.js.map +1 -1
  13. package/dist/src/commands/collections.js +47 -8
  14. package/dist/src/commands/collections.js.map +1 -1
  15. package/dist/src/commands/config.d.ts +46 -0
  16. package/dist/src/commands/config.js +479 -0
  17. package/dist/src/commands/config.js.map +1 -0
  18. package/dist/src/commands/connections.js +12 -7
  19. package/dist/src/commands/connections.js.map +1 -1
  20. package/dist/src/commands/cron-triggers.js +84 -176
  21. package/dist/src/commands/cron-triggers.js.map +1 -1
  22. package/dist/src/commands/database-type-configs.d.ts +2 -0
  23. package/dist/src/commands/database-type-configs.js +171 -0
  24. package/dist/src/commands/database-type-configs.js.map +1 -0
  25. package/dist/src/commands/database-types.js +4 -4
  26. package/dist/src/commands/database-types.js.map +1 -1
  27. package/dist/src/commands/databases.d.ts +63 -0
  28. package/dist/src/commands/databases.js +649 -87
  29. package/dist/src/commands/databases.js.map +1 -1
  30. package/dist/src/commands/documents.js +869 -12
  31. package/dist/src/commands/documents.js.map +1 -1
  32. package/dist/src/commands/email-templates.js +18 -125
  33. package/dist/src/commands/email-templates.js.map +1 -1
  34. package/dist/src/commands/env.d.ts +12 -1
  35. package/dist/src/commands/env.js +98 -25
  36. package/dist/src/commands/env.js.map +1 -1
  37. package/dist/src/commands/feature-flags.d.ts +14 -0
  38. package/dist/src/commands/feature-flags.js +116 -0
  39. package/dist/src/commands/feature-flags.js.map +1 -0
  40. package/dist/src/commands/group-type-configs.js +14 -109
  41. package/dist/src/commands/group-type-configs.js.map +1 -1
  42. package/dist/src/commands/init.d.ts +9 -1
  43. package/dist/src/commands/init.js +1056 -277
  44. package/dist/src/commands/init.js.map +1 -1
  45. package/dist/src/commands/integrations.js +136 -421
  46. package/dist/src/commands/integrations.js.map +1 -1
  47. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  48. package/dist/src/commands/metadata-category-configs.js +112 -0
  49. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  50. package/dist/src/commands/metadata.js +33 -141
  51. package/dist/src/commands/metadata.js.map +1 -1
  52. package/dist/src/commands/prompts.js +206 -563
  53. package/dist/src/commands/prompts.js.map +1 -1
  54. package/dist/src/commands/rule-sets.d.ts +1 -1
  55. package/dist/src/commands/rule-sets.js +16 -132
  56. package/dist/src/commands/rule-sets.js.map +1 -1
  57. package/dist/src/commands/scripts.js +67 -234
  58. package/dist/src/commands/scripts.js.map +1 -1
  59. package/dist/src/commands/sessions.js +17 -5
  60. package/dist/src/commands/sessions.js.map +1 -1
  61. package/dist/src/commands/sync-app-settings.d.ts +113 -70
  62. package/dist/src/commands/sync-app-settings.js +193 -242
  63. package/dist/src/commands/sync-app-settings.js.map +1 -1
  64. package/dist/src/commands/sync.d.ts +1932 -49
  65. package/dist/src/commands/sync.js +9521 -1769
  66. package/dist/src/commands/sync.js.map +1 -1
  67. package/dist/src/commands/tokens.js +8 -2
  68. package/dist/src/commands/tokens.js.map +1 -1
  69. package/dist/src/commands/users.js +92 -1
  70. package/dist/src/commands/users.js.map +1 -1
  71. package/dist/src/commands/vars.js +44 -58
  72. package/dist/src/commands/vars.js.map +1 -1
  73. package/dist/src/commands/webhooks.js +318 -156
  74. package/dist/src/commands/webhooks.js.map +1 -1
  75. package/dist/src/commands/workflows.d.ts +49 -12
  76. package/dist/src/commands/workflows.js +652 -1086
  77. package/dist/src/commands/workflows.js.map +1 -1
  78. package/dist/src/lib/access-rule-display.d.ts +21 -0
  79. package/dist/src/lib/access-rule-display.js +34 -0
  80. package/dist/src/lib/access-rule-display.js.map +1 -0
  81. package/dist/src/lib/api-client.d.ts +541 -61
  82. package/dist/src/lib/api-client.js +559 -102
  83. package/dist/src/lib/api-client.js.map +1 -1
  84. package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
  85. package/dist/src/lib/app-settings-descriptor.js +403 -82
  86. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  87. package/dist/src/lib/canonical-json.d.ts +12 -0
  88. package/dist/src/lib/canonical-json.js +35 -0
  89. package/dist/src/lib/canonical-json.js.map +1 -0
  90. package/dist/src/lib/cli-manifest.d.ts +8 -0
  91. package/dist/src/lib/cli-manifest.js +1 -0
  92. package/dist/src/lib/cli-manifest.js.map +1 -1
  93. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
  94. package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
  95. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  96. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
  97. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
  98. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  99. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  100. package/dist/src/lib/config-object-descriptor.js +658 -0
  101. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  102. package/dist/src/lib/config-payload.d.ts +85 -0
  103. package/dist/src/lib/config-payload.js +116 -0
  104. package/dist/src/lib/config-payload.js.map +1 -0
  105. package/dist/src/lib/config-surface.d.ts +130 -0
  106. package/dist/src/lib/config-surface.js +300 -0
  107. package/dist/src/lib/config-surface.js.map +1 -0
  108. package/dist/src/lib/credentials-store.d.ts +10 -0
  109. package/dist/src/lib/credentials-store.js +23 -0
  110. package/dist/src/lib/credentials-store.js.map +1 -1
  111. package/dist/src/lib/data-input.d.ts +23 -0
  112. package/dist/src/lib/data-input.js +50 -0
  113. package/dist/src/lib/data-input.js.map +1 -0
  114. package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
  115. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  116. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
  117. package/dist/src/lib/env-resolver-core.d.ts +147 -0
  118. package/dist/src/lib/env-resolver-core.js +265 -0
  119. package/dist/src/lib/env-resolver-core.js.map +1 -0
  120. package/dist/src/lib/env-resolver.d.ts +28 -6
  121. package/dist/src/lib/env-resolver.js +44 -32
  122. package/dist/src/lib/env-resolver.js.map +1 -1
  123. package/dist/src/lib/generated-allowlist.js +12 -0
  124. package/dist/src/lib/generated-allowlist.js.map +1 -1
  125. package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
  126. package/dist/src/lib/generated-config-surfaces.js +4058 -0
  127. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  128. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  129. package/dist/src/lib/generated-template-lint.js +624 -0
  130. package/dist/src/lib/generated-template-lint.js.map +1 -0
  131. package/dist/src/lib/init-adopt.d.ts +16 -0
  132. package/dist/src/lib/init-adopt.js +34 -0
  133. package/dist/src/lib/init-adopt.js.map +1 -0
  134. package/dist/src/lib/init-assets.d.ts +39 -0
  135. package/dist/src/lib/init-assets.js +97 -0
  136. package/dist/src/lib/init-assets.js.map +1 -0
  137. package/dist/src/lib/init-config.d.ts +44 -5
  138. package/dist/src/lib/init-config.js +80 -7
  139. package/dist/src/lib/init-config.js.map +1 -1
  140. package/dist/src/lib/init-ios-links.d.ts +50 -0
  141. package/dist/src/lib/init-ios-links.js +153 -0
  142. package/dist/src/lib/init-ios-links.js.map +1 -0
  143. package/dist/src/lib/init-plan.d.ts +80 -0
  144. package/dist/src/lib/init-plan.js +95 -0
  145. package/dist/src/lib/init-plan.js.map +1 -0
  146. package/dist/src/lib/init-production-env.d.ts +48 -0
  147. package/dist/src/lib/init-production-env.js +59 -0
  148. package/dist/src/lib/init-production-env.js.map +1 -0
  149. package/dist/src/lib/init-schema.d.ts +74 -0
  150. package/dist/src/lib/init-schema.js +358 -0
  151. package/dist/src/lib/init-schema.js.map +1 -0
  152. package/dist/src/lib/init-xcode.d.ts +33 -0
  153. package/dist/src/lib/init-xcode.js +114 -0
  154. package/dist/src/lib/init-xcode.js.map +1 -0
  155. package/dist/src/lib/integration-request-config.d.ts +30 -0
  156. package/dist/src/lib/integration-request-config.js +145 -0
  157. package/dist/src/lib/integration-request-config.js.map +1 -0
  158. package/dist/src/lib/local-state.d.ts +55 -0
  159. package/dist/src/lib/local-state.js +167 -0
  160. package/dist/src/lib/local-state.js.map +1 -0
  161. package/dist/src/lib/log-inspection.d.ts +568 -0
  162. package/dist/src/lib/log-inspection.js +639 -0
  163. package/dist/src/lib/log-inspection.js.map +1 -0
  164. package/dist/src/lib/migration-nag.d.ts +2 -2
  165. package/dist/src/lib/migration-nag.js +3 -3
  166. package/dist/src/lib/object-status-filter.d.ts +22 -0
  167. package/dist/src/lib/object-status-filter.js +45 -0
  168. package/dist/src/lib/object-status-filter.js.map +1 -0
  169. package/dist/src/lib/output.d.ts +24 -1
  170. package/dist/src/lib/output.js +42 -1
  171. package/dist/src/lib/output.js.map +1 -1
  172. package/dist/src/lib/package-manager.d.ts +140 -0
  173. package/dist/src/lib/package-manager.js +305 -0
  174. package/dist/src/lib/package-manager.js.map +1 -0
  175. package/dist/src/lib/paginate.d.ts +50 -0
  176. package/dist/src/lib/paginate.js +53 -0
  177. package/dist/src/lib/paginate.js.map +1 -1
  178. package/dist/src/lib/platform-owned.d.ts +29 -3
  179. package/dist/src/lib/platform-owned.js +42 -3
  180. package/dist/src/lib/platform-owned.js.map +1 -1
  181. package/dist/src/lib/project-config.js +13 -5
  182. package/dist/src/lib/project-config.js.map +1 -1
  183. package/dist/src/lib/query-operators.d.ts +1 -1
  184. package/dist/src/lib/query-operators.js +1 -1
  185. package/dist/src/lib/record-filter.d.ts +18 -0
  186. package/dist/src/lib/record-filter.js +55 -0
  187. package/dist/src/lib/record-filter.js.map +1 -0
  188. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  189. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  190. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  191. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  192. package/dist/src/lib/resolve-init-server.js +77 -0
  193. package/dist/src/lib/resolve-init-server.js.map +1 -0
  194. package/dist/src/lib/resolve-platform.d.ts +43 -14
  195. package/dist/src/lib/resolve-platform.js +74 -12
  196. package/dist/src/lib/resolve-platform.js.map +1 -1
  197. package/dist/src/lib/run-status.d.ts +19 -0
  198. package/dist/src/lib/run-status.generated.d.ts +39 -0
  199. package/dist/src/lib/run-status.generated.js +66 -0
  200. package/dist/src/lib/run-status.generated.js.map +1 -0
  201. package/dist/src/lib/run-status.js +19 -0
  202. package/dist/src/lib/run-status.js.map +1 -0
  203. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  204. package/dist/src/lib/server-text-normalization.js +90 -0
  205. package/dist/src/lib/server-text-normalization.js.map +1 -0
  206. package/dist/src/lib/server-url.d.ts +22 -0
  207. package/dist/src/lib/server-url.js +33 -0
  208. package/dist/src/lib/server-url.js.map +1 -0
  209. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  210. package/dist/src/lib/signing-secret-status.js +116 -0
  211. package/dist/src/lib/signing-secret-status.js.map +1 -0
  212. package/dist/src/lib/skill-installer.js +6 -3
  213. package/dist/src/lib/skill-installer.js.map +1 -1
  214. package/dist/src/lib/snapshots.d.ts +2 -2
  215. package/dist/src/lib/snapshots.js +2 -2
  216. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
  217. package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
  218. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  219. package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
  220. package/dist/src/lib/swift-codegen/generator.js +181 -7
  221. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  222. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
  223. package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
  224. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
  225. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  226. package/dist/src/lib/sync-dir-selector.js +30 -0
  227. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  228. package/dist/src/lib/sync-paths.d.ts +39 -0
  229. package/dist/src/lib/sync-paths.js +70 -2
  230. package/dist/src/lib/sync-paths.js.map +1 -1
  231. package/dist/src/lib/sync-resource-types.d.ts +138 -31
  232. package/dist/src/lib/sync-resource-types.js +241 -47
  233. package/dist/src/lib/sync-resource-types.js.map +1 -1
  234. package/dist/src/lib/sync-selectors.d.ts +95 -0
  235. package/dist/src/lib/sync-selectors.js +228 -0
  236. package/dist/src/lib/sync-selectors.js.map +1 -0
  237. package/dist/src/lib/template.d.ts +62 -4
  238. package/dist/src/lib/template.js +147 -39
  239. package/dist/src/lib/template.js.map +1 -1
  240. package/dist/src/lib/test-case-keys.d.ts +29 -0
  241. package/dist/src/lib/test-case-keys.js +55 -0
  242. package/dist/src/lib/test-case-keys.js.map +1 -0
  243. package/dist/src/lib/test-case-variables.d.ts +15 -0
  244. package/dist/src/lib/test-case-variables.js +29 -0
  245. package/dist/src/lib/test-case-variables.js.map +1 -0
  246. package/dist/src/lib/toml-database-config.d.ts +4 -4
  247. package/dist/src/lib/toml-database-config.js +27 -34
  248. package/dist/src/lib/toml-database-config.js.map +1 -1
  249. package/dist/src/lib/toml-metadata-config.d.ts +9 -5
  250. package/dist/src/lib/toml-metadata-config.js +40 -45
  251. package/dist/src/lib/toml-metadata-config.js.map +1 -1
  252. package/dist/src/lib/toml-native-form.d.ts +1 -1
  253. package/dist/src/lib/toml-native-form.js +1 -1
  254. package/dist/src/lib/toml-params-validator.d.ts +1 -1
  255. package/dist/src/lib/toml-params-validator.js +1 -1
  256. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  257. package/dist/src/lib/toml-scalar-edit.js +283 -0
  258. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  259. package/dist/src/lib/user-selector.d.ts +24 -0
  260. package/dist/src/lib/user-selector.js +33 -0
  261. package/dist/src/lib/user-selector.js.map +1 -0
  262. package/dist/src/lib/version-check.d.ts +25 -0
  263. package/dist/src/lib/version-check.js +77 -8
  264. package/dist/src/lib/version-check.js.map +1 -1
  265. package/dist/src/lib/watch.d.ts +121 -0
  266. package/dist/src/lib/watch.js +169 -0
  267. package/dist/src/lib/watch.js.map +1 -0
  268. package/dist/src/lib/workflow-apply.d.ts +69 -45
  269. package/dist/src/lib/workflow-apply.js +99 -72
  270. package/dist/src/lib/workflow-apply.js.map +1 -1
  271. package/dist/src/lib/workflow-codegen/generator.js +4 -4
  272. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  273. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
  274. package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
  275. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  276. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  277. package/dist/src/lib/workflow-config-apply.js +137 -0
  278. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  279. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  280. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  281. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  282. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  283. package/dist/src/lib/workflow-defaults.js +41 -0
  284. package/dist/src/lib/workflow-defaults.js.map +1 -0
  285. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  286. package/dist/src/lib/workflow-include-preserve.js +286 -0
  287. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  288. package/dist/src/lib/workflow-payload.d.ts +31 -20
  289. package/dist/src/lib/workflow-payload.js +100 -110
  290. package/dist/src/lib/workflow-payload.js.map +1 -1
  291. package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
  292. package/dist/src/lib/workflow-toml-validator.js +141 -16
  293. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  294. package/dist/src/types/index.d.ts +78 -22
  295. package/dist/src/validators.d.ts +7 -7
  296. package/dist/src/validators.js +6 -6
  297. package/package.json +11 -6
  298. package/dist/src/commands/settings.d.ts +0 -15
  299. package/dist/src/commands/settings.js +0 -102
  300. package/dist/src/commands/settings.js.map +0 -1
@@ -1,12 +1,30 @@
1
- import { readFileSync, writeFileSync, statSync } from "fs";
2
- import { basename } from "path";
3
- import { parseConfigToml } from "../lib/config-toml.js";
4
- import { lookup as mimeLookup } from "mime-types";
1
+ import { writeFileSync } from "fs";
5
2
  import { ApiClient } from "../lib/api-client.js";
6
3
  import { resolveAppId } from "../lib/config.js";
7
4
  import chalk from "chalk";
8
5
  import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, progress, progressEnd, } from "../lib/output.js";
6
+ import { pollUntil } from "../lib/watch.js";
9
7
  import { confirmPrompt } from "../lib/confirm-prompt.js";
8
+ import { parseStatusFilter } from "../lib/object-status-filter.js";
9
+ import { formatAccessRuleForDisplay } from "../lib/access-rule-display.js";
10
+ /**
11
+ * Resolve a prompt reference — its server id, or the author-chosen key its
12
+ * TOML file is named for.
13
+ *
14
+ * Under TOML-only authoring the key is what an operator has: `prompts/<key>.toml`
15
+ * is the object, and the id is a server detail they never wrote down. Mirrors
16
+ * `resolveScriptId` in `scripts.ts`.
17
+ */
18
+ async function resolvePromptId(client, appId, ref) {
19
+ const { items } = await client.listPrompts(appId, { limit: 100 });
20
+ const byId = (items ?? []).find((p) => p.promptId === ref);
21
+ if (byId)
22
+ return byId.promptId;
23
+ const byKey = (items ?? []).find((p) => p.promptKey === ref);
24
+ if (byKey)
25
+ return byKey.promptId;
26
+ throw new Error(`Prompt not found: ${ref}`);
27
+ }
10
28
  export function registerPromptsCommands(program) {
11
29
  const prompts = program
12
30
  .command("prompts")
@@ -14,9 +32,19 @@ export function registerPromptsCommands(program) {
14
32
  .addHelpText("after", `
15
33
  Examples:
16
34
  $ primitive prompts list
17
- $ primitive prompts create --from-file summarizer.toml
35
+ $ primitive config create prompt summarizer # scaffold prompts/summarizer.toml
36
+ $ primitive config push --only prompt/summarizer # apply it
18
37
  $ primitive prompts execute 01HXY... --vars '{"text":"Hello world"}'
19
- $ primitive prompts configs activate 01HXY... 01ABC...
38
+ $ primitive prompts configs list summarizer # what the server runs
39
+ `);
40
+ // Server-side configuration is authored in TOML and applied with `config push`
41
+ // (issue #2645) — this group no longer carries config-setting flags.
42
+ prompts.addHelpText("after", `
43
+ Configuration lives in prompts/<key>.toml:
44
+ $ primitive config fields prompt # the file's keys, types and defaults
45
+ $ primitive config create prompt <key> # scaffold one locally
46
+ $ primitive config set prompt/<key> <path>=<value>
47
+ $ primitive config push --only prompt/<key>
20
48
  `);
21
49
  // List prompts
22
50
  prompts
@@ -24,14 +52,22 @@ Examples:
24
52
  .description("List prompts")
25
53
  .argument("[app-id]", "App ID (uses current app if not specified)")
26
54
  .option("--app <app-id>", "App ID")
27
- .option("--status <status>", "Filter by status: draft, active, archived")
55
+ .option("--status <status>", "Filter by status: active, inactive, archived")
28
56
  .option("--json", "Output as JSON")
29
57
  .action(async (appId, options) => {
30
58
  const resolvedAppId = resolveAppId(appId, options);
31
59
  const client = new ApiClient();
32
60
  try {
61
+ // #2803 — `draft` is gone; refuse it by name rather than returning the
62
+ // empty list a canonical filter would produce. #2887 added `archived`:
63
+ // the delete flow writes one now, so asking for the retired rows is a
64
+ // question with an answer rather than a 400.
33
65
  const { items } = await client.listPrompts(resolvedAppId, {
34
- status: options.status,
66
+ status: parseStatusFilter("prompts", options.status, [
67
+ "active",
68
+ "inactive",
69
+ "archived",
70
+ ]),
35
71
  });
36
72
  if (options.json) {
37
73
  json(items);
@@ -54,208 +90,154 @@ Examples:
54
90
  process.exit(1);
55
91
  }
56
92
  });
57
- // Create prompt
93
+ // #2645 criterion 9 / #2803 — the operational verbs. They are the only
94
+ // writers of `status`, which is server-owned and no longer a TOML key.
58
95
  prompts
59
- .command("create")
60
- .description("Create a new prompt")
61
- .argument("[app-id]", "App ID (uses current app if not specified)")
62
- .option("--app <app-id>", "App ID")
63
- .option("--key <key>", "Prompt key (unique identifier)")
64
- .option("--name <name>", "Display name")
65
- .option("--description <desc>", "Description")
66
- .option("--provider <provider>", "LLM provider: openrouter, gemini", "openrouter")
67
- .option("--model <model>", "Model name")
68
- .option("--system-prompt <prompt>", "System prompt")
69
- .option("--user-template <template>", "User prompt template")
70
- .option("--temperature <temp>", "Temperature (0-1)")
71
- .option("--max-tokens <n>", "Max output tokens")
72
- .option("--output-format <format>", "Output format: text, json")
73
- .option("--input-schema <json>", "Input schema as JSON")
74
- .option("--output-schema <json>", "Output schema as JSON")
75
- .option("--from-file <path>", "Load prompt from TOML file")
96
+ .command("disable")
97
+ .description("Take prompt out of service now. Operational, not configuration: `status` is server-owned and 'config push' can neither set nor clear it.")
98
+ .argument("<prompt-id>", "Prompt ID")
99
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
76
100
  .option("--json", "Output as JSON")
77
- .action(async (appId, options) => {
78
- const resolvedAppId = resolveAppId(appId, options);
101
+ .action(async (id, options) => {
102
+ const resolvedAppId = resolveAppId(undefined, options);
79
103
  const client = new ApiClient();
80
- let payload;
81
- if (options.fromFile) {
82
- try {
83
- const content = readFileSync(options.fromFile, "utf-8");
84
- const tomlData = parseConfigToml(content);
85
- const prompt = tomlData.prompt || tomlData;
86
- const config = tomlData.configs?.[0] || {};
87
- payload = {
88
- promptKey: prompt.key || prompt.promptKey,
89
- displayName: prompt.displayName || prompt.name,
90
- description: prompt.description,
91
- provider: config.provider || prompt.provider || "openrouter",
92
- model: config.model || prompt.model,
93
- systemPrompt: config.systemPrompt || prompt.systemPrompt,
94
- userPromptTemplate: config.userPromptTemplate || prompt.userPromptTemplate,
95
- temperature: config.temperature || prompt.temperature,
96
- maxTokens: config.maxTokens || prompt.maxTokens,
97
- outputFormat: config.outputFormat || prompt.outputFormat,
98
- inputSchema: prompt.inputSchema,
99
- outputSchema: prompt.outputSchema,
100
- };
101
- }
102
- catch (err) {
103
- error(`Failed to read TOML file: ${err.message}`);
104
- process.exit(1);
105
- }
106
- }
107
- else {
108
- if (!options.key || !options.name || !options.model || !options.userTemplate) {
109
- error("Required: --key, --name, --model, --user-template (or use --from-file)");
110
- process.exit(1);
111
- }
112
- payload = {
113
- promptKey: options.key,
114
- displayName: options.name,
115
- description: options.description,
116
- provider: options.provider,
117
- model: options.model,
118
- systemPrompt: options.systemPrompt,
119
- userPromptTemplate: options.userTemplate,
120
- temperature: options.temperature,
121
- maxTokens: options.maxTokens ? parseInt(options.maxTokens) : undefined,
122
- outputFormat: options.outputFormat,
123
- inputSchema: options.inputSchema,
124
- outputSchema: options.outputSchema,
125
- };
126
- }
127
104
  try {
128
- const result = await client.createPrompt(resolvedAppId, payload);
105
+ const result = await client.setPromptAvailability(resolvedAppId, id, "disable");
129
106
  if (options.json) {
130
107
  json(result);
131
108
  return;
132
109
  }
133
- success(`Prompt created: ${result.displayName}`);
134
- keyValue("Prompt ID", result.promptId);
135
- keyValue("Key", result.promptKey);
110
+ success(`Prompt disabled: ${result.promptKey || id}`);
136
111
  }
137
112
  catch (err) {
138
113
  error(err.message);
139
114
  process.exit(1);
140
115
  }
141
116
  });
142
- // Get prompt
143
117
  prompts
144
- .command("get")
145
- .description("Get prompt details")
118
+ .command("enable")
119
+ .description("Put prompt back in service.")
146
120
  .argument("<prompt-id>", "Prompt ID")
147
121
  .option("--app <app-id>", "App ID (uses current app if not specified)")
148
122
  .option("--json", "Output as JSON")
149
- .action(async (promptId, options) => {
123
+ .action(async (id, options) => {
150
124
  const resolvedAppId = resolveAppId(undefined, options);
151
125
  const client = new ApiClient();
152
126
  try {
153
- const result = await client.getPrompt(resolvedAppId, promptId);
127
+ const result = await client.setPromptAvailability(resolvedAppId, id, "enable");
154
128
  if (options.json) {
155
129
  json(result);
156
130
  return;
157
131
  }
158
- printResult("Prompt ID", result.promptId);
159
- printResult("Key", result.promptKey);
160
- printResult("Name", result.displayName);
161
- printResult("Description", result.description);
162
- printResult("Status", formatStatus(result.status));
163
- printResult("Active Config", result.activeConfigId || "-");
164
- if (result.configs && result.configs.length > 0) {
165
- divider();
166
- info(`Configs (${result.configs.length}):`);
167
- console.log(formatTable(result.configs, [
168
- { header: "ID", key: "configId", format: formatId },
169
- { header: "NAME", key: "configName" },
170
- { header: "MODEL", key: "model" },
171
- { header: "STATUS", key: "status", format: formatStatus },
172
- ]));
173
- }
132
+ success(`Prompt enabled: ${result.promptKey || id}`);
174
133
  }
175
134
  catch (err) {
176
135
  error(err.message);
177
136
  process.exit(1);
178
137
  }
179
138
  });
180
- // Update prompt
139
+ // #2907 — the delete flow's CLI spelling. Not a second writer of `status`:
140
+ // this is the plain `DELETE` #2887 made retire-with-history, which until now
141
+ // only the admin API and the console could reach. Named `archive`, not
142
+ // `delete`, because a configuration noun gets no `delete` verb
143
+ // (docs/cli-design.md) and prune-by-push stays the only CLI hard-delete path.
181
144
  prompts
182
- .command("update")
183
- .description("Update a prompt")
145
+ .command("archive")
146
+ .description("Retire a prompt (soft delete). Delete-lifecycle, not `disable`: the row and its configs are kept so executions still resolve, it goes on holding its key, and there is no un-archive.")
184
147
  .argument("<prompt-id>", "Prompt ID")
185
148
  .option("--app <app-id>", "App ID (uses current app if not specified)")
186
- .option("--name <name>", "Display name")
187
- .option("--description <desc>", "Description")
188
- .option("--status <status>", "Status: draft, active, archived")
189
- .option("--input-schema <json>", "Input schema as JSON")
190
- .option("--output-schema <json>", "Output schema as JSON")
149
+ .option("-y, --yes", "Skip confirmation prompt")
191
150
  .option("--json", "Output as JSON")
192
- .action(async (promptId, options) => {
151
+ .addHelpText("after", `
152
+ What archiving a prompt costs (retire-with-history):
153
+ The member endpoint refuses it, and so does admin execution — the diagnostic
154
+ runs an INACTIVE prompt on purpose, but not an archived one.
155
+ Its configs, executions and analytics keep resolving what they point at.
156
+ The archived row still holds its promptKey.
157
+ \`primitive prompts enable\` refuses an archived prompt; there is no
158
+ un-archive.
159
+ The per-version \`[[configs]] status\` is a different key and is untouched.
160
+
161
+ Reclaiming the key means hard-deleting the row:
162
+ $ rm prompts/<key>.toml
163
+ $ primitive config push --prune # confirmed; hard-deletes the row
164
+ # or hard-delete it in the console, then re-add the file and push.
165
+ `)
166
+ .action(async (id, options) => {
193
167
  const resolvedAppId = resolveAppId(undefined, options);
194
- const payload = {};
195
- if (options.name)
196
- payload.displayName = options.name;
197
- if (options.description)
198
- payload.description = options.description;
199
- if (options.status)
200
- payload.status = options.status;
201
- if (options.inputSchema)
202
- payload.inputSchema = options.inputSchema;
203
- if (options.outputSchema)
204
- payload.outputSchema = options.outputSchema;
205
- if (Object.keys(payload).length === 0) {
206
- error("No update options specified.");
207
- process.exit(1);
168
+ if (!options.yes) {
169
+ let confirmed;
170
+ try {
171
+ confirmed = await confirmPrompt(`Archive prompt ${id}? It is retired, its configs and executions are ` +
172
+ `kept, it goes on holding its key, and there is no un-archive.`);
173
+ }
174
+ catch (err) {
175
+ error(err.message);
176
+ process.exit(1);
177
+ }
178
+ if (!confirmed) {
179
+ info("Cancelled.");
180
+ return;
181
+ }
208
182
  }
209
183
  const client = new ApiClient();
210
184
  try {
211
- const result = await client.updatePrompt(resolvedAppId, promptId, payload);
185
+ // The soft delete: no `hard`, so the configs and stored prompt bodies
186
+ // survive and every execution row keeps resolving.
187
+ const result = await client.deletePrompt(resolvedAppId, id);
212
188
  if (options.json) {
213
189
  json(result);
214
190
  return;
215
191
  }
216
- success("Prompt updated.");
192
+ success(`Prompt archived: ${id}`);
193
+ info("Its configs and executions keep resolving. The row still holds its key — reclaim it by removing its file and running a confirmed `primitive config push --prune`.");
217
194
  }
218
195
  catch (err) {
219
196
  error(err.message);
220
197
  process.exit(1);
221
198
  }
222
199
  });
223
- // Delete prompt
224
200
  prompts
225
- .command("delete")
226
- .description("Delete or archive a prompt")
201
+ .command("get")
202
+ .description("Get prompt details")
227
203
  .argument("<prompt-id>", "Prompt ID")
228
204
  .option("--app <app-id>", "App ID (uses current app if not specified)")
229
- .option("--hard", "Permanently delete instead of archive")
230
- .option("-y, --yes", "Skip confirmation prompt")
205
+ .option("--json", "Output as JSON")
231
206
  .action(async (promptId, options) => {
232
207
  const resolvedAppId = resolveAppId(undefined, options);
233
- if (!options.yes) {
234
- const action = options.hard ? "permanently delete" : "archive";
235
- let confirm;
236
- try {
237
- confirm = await confirmPrompt(`Are you sure you want to ${action} prompt ${promptId}?`);
238
- }
239
- catch (err) {
240
- error(err.message);
241
- process.exit(1);
242
- }
243
- if (!confirm) {
244
- info("Cancelled.");
245
- return;
246
- }
247
- }
248
208
  const client = new ApiClient();
249
209
  try {
250
- await client.deletePrompt(resolvedAppId, promptId, options.hard);
251
- success(`Prompt ${options.hard ? "deleted" : "archived"}.`);
210
+ const result = await client.getPrompt(resolvedAppId, promptId);
211
+ if (options.json) {
212
+ json(result);
213
+ return;
214
+ }
215
+ printResult("Prompt ID", result.promptId);
216
+ printResult("Key", result.promptKey);
217
+ printResult("Name", result.displayName);
218
+ printResult("Description", result.description);
219
+ printResult("Status", formatStatus(result.status));
220
+ // #2652: no rule means every non-admin execution is denied. Print that
221
+ // in words — a blank or "-" would read as "unrestricted" and hide a
222
+ // prompt that was never given a rule.
223
+ printResult("Access Rule", formatAccessRuleForDisplay(result.accessRule, "prompt"));
224
+ printResult("Active Config", result.activeConfigId || "-");
225
+ if (result.configs && result.configs.length > 0) {
226
+ divider();
227
+ info(`Configs (${result.configs.length}):`);
228
+ console.log(formatTable(result.configs, [
229
+ { header: "ID", key: "configId", format: formatId },
230
+ { header: "NAME", key: "configName" },
231
+ { header: "MODEL", key: "model" },
232
+ { header: "STATUS", key: "status", format: formatStatus },
233
+ ]));
234
+ }
252
235
  }
253
236
  catch (err) {
254
237
  error(err.message);
255
238
  process.exit(1);
256
239
  }
257
240
  });
258
- // Execute prompt
259
241
  prompts
260
242
  .command("execute")
261
243
  .description("Execute a prompt with variables")
@@ -397,8 +379,25 @@ Examples:
397
379
  process.exit(1);
398
380
  }
399
381
  });
400
- // Configs subcommand
401
- const configs = prompts.command("configs").description("Manage prompt configs");
382
+ // Configs subcommand — reads only. A prompt's configs are `[[configs]]`
383
+ // entries in its own TOML file (#2645), so the verbs that used to write them
384
+ // were a second writer for state the file already owns.
385
+ const configs = prompts
386
+ .command("configs")
387
+ .description("Inspect a prompt's configs (authored in prompts/<key>.toml)")
388
+ .addHelpText("after", `
389
+ A prompt's configs are the \`[[configs]]\` entries of prompts/<key>.toml. Add,
390
+ edit, retire (\`status = "archived"\`) or mark one live (\`active = true\`) in that
391
+ file, then apply it:
392
+
393
+ $ primitive config push --only prompt/<key>
394
+
395
+ That per-config \`status\` says which VERSION is retired and is yours to author.
396
+ The PROMPT's own availability is server-owned and has no TOML key: use
397
+ 'primitive prompts disable' / 'enable'.
398
+
399
+ Duplicating a config is copying its \`[[configs]]\` block and renaming it.
400
+ `);
402
401
  // List configs
403
402
  configs
404
403
  .command("list")
@@ -432,154 +431,51 @@ Examples:
432
431
  process.exit(1);
433
432
  }
434
433
  });
435
- // Create config
434
+ // Get one config. `list` says which configs exist; this says what one of
435
+ // them actually runs — the body, provider and model the server would use.
436
+ // With `prompts configs create/update` gone, this is where that content is
437
+ // read back from.
436
438
  configs
437
- .command("create")
438
- .description("Create a new config for a prompt")
439
- .argument("<prompt-id>", "Prompt ID")
439
+ .command("get")
440
+ .description("Show one config of a prompt as the server has it")
441
+ .argument("<prompt>", "Prompt ID or key")
442
+ .argument("<config>", "Config ID or name")
440
443
  .option("--app <app-id>", "App ID (uses current app if not specified)")
441
- .option("--name <name>", "Config name")
442
- .option("--description <desc>", "Description")
443
- .option("--provider <provider>", "LLM provider: openrouter, gemini", "openrouter")
444
- .option("--model <model>", "Model name")
445
- .option("--system-prompt <prompt>", "System prompt")
446
- .option("--user-template <template>", "User prompt template")
447
- .option("--temperature <temp>", "Temperature (0-1)")
448
- .option("--max-tokens <n>", "Max output tokens")
449
- .option("--output-format <format>", "Output format: text, json")
450
444
  .option("--json", "Output as JSON")
451
- .action(async (promptId, options) => {
445
+ .action(async (prompt, config, options) => {
452
446
  const resolvedAppId = resolveAppId(undefined, options);
453
- if (!options.name || !options.model || !options.userTemplate) {
454
- error("Required: --name, --model, --user-template");
455
- process.exit(1);
456
- }
457
- const payload = {
458
- configName: options.name,
459
- description: options.description,
460
- provider: options.provider,
461
- model: options.model,
462
- systemPrompt: options.systemPrompt,
463
- userPromptTemplate: options.userTemplate,
464
- temperature: options.temperature,
465
- maxTokens: options.maxTokens ? parseInt(options.maxTokens) : undefined,
466
- outputFormat: options.outputFormat,
467
- };
468
447
  const client = new ApiClient();
469
448
  try {
470
- const result = await client.createPromptConfig(resolvedAppId, promptId, payload);
471
- if (options.json) {
472
- json(result);
473
- return;
449
+ const promptId = await resolvePromptId(client, resolvedAppId, prompt);
450
+ const { items } = await client.listPromptConfigs(resolvedAppId, promptId);
451
+ const summary = (items ?? []).find((c) => c.configId === config || c.configName === config);
452
+ if (!summary) {
453
+ error(`Prompt '${prompt}' has no config '${config}'. Run 'primitive prompts configs list ${prompt}' to see them.`);
454
+ process.exit(1);
474
455
  }
475
- success(`Config created: ${result.configName}`);
476
- keyValue("Config ID", result.configId);
477
- }
478
- catch (err) {
479
- error(err.message);
480
- process.exit(1);
481
- }
482
- });
483
- // Activate config
484
- configs
485
- .command("activate")
486
- .description("Set a config as the active config for a prompt")
487
- .argument("<prompt-id>", "Prompt ID")
488
- .argument("<config-id>", "Config ID")
489
- .option("--app <app-id>", "App ID (uses current app if not specified)")
490
- .action(async (promptId, configId, options) => {
491
- const resolvedAppId = resolveAppId(undefined, options);
492
- const client = new ApiClient();
493
- try {
494
- await client.activatePromptConfig(resolvedAppId, promptId, configId);
495
- success(`Config ${configId} activated.`);
496
- }
497
- catch (err) {
498
- error(err.message);
499
- process.exit(1);
500
- }
501
- });
502
- // Duplicate config
503
- configs
504
- .command("duplicate")
505
- .description("Duplicate a config")
506
- .argument("<prompt-id>", "Prompt ID")
507
- .argument("<config-id>", "Config ID to duplicate")
508
- .option("--app <app-id>", "App ID (uses current app if not specified)")
509
- .option("--name <name>", "New config name")
510
- .option("--json", "Output as JSON")
511
- .action(async (promptId, configId, options) => {
512
- const resolvedAppId = resolveAppId(undefined, options);
513
- const client = new ApiClient();
514
- try {
515
- const result = await client.duplicatePromptConfig(resolvedAppId, promptId, configId, {
516
- configName: options.name,
517
- });
456
+ const full = (await client.getPromptConfig(resolvedAppId, promptId, summary.configId)) ??
457
+ summary;
518
458
  if (options.json) {
519
- json(result);
459
+ json(full);
520
460
  return;
521
461
  }
522
- success(`Config duplicated: ${result.configName}`);
523
- keyValue("New Config ID", result.configId);
524
- }
525
- catch (err) {
526
- error(err.message);
527
- process.exit(1);
528
- }
529
- });
530
- // Update config
531
- configs
532
- .command("update")
533
- .description("Update a config")
534
- .argument("<prompt-id>", "Prompt ID")
535
- .argument("<config-id>", "Config ID")
536
- .option("--app <app-id>", "App ID (uses current app if not specified)")
537
- .option("--name <name>", "Config name")
538
- .option("--description <desc>", "Description")
539
- .option("--provider <provider>", "LLM provider: openrouter, gemini")
540
- .option("--model <model>", "Model name")
541
- .option("--system-prompt <prompt>", "System prompt")
542
- .option("--user-template <template>", "User prompt template")
543
- .option("--temperature <temp>", "Temperature (0-1)")
544
- .option("--max-tokens <n>", "Max output tokens")
545
- .option("--output-format <format>", "Output format: text, json")
546
- .option("--status <status>", "Status: active, archived")
547
- .option("--json", "Output as JSON")
548
- .action(async (promptId, configId, options) => {
549
- const resolvedAppId = resolveAppId(undefined, options);
550
- const payload = {};
551
- if (options.name)
552
- payload.configName = options.name;
553
- if (options.description)
554
- payload.description = options.description;
555
- if (options.provider)
556
- payload.provider = options.provider;
557
- if (options.model)
558
- payload.model = options.model;
559
- if (options.systemPrompt)
560
- payload.systemPrompt = options.systemPrompt;
561
- if (options.userTemplate)
562
- payload.userPromptTemplate = options.userTemplate;
563
- if (options.temperature)
564
- payload.temperature = options.temperature;
565
- if (options.maxTokens)
566
- payload.maxTokens = parseInt(options.maxTokens);
567
- if (options.outputFormat)
568
- payload.outputFormat = options.outputFormat;
569
- if (options.status)
570
- payload.status = options.status;
571
- if (Object.keys(payload).length === 0) {
572
- error("No update options specified.");
573
- process.exit(1);
574
- }
575
- const client = new ApiClient();
576
- try {
577
- const result = await client.updatePromptConfig(resolvedAppId, promptId, configId, payload);
578
- if (options.json) {
579
- json(result);
580
- return;
462
+ printResult("Config ID", full.configId);
463
+ printResult("Name", full.configName);
464
+ printResult("Status", formatStatus(full.status));
465
+ printResult("Provider", full.provider || "-");
466
+ printResult("Model", full.model || "-");
467
+ if (full.description)
468
+ printResult("Description", full.description);
469
+ if (full.systemPrompt) {
470
+ divider();
471
+ info("System prompt:");
472
+ console.log(full.systemPrompt);
473
+ }
474
+ if (full.userPrompt) {
475
+ divider();
476
+ info("User prompt:");
477
+ console.log(full.userPrompt);
581
478
  }
582
- success("Config updated.");
583
479
  }
584
480
  catch (err) {
585
481
  error(err.message);
@@ -591,11 +487,14 @@ Examples:
591
487
  // ============================================
592
488
  const tests = prompts
593
489
  .command("tests")
594
- .description("Manage and run prompt test cases")
490
+ .description("Run and inspect prompt test cases")
595
491
  .addHelpText("after", `
492
+ Test cases are authored in TOML at prompts/<key>.tests/<name>.toml and applied
493
+ with \`primitive config push\` (issue #2769); \`config fields prompt\` lists the
494
+ fields. Deleting a case is removing its file and running \`config push --prune\`.
495
+
596
496
  Examples:
597
497
  $ primitive prompts tests list <prompt-id>
598
- $ primitive prompts tests create <prompt-id> --name "Basic test" --vars '{"input":"hello"}'
599
498
  $ primitive prompts tests run <prompt-id> <test-case-id>
600
499
  $ primitive prompts tests run-all <prompt-id>
601
500
  $ primitive prompts tests run-all <prompt-id> --test-cases 01ABC,01DEF,01GHI
@@ -632,79 +531,6 @@ Examples:
632
531
  process.exit(1);
633
532
  }
634
533
  });
635
- // Create test case
636
- tests
637
- .command("create")
638
- .description("Create a test case for a prompt")
639
- .argument("<prompt-id>", "Prompt ID")
640
- .option("--app <app-id>", "App ID (uses current app if not specified)")
641
- .option("--name <name>", "Test case name (required)")
642
- .option("--vars <json>", "Input variables as JSON (required)")
643
- .option("--pattern <regex>", "Expected output pattern (regex)")
644
- .option("--contains <json>", "Expected strings to contain (JSON array)")
645
- .option("--json-subset <json>", "Expected JSON subset to match")
646
- .option("--config <config-id>", "Config ID to use for this test")
647
- .option("--evaluator-prompt <prompt-id>", "Evaluator prompt ID")
648
- .option("--evaluator-config <config-id>", "Evaluator config ID")
649
- .option("--json", "Output as JSON")
650
- .action(async (promptId, options) => {
651
- const resolvedAppId = resolveAppId(undefined, options);
652
- if (!options.name || !options.vars) {
653
- error("Required: --name and --vars");
654
- process.exit(1);
655
- }
656
- let inputVariables;
657
- try {
658
- inputVariables = JSON.parse(options.vars);
659
- }
660
- catch {
661
- error("Invalid JSON in --vars");
662
- process.exit(1);
663
- }
664
- let expectedOutputContains;
665
- if (options.contains) {
666
- try {
667
- expectedOutputContains = JSON.parse(options.contains);
668
- }
669
- catch {
670
- error("Invalid JSON in --contains");
671
- process.exit(1);
672
- }
673
- }
674
- let expectedJsonSubset;
675
- if (options.jsonSubset) {
676
- try {
677
- expectedJsonSubset = JSON.parse(options.jsonSubset);
678
- }
679
- catch {
680
- error("Invalid JSON in --json-subset");
681
- process.exit(1);
682
- }
683
- }
684
- const client = new ApiClient();
685
- try {
686
- const result = await client.createTestCase(resolvedAppId, "prompt", promptId, {
687
- name: options.name,
688
- inputVariables,
689
- expectedOutputPattern: options.pattern,
690
- expectedOutputContains,
691
- expectedJsonSubset,
692
- configId: options.config,
693
- evaluatorPromptId: options.evaluatorPrompt,
694
- evaluatorConfigId: options.evaluatorConfig,
695
- });
696
- if (options.json) {
697
- json(result);
698
- return;
699
- }
700
- success(`Test case created: ${result.name}`);
701
- keyValue("Test Case ID", result.testCaseId);
702
- }
703
- catch (err) {
704
- error(err.message);
705
- process.exit(1);
706
- }
707
- });
708
534
  // Get test case
709
535
  tests
710
536
  .command("get")
@@ -770,121 +596,6 @@ Examples:
770
596
  process.exit(1);
771
597
  }
772
598
  });
773
- // Update test case
774
- tests
775
- .command("update")
776
- .description("Update a test case")
777
- .argument("<prompt-id>", "Prompt ID")
778
- .argument("<test-case-id>", "Test Case ID")
779
- .option("--app <app-id>", "App ID (uses current app if not specified)")
780
- .option("--name <name>", "Test case name")
781
- .option("--vars <json>", "Input variables as JSON")
782
- .option("--pattern <regex>", "Expected output pattern (regex)")
783
- .option("--contains <json>", "Expected strings to contain (JSON array)")
784
- .option("--json-subset <json>", "Expected JSON subset to match")
785
- .option("--config <config-id>", "Config ID to use for this test")
786
- .option("--clear-pattern", "Clear expected pattern")
787
- .option("--clear-contains", "Clear expected contains")
788
- .option("--clear-json-subset", "Clear expected JSON subset")
789
- .option("--json", "Output as JSON")
790
- .action(async (promptId, testCaseId, options) => {
791
- const resolvedAppId = resolveAppId(undefined, options);
792
- const payload = {};
793
- if (options.name)
794
- payload.name = options.name;
795
- if (options.vars) {
796
- try {
797
- payload.inputVariables = JSON.parse(options.vars);
798
- }
799
- catch {
800
- error("Invalid JSON in --vars");
801
- process.exit(1);
802
- }
803
- }
804
- if (options.clearPattern) {
805
- payload.expectedOutputPattern = null;
806
- }
807
- else if (options.pattern) {
808
- payload.expectedOutputPattern = options.pattern;
809
- }
810
- if (options.clearContains) {
811
- payload.expectedOutputContains = null;
812
- }
813
- else if (options.contains) {
814
- try {
815
- payload.expectedOutputContains = JSON.parse(options.contains);
816
- }
817
- catch {
818
- error("Invalid JSON in --contains");
819
- process.exit(1);
820
- }
821
- }
822
- if (options.clearJsonSubset) {
823
- payload.expectedJsonSubset = null;
824
- }
825
- else if (options.jsonSubset) {
826
- try {
827
- payload.expectedJsonSubset = JSON.parse(options.jsonSubset);
828
- }
829
- catch {
830
- error("Invalid JSON in --json-subset");
831
- process.exit(1);
832
- }
833
- }
834
- if (options.config)
835
- payload.configId = options.config;
836
- if (Object.keys(payload).length === 0) {
837
- error("No update options specified.");
838
- process.exit(1);
839
- }
840
- const client = new ApiClient();
841
- try {
842
- const result = await client.updateTestCase(resolvedAppId, "prompt", promptId, testCaseId, payload);
843
- if (options.json) {
844
- json(result);
845
- return;
846
- }
847
- success("Test case updated.");
848
- }
849
- catch (err) {
850
- error(err.message);
851
- process.exit(1);
852
- }
853
- });
854
- // Delete test case
855
- tests
856
- .command("delete")
857
- .description("Delete a test case")
858
- .argument("<prompt-id>", "Prompt ID")
859
- .argument("<test-case-id>", "Test Case ID")
860
- .option("--app <app-id>", "App ID (uses current app if not specified)")
861
- .option("-y, --yes", "Skip confirmation prompt")
862
- .action(async (promptId, testCaseId, options) => {
863
- const resolvedAppId = resolveAppId(undefined, options);
864
- if (!options.yes) {
865
- let confirm;
866
- try {
867
- confirm = await confirmPrompt(`Are you sure you want to delete test case ${testCaseId}?`);
868
- }
869
- catch (err) {
870
- error(err.message);
871
- process.exit(1);
872
- }
873
- if (!confirm) {
874
- info("Cancelled.");
875
- return;
876
- }
877
- }
878
- const client = new ApiClient();
879
- try {
880
- await client.deleteTestCase(resolvedAppId, "prompt", promptId, testCaseId);
881
- success("Test case deleted.");
882
- }
883
- catch (err) {
884
- error(err.message);
885
- process.exit(1);
886
- }
887
- });
888
599
  // Run a single test case
889
600
  tests
890
601
  .command("run")
@@ -1146,12 +857,20 @@ Examples:
1146
857
  let result = await fetchStatus();
1147
858
  if (options.wait && result.status === "running") {
1148
859
  info("Waiting for batch completion...");
1149
- while (result.status === "running") {
1150
- await new Promise((r) => setTimeout(r, 2000));
1151
- result = await fetchStatus();
860
+ // #1969 — shared poll primitive (delay-then-probe every 2s, honoring
861
+ // an AbortSignal). `onError: "throw"` preserves the prior loop's
862
+ // fail-fast behavior: a throwing fetchStatus (expired token, deleted
863
+ // batch → 401/404) propagates to the command-level catch below and
864
+ // exits non-zero, rather than spinning forever with a frozen progress
865
+ // line. (The follow/watch tail loops legitimately keep "continue".)
866
+ const controller = new AbortController();
867
+ result = await pollUntil(fetchStatus, (r) => r.status !== "running", {
868
+ intervalMs: 2000,
869
+ signal: controller.signal,
870
+ onError: "throw",
1152
871
  // Progress goes to stderr so it can't corrupt JSON on stdout under --json.
1153
- progress(` Completed: ${result.completed}/${result.results?.length || 0} `);
1154
- }
872
+ onValue: (r) => progress(` Completed: ${r.completed}/${r.results?.length || 0} `),
873
+ });
1155
874
  progressEnd();
1156
875
  }
1157
876
  if (options.json) {
@@ -1243,13 +962,15 @@ Examples:
1243
962
  // ============================================
1244
963
  const attachments = tests
1245
964
  .command("attachments")
1246
- .description("Manage test case file attachments")
965
+ .description("Inspect test case file attachments")
1247
966
  .addHelpText("after", `
967
+ Attachments are authored as files in prompts/<key>.tests/<name>/ and applied
968
+ with \`primitive config push\` (#2769); removing one and running
969
+ \`config push --prune\` deletes it server-side. These verbs read server state.
970
+
1248
971
  Examples:
1249
972
  $ primitive prompts tests attachments list <prompt-id> <test-case-id>
1250
- $ primitive prompts tests attachments upload <prompt-id> <test-case-id> ./document.pdf
1251
973
  $ primitive prompts tests attachments download <prompt-id> <test-case-id> document.pdf
1252
- $ primitive prompts tests attachments delete <prompt-id> <test-case-id> document.pdf
1253
974
  `);
1254
975
  // List attachments
1255
976
  attachments
@@ -1287,49 +1008,6 @@ Examples:
1287
1008
  process.exit(1);
1288
1009
  }
1289
1010
  });
1290
- // Upload attachment
1291
- attachments
1292
- .command("upload")
1293
- .description("Upload a file attachment to a test case")
1294
- .argument("<prompt-id>", "Prompt ID")
1295
- .argument("<test-case-id>", "Test Case ID")
1296
- .argument("<file-path>", "Path to the file to upload")
1297
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1298
- .option("--name <name>", "Override the filename")
1299
- .option("--json", "Output as JSON")
1300
- .action(async (promptId, testCaseId, filePath, options) => {
1301
- const resolvedAppId = resolveAppId(undefined, options);
1302
- const client = new ApiClient();
1303
- try {
1304
- // Check file exists and get stats
1305
- const stats = statSync(filePath);
1306
- if (!stats.isFile()) {
1307
- error(`Not a file: ${filePath}`);
1308
- process.exit(1);
1309
- }
1310
- // Check file size (10 MB limit)
1311
- const TEN_MB = 10 * 1024 * 1024;
1312
- if (stats.size > TEN_MB) {
1313
- error(`File too large (max 10MB): ${formatFileSize(stats.size)}`);
1314
- process.exit(1);
1315
- }
1316
- const filename = options.name || basename(filePath);
1317
- const data = readFileSync(filePath);
1318
- const contentType = mimeLookup(filePath) || "application/octet-stream";
1319
- const result = await client.uploadTestCaseAttachment(resolvedAppId, "prompt", promptId, testCaseId, filename, data, contentType);
1320
- if (options.json) {
1321
- json(result);
1322
- return;
1323
- }
1324
- success(`Uploaded: ${filename}`);
1325
- keyValue("Size", formatFileSize(result.attachment.size));
1326
- keyValue("Type", result.attachment.contentType);
1327
- }
1328
- catch (err) {
1329
- error(err.message);
1330
- process.exit(1);
1331
- }
1332
- });
1333
1011
  // Download attachment
1334
1012
  attachments
1335
1013
  .command("download")
@@ -1355,41 +1033,6 @@ Examples:
1355
1033
  process.exit(1);
1356
1034
  }
1357
1035
  });
1358
- // Delete attachment
1359
- attachments
1360
- .command("delete")
1361
- .description("Delete a file attachment from a test case")
1362
- .argument("<prompt-id>", "Prompt ID")
1363
- .argument("<test-case-id>", "Test Case ID")
1364
- .argument("<filename>", "Name of the file to delete")
1365
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1366
- .option("-y, --yes", "Skip confirmation prompt")
1367
- .action(async (promptId, testCaseId, filename, options) => {
1368
- const resolvedAppId = resolveAppId(undefined, options);
1369
- if (!options.yes) {
1370
- let confirm;
1371
- try {
1372
- confirm = await confirmPrompt(`Are you sure you want to delete attachment "${filename}"?`);
1373
- }
1374
- catch (err) {
1375
- error(err.message);
1376
- process.exit(1);
1377
- }
1378
- if (!confirm) {
1379
- info("Cancelled.");
1380
- return;
1381
- }
1382
- }
1383
- const client = new ApiClient();
1384
- try {
1385
- await client.deleteTestCaseAttachment(resolvedAppId, "prompt", promptId, testCaseId, filename);
1386
- success(`Deleted: ${filename}`);
1387
- }
1388
- catch (err) {
1389
- error(err.message);
1390
- process.exit(1);
1391
- }
1392
- });
1393
1036
  }
1394
1037
  function formatFileSize(bytes) {
1395
1038
  if (bytes < 1024)